@nxuss/lemma 0.8.1 → 0.9.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.
Files changed (58) hide show
  1. package/README.md +144 -236
  2. package/dist/cjs/cli/lemma-proxy.d.ts.map +1 -1
  3. package/dist/cjs/cli/lemma-proxy.js +57 -119
  4. package/dist/cjs/cli/lemma-proxy.js.map +1 -1
  5. package/dist/cjs/mcp/tools.d.ts.map +1 -1
  6. package/dist/cjs/mcp/tools.js +868 -0
  7. package/dist/cjs/mcp/tools.js.map +1 -1
  8. package/dist/cjs/observability/IdeContextSync.js +3 -3
  9. package/dist/cjs/observability/IdeContextSync.js.map +1 -1
  10. package/dist/cjs/proxy/ProjectStore.d.ts +2 -0
  11. package/dist/cjs/proxy/ProjectStore.d.ts.map +1 -1
  12. package/dist/cjs/proxy/ProjectStore.js +3 -1
  13. package/dist/cjs/proxy/ProjectStore.js.map +1 -1
  14. package/dist/cjs/proxy/SseRelay.d.ts +4 -25
  15. package/dist/cjs/proxy/SseRelay.d.ts.map +1 -1
  16. package/dist/cjs/proxy/SseRelay.js +60 -63
  17. package/dist/cjs/proxy/SseRelay.js.map +1 -1
  18. package/dist/esm/cli/lemma-proxy.d.ts.map +1 -1
  19. package/dist/esm/cli/lemma-proxy.js +29 -91
  20. package/dist/esm/cli/lemma-proxy.js.map +1 -1
  21. package/dist/esm/mcp/tools.d.ts.map +1 -1
  22. package/dist/esm/mcp/tools.js +868 -0
  23. package/dist/esm/mcp/tools.js.map +1 -1
  24. package/dist/esm/observability/IdeContextSync.js +1 -1
  25. package/dist/esm/observability/IdeContextSync.js.map +1 -1
  26. package/dist/esm/proxy/ProjectStore.d.ts +2 -0
  27. package/dist/esm/proxy/ProjectStore.d.ts.map +1 -1
  28. package/dist/esm/proxy/ProjectStore.js +2 -0
  29. package/dist/esm/proxy/ProjectStore.js.map +1 -1
  30. package/dist/esm/proxy/SseRelay.d.ts +4 -25
  31. package/dist/esm/proxy/SseRelay.d.ts.map +1 -1
  32. package/dist/esm/proxy/SseRelay.js +60 -60
  33. package/dist/esm/proxy/SseRelay.js.map +1 -1
  34. package/package.json +1 -1
  35. package/dist/cjs/protocol/iap.d.ts +0 -54
  36. package/dist/cjs/protocol/iap.d.ts.map +0 -1
  37. package/dist/cjs/protocol/iap.js +0 -108
  38. package/dist/cjs/protocol/iap.js.map +0 -1
  39. package/dist/cjs/subconscious/cache.d.ts +0 -34
  40. package/dist/cjs/subconscious/cache.d.ts.map +0 -1
  41. package/dist/cjs/subconscious/cache.js +0 -156
  42. package/dist/cjs/subconscious/cache.js.map +0 -1
  43. package/dist/cjs/subconscious/embeddings.d.ts +0 -25
  44. package/dist/cjs/subconscious/embeddings.d.ts.map +0 -1
  45. package/dist/cjs/subconscious/embeddings.js +0 -65
  46. package/dist/cjs/subconscious/embeddings.js.map +0 -1
  47. package/dist/esm/protocol/iap.d.ts +0 -54
  48. package/dist/esm/protocol/iap.d.ts.map +0 -1
  49. package/dist/esm/protocol/iap.js +0 -104
  50. package/dist/esm/protocol/iap.js.map +0 -1
  51. package/dist/esm/subconscious/cache.d.ts +0 -34
  52. package/dist/esm/subconscious/cache.d.ts.map +0 -1
  53. package/dist/esm/subconscious/cache.js +0 -152
  54. package/dist/esm/subconscious/cache.js.map +0 -1
  55. package/dist/esm/subconscious/embeddings.d.ts +0 -25
  56. package/dist/esm/subconscious/embeddings.d.ts.map +0 -1
  57. package/dist/esm/subconscious/embeddings.js +0 -58
  58. package/dist/esm/subconscious/embeddings.js.map +0 -1
@@ -325,6 +325,84 @@ const toolDefinitions = [
325
325
  properties: {},
326
326
  },
327
327
  },
