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