328
+ {
329
+ name: "entropy_score",
330
+ description: "Calcula la entropía matemática (complejidad) de uno o más archivos usando el TypeScript Compiler API. Sin LLM. Sin tokens. Devuelve: cyclomatic complexity, nesting depth, ratio de 'any', tamaño de funciones, y un score compuesto 0-100 (0=limpio, 100=caos puro). Usa esto para identificar qué archivos necesitan refactor ANTES de tocarlos.",
331
+ inputSchema: {
332
+ type: "object",
333
+ properties: {
334
+ filePath: { type: "string", description: "Archivo o directorio a analizar (relativo al workspace root)" },
335
+ topN: { type: "number", description: "Solo reportar los N archivos más caóticos (default: 10)", default: 10 },
336
+ },
337
+ },
338
+ },
339
+ {
340
+ name: "coupling_radar",
341
+ description: "Construye un grafo de acoplamiento entre módulos analizando imports/exports con el TypeScript Compiler. Sin LLM. Sin tokens. Detecta: ciclos de dependencia, módulos 'dios' (importados por todo), islas muertas (nadie los importa), y fan-in/fan-out por módulo. Esencial antes de refactors grandes.",
342
+ inputSchema: {
343
+ type: "object",
344
+ properties: {
345
+ dirPath: { type: "string", description: "Directorio a analizar (relativo al workspace root)", default: "src" },
346
+ minFanIn: { type: "number", description: "Reportar solo módulos importados por al menos N archivos", default: 3 },
347
+ },
348
+ },
349
+ },
350
+ {
351
+ name: "pattern_fossil",
352
+ description: "Detecta código zombie: patrones que ya fueron reemplazados en la mayoría del codebase pero siguen vivos en archivos viejos. Sin LLM. Sin tokens. Detecta: callbacks vs async/await, var vs let/const, require() vs import, any vs generics. Devuelve porcentaje de adopción del patrón nuevo vs. los fósiles que quedan.",
353
+ inputSchema: {
354
+ type: "object",
355
+ properties: {
356
+ dirPath: { type: "string", description: "Directorio a escanear (default: src)", default: "src" },
357
+ },
358
+ },
359
+ },
360
+ {
361
+ name: "git_heatmap_risk",
362
+ description: "Analiza el historial de git para identificar zonas de alto riesgo. Sin LLM. Sin tokens. Calcula: churn rate (frecuencia de cambios), co-edición oculta (archivos que siempre cambian juntos = acoplamiento implícito), y un risk score compuesto por archivo. Detecta los archivos que estadísticamente tienen más probabilidad de tener un bug.",
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: {
366
+ days: { type: "number", description: "Ventana de días de historial a analizar (default: 30)", default: 30 },
367
+ topN: { type: "number", description: "Top N archivos más riesgosos a reportar (default: 10)", default: 10 },
368
+ },
369
+ },
370
+ },
371
+ {
372
+ name: "precrime_static",
373
+ description: "MINORITY REPORT para tu código. Combina entropy_score + coupling_radar + git_heatmap_risk en un predictor de riesgo compuesto. Sin LLM. Sin tokens. Devuelve un ranking de archivos y funciones con mayor probabilidad de causar un bug, con justificación matemática de cada factor. Úsalo antes de un deploy o un PR review.",
374
+ inputSchema: {
375
+ type: "object",
376
+ properties: {
377
+ dirPath: { type: "string", description: "Directorio a analizar (default: src)", default: "src" },
378
+ days: { type: "number", description: "Ventana de días de historial git (default: 30)", default: 30 },
379
+ topN: { type: "number", description: "Top N archivos a reportar (default: 10)", default: 10 },
380
+ },
381
+ },
382
+ },
383
+ {
384
+ name: "semantic_dedup_guard",
385
+ description: "Firewall anti-redundancia para The Brain. Antes de hacer store_memory, pasa el contenido por aquí. Consulta ChromaDB localmente sin gastar tokens. Si hay un hit >= 92% → rechaza el store y devuelve el duplicado. Si 75-91% → advierte y muestra el similar. Mantiene The Brain denso y limpio.",
386
+ inputSchema: {
387
+ type: "object",
388
+ properties: {
389
+ query: { type: "string", description: "El query/prompt que ibas a almacenar" },
390
+ response: { type: "string", description: "La respuesta que ibas a almacenar" },
391
+ threshold: { type: "number", description: "Threshold de duplicado exacto (default: 0.92)", default: 0.92 },
392
+ },
393
+ required: ["query", "response"],
394
+ },
395
+ },
396
+ {
397
+ name: "dead_export_necromancer",
398
+ description: "Resucita el código muerto. Usa el TypeScript Compiler para mapear TODOS los exports del workspace y los cruza contra TODOS los imports. Lo que se exporta pero nadie importa = código zombie que está inflando tu contexto y desperdiciando tokens. Sin LLM. Sin tokens. Devuelve lista de exports muertos con estimación de tokens desperdiciados.",
399
+ inputSchema: {
400
+ type: "object",
401
+ properties: {
402
+ dirPath: { type: "string", description: "Directorio a analizar (default: src)", default: "src" },
403
+ },
404
+ },
405
+ },
328
406
  ];
329
407
  const toolHandlers = {
330
408
  scrub_privacy: handleScrubPrivacy,
@@ -352,6 +430,13 @@ const toolHandlers = {
352
430
  compress_context: handleCompressContext,
353
431
  smarter_cache: handleSmarterCache,
354
432
  token_budget: handleTokenBudget,
433
+ entropy_score: handleEntropyScore,
434
+ coupling_radar: handleCouplingRadar,
435
+ pattern_fossil: handlePatternFossil,
436
+ git_heatmap_risk: handleGitHeatmapRisk,
437
+ precrime_static: handlePrecrimeStatic,
438
+ semantic_dedup_guard: handleSemanticDedupGuard,
439
+ dead_export_necromancer: handleDeadExportNecromancer,
355
440
  };
356
441
  export function setupToolsHandlers(server, onToolCall) {
357
442
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -1451,4 +1536,787 @@ async function handleTokenBudget(_args) {
1451
1536
  }],
1452
1537
  };
1453
1538
  }
1539
+ function computeCyclomaticComplexity(node) {
1540
+ let count = 1; // base
1541
+ function walk(n) {
1542
+ switch (n.kind) {
1543
+ case ts.SyntaxKind.IfStatement:
1544
+ case ts.SyntaxKind.WhileStatement:
1545
+ case ts.SyntaxKind.ForStatement:
1546
+ case ts.SyntaxKind.ForInStatement:
1547
+ case ts.SyntaxKind.ForOfStatement:
1548
+ case ts.SyntaxKind.CaseClause:
1549
+ case ts.SyntaxKind.CatchClause:
1550
+ case ts.SyntaxKind.ConditionalExpression:
1551
+ count++;
1552
+ break;
1553
+ case ts.SyntaxKind.BinaryExpression: {
1554
+ const bin = n;
1555
+ if (bin.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ||
1556
+ bin.operatorToken.kind === ts.SyntaxKind.BarBarToken ||
1557
+ bin.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken)
1558
+ count++;
1559
+ break;
1560
+ }
1561
+ }
1562
+ ts.forEachChild(n, walk);
1563
+ }
1564
+ walk(node);
1565
+ return count;
1566
+ }
1567
+ function computeMaxNesting(node, depth = 0) {
1568
+ const nestingNodes = new Set([
1569
+ ts.SyntaxKind.IfStatement, ts.SyntaxKind.WhileStatement,
1570
+ ts.SyntaxKind.ForStatement, ts.SyntaxKind.ForInStatement,
1571
+ ts.SyntaxKind.ForOfStatement, ts.SyntaxKind.SwitchStatement,
1572
+ ts.SyntaxKind.TryStatement, ts.SyntaxKind.Block,
1573
+ ]);
1574
+ const isNesting = nestingNodes.has(node.kind);
1575
+ const currentDepth = isNesting ? depth + 1 : depth;
1576
+ let max = currentDepth;
1577
+ ts.forEachChild(node, child => {
1578
+ max = Math.max(max, computeMaxNesting(child, currentDepth));
1579
+ });
1580
+ return max;
1581
+ }
1582
+ function analyzeFileEntropy(filePath, relPath) {
1583
+ try {
1584
+ const src = fs.readFileSync(filePath, "utf8");
1585
+ const sourceFile = ts.createSourceFile(filePath, src, ts.ScriptTarget.Latest, true);
1586
+ const lines = src.split("\n");
1587
+ // Count 'any' tokens
1588
+ const anyCount = (src.match(/:\s*any\b/g) || []).length;
1589
+ const typeAnnotations = (src.match(/:\s*\w+/g) || []).length;
1590
+ const anyRatio = typeAnnotations > 0 ? anyCount / typeAnnotations : 0;
1591
+ const functions = [];
1592
+ function visitFunctions(node) {
1593
+ const isFn = ts.isFunctionDeclaration(node) ||
1594
+ ts.isMethodDeclaration(node) ||
1595
+ ts.isArrowFunction(node) ||
1596
+ ts.isFunctionExpression(node);
1597
+ if (isFn) {
1598
+ const nameNode = node.name;
1599
+ const name = nameNode ? nameNode.getText(sourceFile) : "<anonymous>";
1600
+ const start = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line;
1601
+ const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd()).line;
1602
+ const fnLines = end - start + 1;
1603
+ const cc = computeCyclomaticComplexity(node);
1604
+ const depth = computeMaxNesting(node);
1605
+ functions.push({ name, lines: fnLines, cyclomaticComplexity: cc, nestingDepth: depth });
1606
+ }
1607
+ ts.forEachChild(node, visitFunctions);
1608
+ }
1609
+ visitFunctions(sourceFile);
1610
+ const avgCC = functions.length > 0
1611
+ ? functions.reduce((s, f) => s + f.cyclomaticComplexity, 0) / functions.length
1612
+ : 1;
1613
+ const maxDepth = functions.length > 0
1614
+ ? Math.max(...functions.map(f => f.nestingDepth))
1615
+ : 0;
1616
+ const avgLines = functions.length > 0
1617
+ ? functions.reduce((s, f) => s + f.lines, 0) / functions.length
1618
+ : lines.length;
1619
+ // Entropy score: weighted composite 0-100
1620
+ const ccScore = Math.min(100, (avgCC / 15) * 40); // 40% weight
1621
+ const nestScore = Math.min(100, (maxDepth / 8) * 20); // 20% weight
1622
+ const anyScore = Math.min(100, anyRatio * 100) * 0.20; // 20% weight
1623
+ const sizeScore = Math.min(100, (avgLines / 80) * 100) * 0.20; // 20% weight
1624
+ const entropyScore = Math.round(ccScore + nestScore + anyScore + sizeScore);
1625
+ const verdict = entropyScore >= 80 ? "🔥 PELIGRO — Refactor urgente" :
1626
+ entropyScore >= 60 ? "⚠️ ALTO — Revisar antes de tocar" :
1627
+ entropyScore >= 40 ? "🟡 MEDIO — Manejable" :
1628
+ "✅ LIMPIO — Bajo riesgo";
1629
+ return {
1630
+ file: relPath,
1631
+ entropyScore,
1632
+ cyclomaticComplexity: Math.round(avgCC * 10) / 10,
1633
+ maxNestingDepth: maxDepth,
1634
+ anyRatio: Math.round(anyRatio * 1000) / 10,
1635
+ avgFunctionLines: Math.round(avgLines),
1636
+ functionCount: functions.length,
1637
+ functions: functions.sort((a, b) => b.cyclomaticComplexity - a.cyclomaticComplexity).slice(0, 5),
1638
+ verdict,
1639
+ };
1640
+ }
1641
+ catch {
1642
+ return null;
1643
+ }
1644
+ }
1645
+ async function handleEntropyScore(args) {
1646
+ const filePath = args?.filePath || "";
1647
+ const topN = args?.topN || 10;
1648
+ const workspaceRoot = process.cwd();
1649
+ try {
1650
+ const { resolved } = safeResolvePath(workspaceRoot, filePath);
1651
+ const isFile = fs.statSync(resolved).isFile();
1652
+ const filesToAnalyze = [];
1653
+ if (isFile) {
1654
+ filesToAnalyze.push({ abs: resolved, rel: filePath });
1655
+ }
1656
+ else {
1657
+ function walkForTs(dir, rel) {
1658
+ try {
1659
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1660
+ for (const e of entries) {
1661
+ if (["node_modules", ".git", "dist", "chroma_data", ".lemma"].includes(e.name))
1662
+ continue;
1663
+ const abs = path.join(dir, e.name);
1664
+ const relPath2 = rel ? path.join(rel, e.name) : e.name;
1665
+ if (e.isDirectory())
1666
+ walkForTs(abs, relPath2);
1667
+ else if (e.name.match(/\.(ts|tsx)$/) && !e.name.endsWith(".d.ts")) {
1668
+ filesToAnalyze.push({ abs, rel: relPath2 });
1669
+ }
1670
+ }
1671
+ }
1672
+ catch { }
1673
+ }
1674
+ walkForTs(resolved, filePath);
1675
+ }
1676
+ const results = filesToAnalyze
1677
+ .map(f => analyzeFileEntropy(f.abs, f.rel))
1678
+ .filter((r) => r !== null)
1679
+ .sort((a, b) => b.entropyScore - a.entropyScore)
1680
+ .slice(0, topN);
1681
+ const avgEntropy = results.length > 0
1682
+ ? Math.round(results.reduce((s, r) => s + r.entropyScore, 0) / results.length)
1683
+ : 0;
1684
+ const summary = [
1685
+ `# 🧮 Entropy Score Report`,
1686
+ `Archivos analizados: ${filesToAnalyze.length} | Top ${topN} mostrados | Entropía promedio: ${avgEntropy}/100`,
1687
+ ``,
1688
+ ...results.map((r, i) => `## ${i + 1}. ${r.file} — Score: ${r.entropyScore}/100 ${r.verdict}\n` +
1689
+ ` CC: ${r.cyclomaticComplexity} | Nesting: ${r.maxNestingDepth} | Any ratio: ${r.anyRatio}% | Avg fn: ${r.avgFunctionLines} líneas | Fns: ${r.functionCount}\n` +
1690
+ (r.functions.length > 0
1691
+ ? ` Top funciones caóticas: ${r.functions.slice(0, 3).map(f => `${f.name}(CC:${f.cyclomaticComplexity})`).join(", ")}`
1692
+ : "")),
1693
+ ].join("\n");
1694
+ return { content: [{ type: "text", text: summary }] };
1695
+ }
1696
+ catch (err) {
1697
+ logError("entropy_score", err);
1698
+ return { content: [{ type: "text", text: `Entropy score failed: ${err.message}` }] };
1699
+ }
1700
+ }
1701
+ function extractImports(filePath, relPath, workspaceRoot) {
1702
+ try {
1703
+ const src = fs.readFileSync(filePath, "utf8");
1704
+ const sourceFile = ts.createSourceFile(filePath, src, ts.ScriptTarget.Latest, true);
1705
+ const imports = [];
1706
+ ts.forEachChild(sourceFile, node => {
1707
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
1708
+ const spec = node.moduleSpecifier.text;
1709
+ if (spec.startsWith(".")) {
1710
+ // Resolve relative import to normalized path
1711
+ const dir = path.dirname(filePath);
1712
+ let resolved = path.resolve(dir, spec);
1713
+ // Try to find the actual file with extension
1714
+ for (const ext of [".ts", ".tsx", "/index.ts", "/index.tsx"]) {
1715
+ if (fs.existsSync(resolved + ext)) {
1716
+ resolved = resolved + ext;
1717
+ break;
1718
+ }
1719
+ }
1720
+ const rel = path.relative(workspaceRoot, resolved).replace(/\\/g, "/");
1721
+ imports.push(rel);
1722
+ }
1723
+ }
1724
+ });
1725
+ return imports;
1726
+ }
1727
+ catch {
1728
+ return [];
1729
+ }
1730
+ }
1731
+ async function handleCouplingRadar(args) {
1732
+ const dirPath = args?.dirPath || "src";
1733
+ const minFanIn = args?.minFanIn || 3;
1734
+ const workspaceRoot = process.cwd();
1735
+ try {
1736
+ const { resolved } = safeResolvePath(workspaceRoot, dirPath);
1737
+ const allFiles = [];
1738
+ function walkForTs2(dir, rel) {
1739
+ try {
1740
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1741
+ for (const e of entries) {
1742
+ if (["node_modules", ".git", "dist", "chroma_data", ".lemma"].includes(e.name))
1743
+ continue;
1744
+ const abs = path.join(dir, e.name);
1745
+ const relPath2 = rel ? path.join(rel, e.name).replace(/\\/g, "/") : e.name;
1746
+ if (e.isDirectory())
1747
+ walkForTs2(abs, relPath2);
1748
+ else if (e.name.match(/\.(ts|tsx)$/) && !e.name.endsWith(".d.ts")) {
1749
+ allFiles.push({ abs, rel: relPath2 });
1750
+ }
1751
+ }
1752
+ }
1753
+ catch { }
1754
+ }
1755
+ walkForTs2(resolved, dirPath);
1756
+ // Build adjacency map
1757
+ const moduleMap = new Map();
1758
+ for (const f of allFiles) {
1759
+ moduleMap.set(f.rel, { file: f.rel, imports: [], importedBy: [], fanIn: 0, fanOut: 0 });
1760
+ }
1761
+ for (const f of allFiles) {
1762
+ const imports = extractImports(f.abs, f.rel, workspaceRoot);
1763
+ const node = moduleMap.get(f.rel);
1764
+ for (const imp of imports) {
1765
+ if (moduleMap.has(imp)) {
1766
+ node.imports.push(imp);
1767
+ moduleMap.get(imp).importedBy.push(f.rel);
1768
+ }
1769
+ }
1770
+ }
1771
+ // Compute fan-in / fan-out
1772
+ for (const node of moduleMap.values()) {
1773
+ node.fanIn = node.importedBy.length;
1774
+ node.fanOut = node.imports.length;
1775
+ }
1776
+ // Detect cycles using DFS
1777
+ const cycles = [];
1778
+ const visited = new Set();
1779
+ const stack = new Set();
1780
+ function dfs(file, currentPath) {
1781
+ if (stack.has(file)) {
1782
+ const cycleStart = currentPath.indexOf(file);
1783
+ if (cycleStart >= 0)
1784
+ cycles.push(currentPath.slice(cycleStart).concat(file));
1785
+ return;
1786
+ }
1787
+ if (visited.has(file))
1788
+ return;
1789
+ visited.add(file);
1790
+ stack.add(file);
1791
+ const node = moduleMap.get(file);
1792
+ if (node) {
1793
+ for (const imp of node.imports)
1794
+ dfs(imp, [...currentPath, file]);
1795
+ }
1796
+ stack.delete(file);
1797
+ }
1798
+ for (const f of allFiles)
1799
+ dfs(f.rel, []);
1800
+ // High fan-in (god modules)
1801
+ const godModules = [...moduleMap.values()]
1802
+ .filter(n => n.fanIn >= minFanIn)
1803
+ .sort((a, b) => b.fanIn - a.fanIn)
1804
+ .slice(0, 10);
1805
+ // Dead islands (no fan-in, not an index/entry)
1806
+ const deadIslands = [...moduleMap.values()]
1807
+ .filter(n => n.fanIn === 0 && n.fanOut === 0 && !n.file.includes("index"))
1808
+ .slice(0, 10);
1809
+ const report = [
1810
+ `# 🕸️ Coupling Radar`,
1811
+ `Módulos analizados: ${moduleMap.size}`,
1812
+ ``,
1813
+ `## 🔄 Ciclos de dependencia detectados: ${cycles.length}`,
1814
+ cycles.slice(0, 5).map(c => ` ⚡ ${c.join(" → ")}`).join("\n") || " Ninguno detectado ✅",
1815
+ ``,
1816
+ `## 👁️ Módulos DIOS (fan-in >= ${minFanIn}):`,
1817
+ godModules.map(n => ` 🌐 ${n.file}\n fan-in: ${n.fanIn} importadores | fan-out: ${n.fanOut} dependencias`).join("\n") || " Ninguno",
1818
+ ``,
1819
+ `## 👻 Islas muertas (sin importadores y sin dependencias):`,
1820
+ deadIslands.map(n => ` 💀 ${n.file}`).join("\n") || " Ninguno ✅",
1821
+ ``,
1822
+ `## 📊 Top acoplados (mayor fan-in + fan-out):`,
1823
+ [...moduleMap.values()]
1824
+ .sort((a, b) => (b.fanIn + b.fanOut) - (a.fanIn + a.fanOut))
1825
+ .slice(0, 8)
1826
+ .map(n => ` ${n.file} → in:${n.fanIn} out:${n.fanOut}`)
1827
+ .join("\n"),
1828
+ ].join("\n");
1829
+ return { content: [{ type: "text", text: report }] };
1830
+ }
1831
+ catch (err) {
1832
+ logError("coupling_radar", err);
1833
+ return { content: [{ type: "text", text: `Coupling radar failed: ${err.message}` }] };
1834
+ }
1835
+ }
1836
+ const FOSSIL_PATTERNS = [
1837
+ {
1838
+ name: "async/await vs callbacks",
1839
+ modernPattern: /async\s+function|\basync\s*\(|await\s+/g,
1840
+ fossilPattern: /\.then\s*\(|\.catch\s*\(|callback\s*\(/g,
1841
+ modernLabel: "async/await",
1842
+ fossilLabel: "callbacks/.then",
1843
+ },
1844
+ {
1845
+ name: "const/let vs var",
1846
+ modernPattern: /\b(const|let)\s+\w+/g,
1847
+ fossilPattern: /\bvar\s+\w+/g,
1848
+ modernLabel: "const/let",
1849
+ fossilLabel: "var",
1850
+ },
1851
+ {
1852
+ name: "ESM import vs CommonJS require",
1853
+ modernPattern: /^import\s+/gm,
1854
+ fossilPattern: /\brequire\s*\(/g,
1855
+ modernLabel: "ESM import",
1856
+ fossilLabel: "require()",
1857
+ },
1858
+ {
1859
+ name: "Typed generics vs any",
1860
+ modernPattern: /<[A-Z]\w*>/g,
1861
+ fossilPattern: /:\s*any\b/g,
1862
+ modernLabel: "generics/types",
1863
+ fossilLabel: ": any",
1864
+ },
1865
+ {
1866
+ name: "Template literals vs concatenation",
1867
+ modernPattern: /`[^`]*\$\{/g,
1868
+ fossilPattern: /["']\s*\+\s*\w+\s*\+\s*["']/g,
1869
+ modernLabel: "template literals",
1870
+ fossilLabel: "string concatenation",
1871
+ },
1872
+ ];
1873
+ async function handlePatternFossil(args) {
1874
+ const dirPath = args?.dirPath || "src";
1875
+ const workspaceRoot = process.cwd();
1876
+ try {
1877
+ const { resolved } = safeResolvePath(workspaceRoot, dirPath);
1878
+ const allFiles = [];
1879
+ function walkForSource(dir, rel) {
1880
+ try {
1881
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
1882
+ for (const e of entries) {
1883
+ if (["node_modules", ".git", "dist", "chroma_data", ".lemma"].includes(e.name))
1884
+ continue;
1885
+ const abs = path.join(dir, e.name);
1886
+ const relPath2 = rel ? path.join(rel, e.name) : e.name;
1887
+ if (e.isDirectory())
1888
+ walkForSource(abs, relPath2);
1889
+ else if (e.name.match(/\.(ts|tsx|js|jsx)$/) && !e.name.endsWith(".d.ts")) {
1890
+ allFiles.push({ abs, rel: relPath2 });
1891
+ }
1892
+ }
1893
+ }
1894
+ catch { }
1895
+ }
1896
+ walkForSource(resolved, dirPath);
1897
+ const results = [];
1898
+ for (const fp of FOSSIL_PATTERNS) {
1899
+ let totalModern = 0;
1900
+ let totalFossil = 0;
1901
+ const fossilFiles = [];
1902
+ for (const f of allFiles) {
1903
+ try {
1904
+ const src = fs.readFileSync(f.abs, "utf8");
1905
+ const modernMatches = (src.match(fp.modernPattern) || []).length;
1906
+ const fossilMatches = (src.match(fp.fossilPattern) || []).length;
1907
+ totalModern += modernMatches;
1908
+ if (fossilMatches > 0) {
1909
+ totalFossil += fossilMatches;
1910
+ fossilFiles.push(`${f.rel} (${fossilMatches}x)`);
1911
+ }
1912
+ }
1913
+ catch { }
1914
+ }
1915
+ const total = totalModern + totalFossil;
1916
+ const adoptionPercent = total > 0 ? Math.round((totalModern / total) * 100) : 100;
1917
+ if (totalFossil > 0) {
1918
+ results.push({
1919
+ pattern: fp.name,
1920
+ modernCount: totalModern,
1921
+ fossilCount: totalFossil,
1922
+ adoptionPercent,
1923
+ fossilFiles: fossilFiles.slice(0, 5),
1924
+ });
1925
+ }
1926
+ }
1927
+ if (results.length === 0) {
1928
+ return { content: [{ type: "text", text: "✅ No se detectaron fósiles de código. Codebase moderno y consistente." }] };
1929
+ }
1930
+ const report = [
1931
+ `# 👻 Pattern Fossil Detector`,
1932
+ `Archivos escaneados: ${allFiles.length}`,
1933
+ ``,
1934
+ ...results.map(r => [
1935
+ `## ${r.adoptionPercent < 70 ? "🦕" : "⚠️"} ${r.pattern}`,
1936
+ `Adopción moderna: **${r.adoptionPercent}%** | Modern: ${r.modernCount} | Fósil: ${r.fossilCount}`,
1937
+ `Archivos con código fósil:`,
1938
+ r.fossilFiles.map(f => ` - ${f}`).join("\n"),
1939
+ ].join("\n")),
1940
+ ].join("\n\n");
1941
+ return { content: [{ type: "text", text: report }] };
1942
+ }
1943
+ catch (err) {
1944
+ logError("pattern_fossil", err);
1945
+ return { content: [{ type: "text", text: `Pattern fossil failed: ${err.message}` }] };
1946
+ }
1947
+ }
1948
+ // ─── git_heatmap_risk ─────────────────────────────────────────────────────────
1949
+ async function handleGitHeatmapRisk(args) {
1950
+ const days = args?.days || 30;
1951
+ const topN = args?.topN || 10;
1952
+ const workspaceRoot = process.cwd();
1953
+ try {
1954
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split("T")[0];
1955
+ // Get commit log with changed files
1956
+ let gitLog = "";
1957
+ try {
1958
+ gitLog = execSync(`git log --since="${since}" --name-only --pretty=format:"COMMIT:%H" --diff-filter=M`, { cwd: workspaceRoot, encoding: "utf8", timeout: 10000 });
1959
+ }
1960
+ catch {
1961
+ return { content: [{ type: "text", text: "⚠️ Git no disponible o repositorio sin historial suficiente." }] };
1962
+ }
1963
+ // Parse commit groups
1964
+ const commitGroups = [];
1965
+ let currentGroup = [];
1966
+ for (const line of gitLog.split("\n")) {
1967
+ const trimmed = line.trim();
1968
+ if (trimmed.startsWith("COMMIT:")) {
1969
+ if (currentGroup.length > 0)
1970
+ commitGroups.push(currentGroup);
1971
+ currentGroup = [];
1972
+ }
1973
+ else if (trimmed && !trimmed.startsWith("COMMIT:")) {
1974
+ currentGroup.push(trimmed);
1975
+ }
1976
+ }
1977
+ if (currentGroup.length > 0)
1978
+ commitGroups.push(currentGroup);
1979
+ // Churn rate per file
1980
+ const fileChurn = new Map();
1981
+ for (const group of commitGroups) {
1982
+ for (const file of group) {
1983
+ fileChurn.set(file, (fileChurn.get(file) || 0) + 1);
1984
+ }
1985
+ }
1986
+ // Co-change pairs (files changed together)
1987
+ const coChange = new Map();
1988
+ for (const group of commitGroups) {
1989
+ if (group.length < 2)
1990
+ continue;
1991
+ for (let i = 0; i < group.length; i++) {
1992
+ for (let j = i + 1; j < group.length; j++) {
1993
+ const key = [group[i], group[j]].sort().join(" <-> ");
1994
+ coChange.set(key, (coChange.get(key) || 0) + 1);
1995
+ }
1996
+ }
1997
+ }
1998
+ // Build risk scores
1999
+ const maxChurn = Math.max(...fileChurn.values(), 1);
2000
+ const riskScores = [...fileChurn.entries()]
2001
+ .map(([file, churn]) => ({
2002
+ file,
2003
+ churn,
2004
+ churnScore: Math.round((churn / maxChurn) * 100),
2005
+ riskScore: Math.round((churn / maxChurn) * 100),
2006
+ }))
2007
+ .sort((a, b) => b.riskScore - a.riskScore)
2008
+ .slice(0, topN);
2009
+ // Top co-change pairs
2010
+ const topCoChange = [...coChange.entries()]
2011
+ .filter(([, count]) => count >= 2)
2012
+ .sort((a, b) => b[1] - a[1])
2013
+ .slice(0, 5);
2014
+ const report = [
2015
+ `# 🔥 Git Heatmap Risk`,
2016
+ `Período: últimos ${days} días | Commits analizados: ${commitGroups.length} | Archivos rastreados: ${fileChurn.size}`,
2017
+ ``,
2018
+ `## 🌡️ Top ${topN} archivos con mayor churn (más cambian = más riesgo):`,
2019
+ riskScores.map((r, i) => ` ${i + 1}. ${r.file}\n Cambios: ${r.churn}x | Risk score: ${r.riskScore}/100 ${r.riskScore >= 70 ? "🔥" : r.riskScore >= 40 ? "⚠️" : "🟢"}`).join("\n"),
2020
+ ``,
2021
+ `## 🔗 Acoplamiento oculto (siempre cambian juntos):`,
2022
+ topCoChange.length > 0
2023
+ ? topCoChange.map(([pair, count]) => ` ⚡ ${pair}\n Co-editados ${count} veces — posible acoplamiento implícito`).join("\n")
2024
+ : " Ningún patrón de co-edición significativo detectado.",
2025
+ ].join("\n");
2026
+ return { content: [{ type: "text", text: report }] };
2027
+ }
2028
+ catch (err) {
2029
+ logError("git_heatmap_risk", err);
2030
+ return { content: [{ type: "text", text: `Git heatmap failed: ${err.message}` }] };
2031
+ }
2032
+ }
2033
+ // ─── precrime_static ──────────────────────────────────────────────────────────
2034
+ async function handlePrecrimeStatic(args) {
2035
+ const dirPath = args?.dirPath || "src";
2036
+ const days = args?.days || 30;
2037
+ const topN = args?.topN || 10;
2038
+ const workspaceRoot = process.cwd();
2039
+ try {
2040
+ // 1. Run entropy analysis
2041
+ const { resolved } = safeResolvePath(workspaceRoot, dirPath);
2042
+ const allFiles = [];
2043
+ function walkTs(dir, rel) {
2044
+ try {
2045
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
2046
+ for (const e of entries) {
2047
+ if (["node_modules", ".git", "dist", "chroma_data", ".lemma"].includes(e.name))
2048
+ continue;
2049
+ const abs = path.join(dir, e.name);
2050
+ const relPath2 = rel ? path.join(rel, e.name).replace(/\\/g, "/") : e.name;
2051
+ if (e.isDirectory())
2052
+ walkTs(abs, relPath2);
2053
+ else if (e.name.match(/\.(ts|tsx)$/) && !e.name.endsWith(".d.ts")) {
2054
+ allFiles.push({ abs, rel: relPath2 });
2055
+ }
2056
+ }
2057
+ }
2058
+ catch { }
2059
+ }
2060
+ walkTs(resolved, dirPath);
2061
+ const entropyResults = allFiles
2062
+ .map(f => analyzeFileEntropy(f.abs, f.rel))
2063
+ .filter((r) => r !== null);
2064
+ const entropyMap = new Map(entropyResults.map(r => [r.file, r.entropyScore]));
2065
+ // 2. Git churn
2066
+ const churnMap = new Map();
2067
+ try {
2068
+ const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split("T")[0];
2069
+ const gitLog = execSync(`git log --since="${since}" --name-only --pretty=format:"COMMIT:%H" --diff-filter=M`, { cwd: workspaceRoot, encoding: "utf8", timeout: 10000 });
2070
+ for (const line of gitLog.split("\n")) {
2071
+ const t = line.trim();
2072
+ if (t && !t.startsWith("COMMIT:")) {
2073
+ churnMap.set(t, (churnMap.get(t) || 0) + 1);
2074
+ }
2075
+ }
2076
+ }
2077
+ catch { }
2078
+ const maxChurn = Math.max(...churnMap.values(), 1);
2079
+ // 3. Coupling fan-in
2080
+ const fanInMap = new Map();
2081
+ for (const f of allFiles) {
2082
+ const imports = extractImports(f.abs, f.rel, workspaceRoot);
2083
+ for (const imp of imports) {
2084
+ fanInMap.set(imp, (fanInMap.get(imp) || 0) + 1);
2085
+ }
2086
+ }
2087
+ const maxFanIn = Math.max(...fanInMap.values(), 1);
2088
+ const precrimeResults = allFiles.map(f => {
2089
+ const entropy = entropyMap.get(f.rel) || 0;
2090
+ const churn = churnMap.get(f.rel) || 0;
2091
+ const fanIn = fanInMap.get(f.rel) || 0;
2092
+ const churnScore = Math.round((churn / maxChurn) * 100);
2093
+ const couplingScore = Math.round((fanIn / maxFanIn) * 100);
2094
+ // Weighted: entropy 40%, churn 40%, coupling 20%
2095
+ const precrimeScore = Math.round(entropy * 0.4 + churnScore * 0.4 + couplingScore * 0.2);
2096
+ const entropyResult = entropyResults.find(r => r.file === f.rel);
2097
+ const topRiskyFunctions = entropyResult?.functions
2098
+ .filter(fn => fn.cyclomaticComplexity >= 5)
2099
+ .slice(0, 3)
2100
+ .map(fn => `${fn.name}(CC:${fn.cyclomaticComplexity}, ${fn.lines}L)`) || [];
2101
+ const verdict = precrimeScore >= 75 ? "🚨 ARRESTO PREVENTIVO — Alto riesgo de bug" :
2102
+ precrimeScore >= 50 ? "⚠️ SOSPECHOSO — Revisar antes de merge" :
2103
+ precrimeScore >= 25 ? "🟡 VIGILANCIA — Bajo radar" :
2104
+ "✅ LIMPIO";
2105
+ return { file: f.rel, precrimeScore, entropyScore: entropy, churnScore, couplingScore, verdict, topRiskyFunctions };
2106
+ });
2107
+ const topThreats = precrimeResults
2108
+ .sort((a, b) => b.precrimeScore - a.precrimeScore)
2109
+ .slice(0, topN);
2110
+ const report = [
2111
+ `# 🔮 PreCrime Static — Minority Report para tu Código`,
2112
+ `Archivos bajo análisis: ${allFiles.length} | Período git: ${days}d | Factores: Entropía 40% + Churn 40% + Acoplamiento 20%`,
2113
+ ``,
2114
+ `## 🚨 Predicciones de riesgo:`,
2115
+ ``,
2116
+ ...topThreats.map((r, i) => [
2117
+ `### ${i + 1}. ${r.file}`,
2118
+ `**PreCrime Score: ${r.precrimeScore}/100** — ${r.verdict}`,
2119
+ `| Factor | Score |`,
2120
+ `|--------|-------|`,
2121
+ `| 🧮 Entropía (40%) | ${r.entropyScore}/100 |`,
2122
+ `| 🔥 Churn git (40%) | ${r.churnScore}/100 |`,
2123
+ `| 🕸️ Acoplamiento (20%) | ${r.couplingScore}/100 |`,
2124
+ r.topRiskyFunctions.length > 0
2125
+ ? `\n**Funciones sospechosas:** ${r.topRiskyFunctions.join(", ")}`
2126
+ : "",
2127
+ ].join("\n")),
2128
+ ].join("\n");
2129
+ return { content: [{ type: "text", text: report }] };
2130
+ }
2131
+ catch (err) {
2132
+ logError("precrime_static", err);
2133
+ return { content: [{ type: "text", text: `PreCrime static failed: ${err.message}` }] };
2134
+ }
2135
+ }
2136
+ // ─── semantic_dedup_guard ─────────────────────────────────────────────────────
2137
+ async function handleSemanticDedupGuard(args) {
2138
+ const query = args?.query;
2139
+ const response = args?.response;
2140
+ const threshold = typeof args?.threshold === "number" ? args.threshold : 0.92;
2141
+ if (!query || !response)
2142
+ throw new Error("query and response are required");
2143
+ const port = getProxyPort();
2144
+ try {
2145
+ const searchRes = await axios.get(`http://localhost:${port}/api/search?q=${encodeURIComponent(query)}&limit=3`);
2146
+ const results = searchRes.data.results || [];
2147
+ const topHit = results[0];
2148
+ if (!topHit) {
2149
+ return {
2150
+ content: [{ type: "text", text: JSON.stringify({
2151
+ verdict: "STORE_ALLOWED",
2152
+ reason: "The Brain está vacío para esta query. Procede con store_memory.",
2153
+ similarity: 0,
2154
+ }, null, 2) }],
2155
+ };
2156
+ }
2157
+ const sim = topHit.similarity || 0;
2158
+ if (sim >= threshold) {
2159
+ return {
2160
+ content: [{ type: "text", text: JSON.stringify({
2161
+ verdict: "DUPLICATE_REJECTED",
2162
+ reason: `Duplicado exacto detectado (${(sim * 100).toFixed(1)}% ≥ ${(threshold * 100).toFixed(0)}%). NO hagas store_memory — ya existe.`,
2163
+ similarity: sim,
2164
+ existingQuery: typeof topHit.prompt === "string" ? topHit.prompt.substring(0, 200) : "N/A",
2165
+ existingResponse: (typeof topHit.response === "string"
2166
+ ? topHit.response
2167
+ : topHit.response?.choices?.[0]?.message?.content || "").substring(0, 300),
2168
+ tokensSaved: Math.floor(response.length / 4),
2169
+ }, null, 2) }],
2170
+ };
2171
+ }
2172
+ if (sim >= 0.75) {
2173
+ return {
2174
+ content: [{ type: "text", text: JSON.stringify({
2175
+ verdict: "SIMILAR_WARNING",
2176
+ reason: `Contenido similar detectado (${(sim * 100).toFixed(1)}%). Decide si es distinto suficiente para guardar.`,
2177
+ similarity: sim,
2178
+ existingQuery: typeof topHit.prompt === "string" ? topHit.prompt.substring(0, 200) : "N/A",
2179
+ recommendation: "Si aporta info nueva → procede con store_memory. Si es redundante → descarta.",
2180
+ }, null, 2) }],
2181
+ };
2182
+ }
2183
+ return {
2184
+ content: [{ type: "text", text: JSON.stringify({
2185
+ verdict: "STORE_ALLOWED",
2186
+ reason: `Contenido único (${(sim * 100).toFixed(1)}% de similitud con lo más cercano). Procede con store_memory.`,
2187
+ similarity: sim,
2188
+ }, null, 2) }],
2189
+ };
2190
+ }
2191
+ catch (e) {
2192
+ logError("semantic_dedup_guard", e);
2193
+ return {
2194
+ content: [{ type: "text", text: JSON.stringify({
2195
+ verdict: "STORE_ALLOWED",
2196
+ reason: "No se pudo consultar The Brain (proxy no disponible). Procediendo con store.",
2197
+ error: e.message,
2198
+ }, null, 2) }],
2199
+ };
2200
+ }
2201
+ }
2202
+ // ─── dead_export_necromancer ──────────────────────────────────────────────────
2203
+ async function handleDeadExportNecromancer(args) {
2204
+ const dirPath = args?.dirPath || "src";
2205
+ const workspaceRoot = process.cwd();
2206
+ try {
2207
+ const { resolved } = safeResolvePath(workspaceRoot, dirPath);
2208
+ const allFiles = [];
2209
+ function walkForNecro(dir, rel) {
2210
+ try {
2211
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
2212
+ for (const e of entries) {
2213
+ if (["node_modules", ".git", "dist", "chroma_data", ".lemma"].includes(e.name))
2214
+ continue;
2215
+ const abs = path.join(dir, e.name);
2216
+ const relPath2 = rel ? path.join(rel, e.name).replace(/\\/g, "/") : e.name;
2217
+ if (e.isDirectory())
2218
+ walkForNecro(abs, relPath2);
2219
+ else if (e.name.match(/\.(ts|tsx)$/) && !e.name.endsWith(".d.ts")) {
2220
+ allFiles.push({ abs, rel: relPath2 });
2221
+ }
2222
+ }
2223
+ }
2224
+ catch { }
2225
+ }
2226
+ walkForNecro(resolved, dirPath);
2227
+ const allExports = [];
2228
+ for (const f of allFiles) {
2229
+ try {
2230
+ const src = fs.readFileSync(f.abs, "utf8");
2231
+ const sourceFile = ts.createSourceFile(f.abs, src, ts.ScriptTarget.Latest, true);
2232
+ function visitExports(node) {
2233
+ const mods = ts.canHaveModifiers(node) ? ts.getModifiers(node) : undefined;
2234
+ const isExported = mods?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
2235
+ if (isExported) {
2236
+ const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
2237
+ if (ts.isFunctionDeclaration(node) && node.name) {
2238
+ allExports.push({ file: f.rel, name: node.name.text, line, kind: "function" });
2239
+ }
2240
+ else if (ts.isClassDeclaration(node) && node.name) {
2241
+ allExports.push({ file: f.rel, name: node.name.text, line, kind: "class" });
2242
+ }
2243
+ else if (ts.isInterfaceDeclaration(node) && node.name) {
2244
+ allExports.push({ file: f.rel, name: node.name.text, line, kind: "interface" });
2245
+ }
2246
+ else if (ts.isTypeAliasDeclaration(node) && node.name) {
2247
+ allExports.push({ file: f.rel, name: node.name.text, line, kind: "type" });
2248
+ }
2249
+ else if (ts.isVariableStatement(node)) {
2250
+ for (const decl of node.declarationList.declarations) {
2251
+ if (ts.isIdentifier(decl.name)) {
2252
+ allExports.push({ file: f.rel, name: decl.name.text, line, kind: "const" });
2253
+ }
2254
+ }
2255
+ }
2256
+ }
2257
+ ts.forEachChild(node, visitExports);
2258
+ }
2259
+ visitExports(sourceFile);
2260
+ }
2261
+ catch { }
2262
+ }
2263
+ // Collect all imported names across the workspace
2264
+ const importedNames = new Set();
2265
+ for (const f of allFiles) {
2266
+ try {
2267
+ const src = fs.readFileSync(f.abs, "utf8");
2268
+ const sourceFile = ts.createSourceFile(f.abs, src, ts.ScriptTarget.Latest, true);
2269
+ ts.forEachChild(sourceFile, node => {
2270
+ if (ts.isImportDeclaration(node) && node.importClause) {
2271
+ const clause = node.importClause;
2272
+ if (clause.name)
2273
+ importedNames.add(clause.name.text); // default import
2274
+ if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) {
2275
+ for (const spec of clause.namedBindings.elements) {
2276
+ importedNames.add(spec.name.text);
2277
+ }
2278
+ }
2279
+ }
2280
+ });
2281
+ }
2282
+ catch { }
2283
+ }
2284
+ // Find dead exports
2285
+ const deadExports = allExports.filter(e => !importedNames.has(e.name));
2286
+ // Estimate tokens wasted (approximate: name + type annotation + function body avg ~50 tokens per export)
2287
+ const estimatedWastedTokens = deadExports.length * 50;
2288
+ if (deadExports.length === 0) {
2289
+ return {
2290
+ content: [{ type: "text", text: "✅ No se encontraron exports muertos. Cada export tiene al menos un importador." }],
2291
+ };
2292
+ }
2293
+ // Group by file
2294
+ const byFile = new Map();
2295
+ for (const e of deadExports) {
2296
+ if (!byFile.has(e.file))
2297
+ byFile.set(e.file, []);
2298
+ byFile.get(e.file).push(e);
2299
+ }
2300
+ const report = [
2301
+ `# 💀 Dead Export Necromancer`,
2302
+ `Exports totales: ${allExports.length} | Exports muertos: ${deadExports.length} | ~${estimatedWastedTokens} tokens desperdiciados en contexto`,
2303
+ ``,
2304
+ `## 🪦 Código zombie por archivo:`,
2305
+ [...byFile.entries()]
2306
+ .sort((a, b) => b[1].length - a[1].length)
2307
+ .map(([file, exports]) => `### ${file} (${exports.length} exports muertos)\n` +
2308
+ exports.map(e => ` 💀 \`${e.name}\` [${e.kind}] — línea ${e.line}`).join("\n"))
2309
+ .join("\n\n"),
2310
+ ``,
2311
+ `## 💡 Recomendación:`,
2312
+ `Eliminar estos ${deadExports.length} exports muertos liberaría ~${estimatedWastedTokens} tokens de contexto por sesión.`,
2313
+ `Verifica que no sean parte de la API pública del paquete antes de eliminar.`,
2314
+ ].join("\n");
2315
+ return { content: [{ type: "text", text: report }] };
2316
+ }
2317
+ catch (err) {
2318
+ logError("dead_export_necromancer", err);
2319
+ return { content: [{ type: "text", text: `Dead export necromancer failed: ${err.message}` }] };
2320
+ }
2321
+ }
1454
2322
  //# sourceMappingURL=tools.js.map