hanuscode 1.0.0__py3-none-any.whl

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 (93) hide show
  1. hanus/__init__.py +5 -0
  2. hanus/__main__.py +10 -0
  3. hanus/action_handlers.py +76 -0
  4. hanus/action_parser.py +82 -0
  5. hanus/agent_runner.py +1445 -0
  6. hanus/analysis/__init__.py +5 -0
  7. hanus/analysis/debt.py +702 -0
  8. hanus/analysis/dependencies.py +475 -0
  9. hanus/cache/__init__.py +5 -0
  10. hanus/cache/response_cache.py +560 -0
  11. hanus/config.py +401 -0
  12. hanus/connectors/__init__.py +19 -0
  13. hanus/connectors/base.py +114 -0
  14. hanus/connectors/claude_connector.py +146 -0
  15. hanus/connectors/gemini_connector.py +141 -0
  16. hanus/connectors/glm_connector.py +160 -0
  17. hanus/connectors/ollama_connector.py +174 -0
  18. hanus/connectors/openai_connector.py +122 -0
  19. hanus/connectors/registry.py +26 -0
  20. hanus/context/__init__.py +7 -0
  21. hanus/context/manager.py +837 -0
  22. hanus/context/selective.py +626 -0
  23. hanus/error_recovery/__init__.py +5 -0
  24. hanus/error_recovery/auto_fix.py +605 -0
  25. hanus/hooks/__init__.py +5 -0
  26. hanus/hooks/manager.py +247 -0
  27. hanus/instincts/__init__.py +44 -0
  28. hanus/instincts/cli.py +372 -0
  29. hanus/instincts/detector.py +281 -0
  30. hanus/instincts/evolver.py +361 -0
  31. hanus/instincts/manager.py +343 -0
  32. hanus/instincts/types.py +253 -0
  33. hanus/logger.py +81 -0
  34. hanus/memory/__init__.py +8 -0
  35. hanus/memory/manager.py +265 -0
  36. hanus/memory/types.py +119 -0
  37. hanus/monitor.py +341 -0
  38. hanus/parallel/__init__.py +5 -0
  39. hanus/parallel/executor.py +300 -0
  40. hanus/permissions.py +182 -0
  41. hanus/plan/__init__.py +8 -0
  42. hanus/plan/mode.py +267 -0
  43. hanus/plan/models.py +152 -0
  44. hanus/plugin_manager.py +754 -0
  45. hanus/plugin_registry.py +391 -0
  46. hanus/plugins/__init__.py +1 -0
  47. hanus/plugins/arena.py +630 -0
  48. hanus/plugins/code_review.py +123 -0
  49. hanus/plugins/cortex.py +1750 -0
  50. hanus/plugins/deps_check.py +27 -0
  51. hanus/plugins/git_ops.py +33 -0
  52. hanus/plugins/metasploit.py +530 -0
  53. hanus/plugins/notes.py +583 -0
  54. hanus/plugins/search_code.py +59 -0
  55. hanus/plugins/searchsploit.py +495 -0
  56. hanus/plugins/strategist.py +175 -0
  57. hanus/plugins/webui.py +5200 -0
  58. hanus/profiles.py +479 -0
  59. hanus/profiles_builtin/__init__.py +0 -0
  60. hanus/profiles_builtin/architect/profile.yaml +12 -0
  61. hanus/profiles_builtin/architect/system_prompt.txt +71 -0
  62. hanus/profiles_builtin/deep/profile.yaml +12 -0
  63. hanus/profiles_builtin/deep/system_prompt.txt +66 -0
  64. hanus/profiles_builtin/developer/__init__.py +0 -0
  65. hanus/profiles_builtin/developer/profile.yaml +9 -0
  66. hanus/profiles_builtin/developer/system_prompt.txt +176 -0
  67. hanus/profiles_builtin/speed/profile.yaml +12 -0
  68. hanus/profiles_builtin/speed/system_prompt.txt +51 -0
  69. hanus/project_tools.py +177 -0
  70. hanus/query_engine.py +1594 -0
  71. hanus/rules/__init__.py +237 -0
  72. hanus/search/__init__.py +5 -0
  73. hanus/search/semantic.py +596 -0
  74. hanus/session_manager.py +547 -0
  75. hanus/skill_manager.py +702 -0
  76. hanus/skills/__init__.py +4 -0
  77. hanus/subagent/__init__.py +8 -0
  78. hanus/subagent/agents/__init__.py +253 -0
  79. hanus/subagent/manager.py +309 -0
  80. hanus/subagent/types.py +266 -0
  81. hanus/suggestions/__init__.py +5 -0
  82. hanus/suggestions/proactive.py +451 -0
  83. hanus/tasks/__init__.py +8 -0
  84. hanus/tasks/manager.py +330 -0
  85. hanus/tasks/models.py +106 -0
  86. hanus/terminal_prompt.py +166 -0
  87. hanus/tools.py +1849 -0
  88. hanus/ui.py +939 -0
  89. hanuscode-1.0.0.dist-info/METADATA +1151 -0
  90. hanuscode-1.0.0.dist-info/RECORD +93 -0
  91. hanuscode-1.0.0.dist-info/WHEEL +5 -0
  92. hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
  93. hanuscode-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,5 @@
1
+ # hanus/analysis/__init__.py
2
+ """Code analysis tools for HanusCode."""
3
+ from .dependencies import DependencyAnalyzer, DependencyGraph, DependencyNode
4
+
5
+ __all__ = ["DependencyAnalyzer", "DependencyGraph", "DependencyNode"]
hanus/analysis/debt.py ADDED
@@ -0,0 +1,702 @@
1
+ # hanus/analysis/debt.py
2
+ """
3
+ Análisis de deuda técnica.
4
+
5
+ Detecta código muerto, complejidad ciclomática,
6
+ score de mantenibilidad y sugiere refactors.
7
+ """
8
+ from __future__ import annotations
9
+ import ast
10
+ import re
11
+ from dataclasses import dataclass, field
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional, Any, Set, Tuple
14
+ from enum import Enum
15
+ from collections import defaultdict
16
+
17
+
18
+ class DebtType(Enum):
19
+ """Tipo de deuda técnica."""
20
+ CODE_DUPLICATION = "code_duplication"
21
+ DEAD_CODE = "dead_code"
22
+ HIGH_COMPLEXITY = "high_complexity"
23
+ LONG_FUNCTION = "long_function"
24
+ DEEP_NESTING = "deep_nesting"
25
+ TOO_MANY_PARAMETERS = "too_many_parameters"
26
+ MISSING_TESTS = "missing_tests"
27
+ UNUSED_IMPORTS = "unused_imports"
28
+ UNUSED_VARIABLES = "unused_variables"
29
+ MAGIC_NUMBERS = "magic_numbers"
30
+ TODO_COMMENTS = "todo_comments"
31
+ HARDCODED_VALUES = "hardcoded_values"
32
+ GOD_CLASS = "god_class"
33
+ FEATURE_ENVY = "feature_envy"
34
+ LONG_PARAMETER_LIST = "long_parameter_list"
35
+
36
+
37
+ class DebtSeverity(Enum):
38
+ """Severidad de la deuda."""
39
+ LOW = 1
40
+ MEDIUM = 2
41
+ HIGH = 3
42
+ CRITICAL = 4
43
+
44
+
45
+ @dataclass
46
+ class DebtIssue:
47
+ """Un problema de deuda técnica detectado."""
48
+ id: str
49
+ type: DebtType
50
+ severity: DebtSeverity
51
+ file_path: str
52
+ line_start: int
53
+ line_end: int
54
+ description: str
55
+ suggestion: str
56
+ effort_minutes: int # Estimación de esfuerzo para arreglar
57
+ metadata: Dict[str, Any] = field(default_factory=dict)
58
+
59
+ def to_dict(self) -> Dict:
60
+ return {
61
+ "id": self.id,
62
+ "type": self.type.value,
63
+ "severity": self.severity.value,
64
+ "file_path": self.file_path,
65
+ "line_start": self.line_start,
66
+ "line_end": self.line_end,
67
+ "description": self.description,
68
+ "suggestion": self.suggestion,
69
+ "effort_minutes": self.effort_minutes,
70
+ }
71
+
72
+
73
+ @dataclass
74
+ class FileMetrics:
75
+ """Métricas de un archivo."""
76
+ file_path: str
77
+ lines_of_code: int
78
+ cyclomatic_complexity: float
79
+ cognitive_complexity: float
80
+ maintainability_index: float # 0-100, higher is better
81
+ function_count: int
82
+ class_count: int
83
+ comment_ratio: float
84
+ duplication_ratio: float
85
+ issues: List[DebtIssue] = field(default_factory=list)
86
+
87
+ def to_dict(self) -> Dict:
88
+ return {
89
+ "file_path": self.file_path,
90
+ "lines_of_code": self.lines_of_code,
91
+ "cyclomatic_complexity": round(self.cyclomatic_complexity, 2),
92
+ "cognitive_complexity": round(self.cognitive_complexity, 2),
93
+ "maintainability_index": round(self.maintainability_index, 2),
94
+ "function_count": self.function_count,
95
+ "class_count": self.class_count,
96
+ "comment_ratio": round(self.comment_ratio, 2),
97
+ "duplication_ratio": round(self.duplication_ratio, 2),
98
+ "issue_count": len(self.issues),
99
+ }
100
+
101
+
102
+ @dataclass
103
+ class DebtReport:
104
+ """Reporte completo de deuda técnica."""
105
+ project_root: str
106
+ files_analyzed: int
107
+ total_issues: int
108
+ total_effort_hours: float
109
+ maintainability_score: float # 0-100, higher is better
110
+ metrics_by_file: Dict[str, FileMetrics] = field(default_factory=dict)
111
+ issues: List[DebtIssue] = field(default_factory=list)
112
+
113
+ def get_summary(self) -> Dict:
114
+ """Obtiene resumen ejecutivo."""
115
+ by_severity = defaultdict(int)
116
+ by_type = defaultdict(int)
117
+
118
+ for issue in self.issues:
119
+ by_severity[issue.severity.name] += 1
120
+ by_type[issue.type.value] += 1
121
+
122
+ return {
123
+ "files_analyzed": self.files_analyzed,
124
+ "total_issues": self.total_issues,
125
+ "total_effort_hours": round(self.total_effort_hours, 1),
126
+ "maintainability_score": round(self.maintainability_score, 1),
127
+ "by_severity": dict(by_severity),
128
+ "by_type": dict(by_type),
129
+ }
130
+
131
+
132
+ class DebtAnalyzer:
133
+ """
134
+ Analiza deuda técnica en un proyecto.
135
+
136
+ Métricas:
137
+ - Complejidad ciclomática
138
+ - Líneas por función
139
+ - Profundidad de anidación
140
+ - Duplicación de código
141
+ - Imports no usados
142
+ """
143
+
144
+ # Umbrales configurables
145
+ MAX_FUNCTION_LINES = 50
146
+ MAX_FUNCTION_PARAMS = 5
147
+ MAX_NESTING_DEPTH = 4
148
+ MAX_CLASS_METHODS = 15
149
+ MAX_CYCLOMATIC_COMPLEXITY = 10
150
+ MIN_MAINTAINABILITY_INDEX = 20
151
+
152
+ def __init__(self, project_root: Path):
153
+ self.project_root = project_root
154
+ self._issue_counter = 0
155
+
156
+ def analyze_project(
157
+ self,
158
+ include_patterns: List[str] = None,
159
+ exclude_patterns: List[str] = None
160
+ ) -> DebtReport:
161
+ """
162
+ Analiza todo el proyecto.
163
+
164
+ Args:
165
+ include_patterns: Patrones de archivos a incluir
166
+ exclude_patterns: Patrones a excluir
167
+
168
+ Returns:
169
+ DebtReport con el análisis completo
170
+ """
171
+ include_patterns = include_patterns or ["*.py", "*.js", "*.ts"]
172
+ exclude_patterns = exclude_patterns or [
173
+ "node_modules", "__pycache__", ".git", "venv", "env",
174
+ "dist", "build", "test", "tests"
175
+ ]
176
+
177
+ report = DebtReport(
178
+ project_root=str(self.project_root),
179
+ files_analyzed=0,
180
+ total_issues=0,
181
+ total_effort_hours=0,
182
+ maintainability_score=100,
183
+ )
184
+
185
+ # Recolectar archivos
186
+ files = self._collect_files(include_patterns, exclude_patterns)
187
+
188
+ # Analizar cada archivo
189
+ for file_path in files:
190
+ metrics = self.analyze_file(file_path)
191
+ report.metrics_by_file[str(file_path)] = metrics
192
+ report.issues.extend(metrics.issues)
193
+ report.files_analyzed += 1
194
+
195
+ # Calcular totales
196
+ report.total_issues = len(report.issues)
197
+ report.total_effort_hours = sum(i.effort_minutes for i in report.issues) / 60
198
+
199
+ # Calcular score de mantenibilidad promedio
200
+ if report.metrics_by_file:
201
+ report.maintainability_score = sum(
202
+ m.maintainability_index for m in report.metrics_by_file.values()
203
+ ) / len(report.metrics_by_file)
204
+
205
+ return report
206
+
207
+ def analyze_file(self, file_path: Path) -> FileMetrics:
208
+ """
209
+ Analiza un archivo específico.
210
+
211
+ Args:
212
+ file_path: Ruta del archivo
213
+
214
+ Returns:
215
+ FileMetrics con el análisis
216
+ """
217
+ ext = file_path.suffix.lower()
218
+
219
+ try:
220
+ content = file_path.read_text(encoding="utf-8", errors="replace")
221
+ except Exception:
222
+ return FileMetrics(
223
+ file_path=str(file_path),
224
+ lines_of_code=0,
225
+ cyclomatic_complexity=0,
226
+ cognitive_complexity=0,
227
+ maintainability_index=0,
228
+ function_count=0,
229
+ class_count=0,
230
+ comment_ratio=0,
231
+ duplication_ratio=0,
232
+ )
233
+
234
+ lines = content.split('\n')
235
+ loc = len([l for l in lines if l.strip() and not l.strip().startswith('#')])
236
+
237
+ if ext == '.py':
238
+ return self._analyze_python(file_path, content, lines, loc)
239
+ elif ext in ('.js', '.ts', '.jsx', '.tsx'):
240
+ return self._analyze_javascript(file_path, content, lines, loc)
241
+
242
+ return FileMetrics(
243
+ file_path=str(file_path),
244
+ lines_of_code=loc,
245
+ cyclomatic_complexity=0,
246
+ cognitive_complexity=0,
247
+ maintainability_index=50,
248
+ function_count=0,
249
+ class_count=0,
250
+ comment_ratio=0,
251
+ duplication_ratio=0,
252
+ )
253
+
254
+ # ══════════════════════════════════════════════════════════════════════════
255
+ # ANÁLISIS PYTHON
256
+ # ══════════════════════════════════════════════════════════════════════════
257
+
258
+ def _analyze_python(
259
+ self,
260
+ file_path: Path,
261
+ content: str,
262
+ lines: List[str],
263
+ loc: int
264
+ ) -> FileMetrics:
265
+ """Analiza código Python."""
266
+ issues = []
267
+ function_count = 0
268
+ class_count = 0
269
+ total_complexity = 0
270
+ total_cognitive = 0
271
+
272
+ try:
273
+ tree = ast.parse(content)
274
+ except SyntaxError:
275
+ return FileMetrics(
276
+ file_path=str(file_path),
277
+ lines_of_code=loc,
278
+ cyclomatic_complexity=0,
279
+ cognitive_complexity=0,
280
+ maintainability_index=0,
281
+ function_count=0,
282
+ class_count=0,
283
+ comment_ratio=0,
284
+ duplication_ratio=0,
285
+ )
286
+
287
+ # Analizar imports no usados
288
+ imports = set()
289
+ for node in ast.walk(tree):
290
+ if isinstance(node, ast.Import):
291
+ for alias in node.names:
292
+ imports.add(alias.asname or alias.name.split('.')[0])
293
+ elif isinstance(node, ast.ImportFrom):
294
+ for alias in node.names:
295
+ imports.add(alias.asname or alias.name)
296
+
297
+ # Encontrar nombres usados
298
+ used_names = set()
299
+ for node in ast.walk(tree):
300
+ if isinstance(node, ast.Name):
301
+ used_names.add(node.id)
302
+
303
+ unused_imports = imports - used_names
304
+ for imp in unused_imports:
305
+ issues.append(self._create_issue(
306
+ DebtType.UNUSED_IMPORTS,
307
+ DebtSeverity.LOW,
308
+ str(file_path),
309
+ 1, 1,
310
+ f"Unused import: {imp}",
311
+ f"Remove import: {imp}",
312
+ 2
313
+ ))
314
+
315
+ # Analizar clases y funciones
316
+ for node in ast.iter_child_nodes(tree):
317
+ if isinstance(node, ast.ClassDef):
318
+ class_count += 1
319
+ issues.extend(self._analyze_class(node, str(file_path)))
320
+
321
+ elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
322
+ function_count += 1
323
+ complexity = self._calculate_complexity(node)
324
+ cognitive = self._calculate_cognitive_complexity(node)
325
+ total_complexity += complexity
326
+ total_cognitive += cognitive
327
+
328
+ issues.extend(self._analyze_function(node, str(file_path), complexity))
329
+
330
+ # Detectar código duplicado (simplificado)
331
+ duplication = self._detect_duplication(lines)
332
+ if duplication > 0.1:
333
+ issues.append(self._create_issue(
334
+ DebtType.CODE_DUPLICATION,
335
+ DebtSeverity.MEDIUM,
336
+ str(file_path),
337
+ 1, loc,
338
+ f"Code duplication detected ({duplication:.0%})",
339
+ "Refactor duplicated code into reusable functions",
340
+ int(duplication * loc * 2)
341
+ ))
342
+
343
+ # Detectar TODOs
344
+ for i, line in enumerate(lines, 1):
345
+ if 'TODO' in line or 'FIXME' in line:
346
+ issues.append(self._create_issue(
347
+ DebtType.TODO_COMMENTS,
348
+ DebtSeverity.LOW,
349
+ str(file_path),
350
+ i, i,
351
+ "TODO/FIXME comment found",
352
+ "Implement or remove TODO",
353
+ 15
354
+ ))
355
+
356
+ # Calcular métricas
357
+ avg_complexity = total_complexity / function_count if function_count > 0 else 0
358
+ avg_cognitive = total_cognitive / function_count if function_count > 0 else 0
359
+
360
+ # Maintainability Index (simplificado)
361
+ # MI = 171 - 5.2 * ln(V) - 0.23 * G - 16.2 * ln(LOC)
362
+ # Simplificado para este análisis
363
+ mi = max(0, min(100, 100 - avg_complexity * 5 - (loc / 100) * 5))
364
+
365
+ # Comment ratio
366
+ comment_lines = len([l for l in lines if l.strip().startswith('#')])
367
+ comment_ratio = comment_lines / loc if loc > 0 else 0
368
+
369
+ return FileMetrics(
370
+ file_path=str(file_path),
371
+ lines_of_code=loc,
372
+ cyclomatic_complexity=avg_complexity,
373
+ cognitive_complexity=avg_cognitive,
374
+ maintainability_index=mi,
375
+ function_count=function_count,
376
+ class_count=class_count,
377
+ comment_ratio=comment_ratio,
378
+ duplication_ratio=duplication,
379
+ issues=issues,
380
+ )
381
+
382
+ def _analyze_class(self, node: ast.ClassDef, file_path: str) -> List[DebtIssue]:
383
+ """Analiza una clase."""
384
+ issues = []
385
+
386
+ # God Class: demasiados métodos
387
+ methods = [n for n in node.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
388
+ if len(methods) > self.MAX_CLASS_METHODS:
389
+ issues.append(self._create_issue(
390
+ DebtType.GOD_CLASS,
391
+ DebtSeverity.HIGH,
392
+ file_path,
393
+ node.lineno, node.end_lineno or node.lineno,
394
+ f"Class '{node.name}' has {len(methods)} methods (max {self.MAX_CLASS_METHODS})",
395
+ "Split class into smaller, focused classes",
396
+ len(methods) * 5
397
+ ))
398
+
399
+ # Verificar métodos individuales
400
+ for method in methods:
401
+ issues.extend(self._analyze_function(method, file_path, 0, parent_class=node.name))
402
+
403
+ return issues
404
+
405
+ def _analyze_function(
406
+ self,
407
+ node: ast.FunctionDef,
408
+ file_path: str,
409
+ complexity: int,
410
+ parent_class: str = None
411
+ ) -> List[DebtIssue]:
412
+ """Analiza una función/método."""
413
+ issues = []
414
+ func_name = f"{parent_class}.{node.name}" if parent_class else node.name
415
+
416
+ # Líneas de código
417
+ func_lines = (node.end_lineno or node.lineno) - node.lineno + 1
418
+
419
+ # Función larga
420
+ if func_lines > self.MAX_FUNCTION_LINES:
421
+ issues.append(self._create_issue(
422
+ DebtType.LONG_FUNCTION,
423
+ DebtSeverity.MEDIUM,
424
+ file_path,
425
+ node.lineno, node.end_lineno or node.lineno,
426
+ f"Function '{func_name}' has {func_lines} lines (max {self.MAX_FUNCTION_LINES})",
427
+ "Break into smaller functions",
428
+ func_lines // 10
429
+ ))
430
+
431
+ # Muchos parámetros
432
+ param_count = len(node.args.args) + len(node.args.kwonlyargs)
433
+ if node.args.vararg:
434
+ param_count += 1
435
+ if node.args.kwarg:
436
+ param_count += 1
437
+
438
+ if param_count > self.MAX_FUNCTION_PARAMS:
439
+ issues.append(self._create_issue(
440
+ DebtType.TOO_MANY_PARAMETERS,
441
+ DebtSeverity.MEDIUM,
442
+ file_path,
443
+ node.lineno, node.lineno,
444
+ f"Function '{func_name}' has {param_count} parameters (max {self.MAX_FUNCTION_PARAMS})",
445
+ "Use parameter object or refactor",
446
+ param_count * 3
447
+ ))
448
+
449
+ # Alta complejidad ciclomática
450
+ if complexity > self.MAX_CYCLOMATIC_COMPLEXITY:
451
+ issues.append(self._create_issue(
452
+ DebtType.HIGH_COMPLEXITY,
453
+ DebtSeverity.HIGH,
454
+ file_path,
455
+ node.lineno, node.end_lineno or node.lineno,
456
+ f"Function '{func_name}' has complexity {complexity} (max {self.MAX_CYCLOMATIC_COMPLEXITY})",
457
+ "Simplify control flow, extract methods",
458
+ complexity * 5
459
+ ))
460
+
461
+ # Profundidad de anidación
462
+ max_depth = self._calculate_nesting_depth(node)
463
+ if max_depth > self.MAX_NESTING_DEPTH:
464
+ issues.append(self._create_issue(
465
+ DebtType.DEEP_NESTING,
466
+ DebtSeverity.MEDIUM,
467
+ file_path,
468
+ node.lineno, node.end_lineno or node.lineno,
469
+ f"Function '{func_name}' has nesting depth {max_depth} (max {self.MAX_NESTING_DEPTH})",
470
+ "Extract nested logic into separate functions",
471
+ max_depth * 10
472
+ ))
473
+
474
+ # Números mágicos
475
+ for child in ast.walk(node):
476
+ if isinstance(child, ast.Constant) and isinstance(child.value, (int, float)):
477
+ # Ignorar 0, 1, -1 y otros comunes
478
+ if child.value not in (0, 1, -1, 2, 10, 100, 1000, True, False):
479
+ issues.append(self._create_issue(
480
+ DebtType.MAGIC_NUMBERS,
481
+ DebtSeverity.LOW,
482
+ file_path,
483
+ child.lineno, child.lineno,
484
+ f"Magic number {child.value} in function '{func_name}'",
485
+ "Define as named constant",
486
+ 2
487
+ ))
488
+
489
+ return issues
490
+
491
+ # ══════════════════════════════════════════════════════════════════════════
492
+ # ANÁLISIS JAVASCRIPT
493
+ # ══════════════════════════════════════════════════════════════════════════
494
+
495
+ def _analyze_javascript(
496
+ self,
497
+ file_path: Path,
498
+ content: str,
499
+ lines: List[str],
500
+ loc: int
501
+ ) -> FileMetrics:
502
+ """Analiza código JavaScript."""
503
+ issues = []
504
+ function_count = 0
505
+ class_count = 0
506
+
507
+ # Contar funciones
508
+ func_pattern = r'(?:export\s+)?(?:async\s+)?function\s+(\w+)'
509
+ function_count = len(re.findall(func_pattern, content))
510
+
511
+ # Contar clases
512
+ class_pattern = r'(?:export\s+)?class\s+(\w+)'
513
+ class_count = len(re.findall(class_pattern, content))
514
+
515
+ # Detectar var (debería usar let/const)
516
+ var_pattern = r'\bvar\s+\w+'
517
+ for i, line in enumerate(lines, 1):
518
+ if re.search(var_pattern, line):
519
+ issues.append(self._create_issue(
520
+ DebtType.DEPRECATED_USAGE,
521
+ DebtSeverity.LOW,
522
+ str(file_path),
523
+ i, i,
524
+ "Use of 'var' - prefer 'let' or 'const'",
525
+ "Replace 'var' with 'let' or 'const'",
526
+ 2
527
+ ))
528
+
529
+ # Detectar console.log
530
+ for i, line in enumerate(lines, 1):
531
+ if 'console.log' in line:
532
+ issues.append(self._create_issue(
533
+ DebtType.DEAD_CODE,
534
+ DebtSeverity.LOW,
535
+ str(file_path),
536
+ i, i,
537
+ "console.log statement found",
538
+ "Remove or replace with proper logging",
539
+ 1
540
+ ))
541
+
542
+ # TODOs
543
+ for i, line in enumerate(lines, 1):
544
+ if 'TODO' in line or 'FIXME' in line:
545
+ issues.append(self._create_issue(
546
+ DebtType.TODO_COMMENTS,
547
+ DebtSeverity.LOW,
548
+ str(file_path),
549
+ i, i,
550
+ "TODO/FIXME comment found",
551
+ "Implement or remove TODO",
552
+ 15
553
+ ))
554
+
555
+ # Estimar complejidad (simplificado)
556
+ complexity = len(re.findall(r'\b(if|for|while|switch|catch|&&|\|\|)\b', content))
557
+
558
+ return FileMetrics(
559
+ file_path=str(file_path),
560
+ lines_of_code=loc,
561
+ cyclomatic_complexity=complexity / function_count if function_count > 0 else 0,
562
+ cognitive_complexity=0,
563
+ maintainability_index=max(0, min(100, 100 - complexity)),
564
+ function_count=function_count,
565
+ class_count=class_count,
566
+ comment_ratio=0,
567
+ duplication_ratio=0,
568
+ issues=issues,
569
+ )
570
+
571
+ # ══════════════════════════════════════════════════════════════════════════
572
+ # MÉTODOS PRIVADOS
573
+ # ══════════════════════════════════════════════════════════════════════════
574
+
575
+ def _calculate_complexity(self, node: ast.FunctionDef) -> int:
576
+ """Calcula complejidad ciclomática."""
577
+ complexity = 1
578
+
579
+ for child in ast.walk(node):
580
+ if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler)):
581
+ complexity += 1
582
+ elif isinstance(child, ast.BoolOp):
583
+ complexity += len(child.values) - 1
584
+ elif isinstance(child, ast.comprehension):
585
+ complexity += 1
586
+ complexity += len(child.ifs)
587
+
588
+ return complexity
589
+
590
+ def _calculate_cognitive_complexity(self, node: ast.FunctionDef) -> int:
591
+ """Calcula complejidad cognitiva."""
592
+ complexity = 0
593
+ nesting = 0
594
+
595
+ def visit(n, depth=0):
596
+ nonlocal complexity, nesting
597
+
598
+ if isinstance(n, (ast.If, ast.While, ast.For)):
599
+ complexity += depth + 1
600
+ for child in ast.iter_child_nodes(n):
601
+ visit(child, depth + 1)
602
+ elif isinstance(n, ast.ExceptHandler):
603
+ complexity += depth + 1
604
+ for child in ast.iter_child_nodes(n):
605
+ visit(child, depth + 1)
606
+ else:
607
+ for child in ast.iter_child_nodes(n):
608
+ visit(child, depth)
609
+
610
+ visit(node)
611
+ return complexity
612
+
613
+ def _calculate_nesting_depth(self, node: ast.FunctionDef) -> int:
614
+ """Calcula profundidad máxima de anidación."""
615
+ max_depth = 0
616
+
617
+ def visit(n, depth=0):
618
+ nonlocal max_depth
619
+
620
+ if isinstance(n, (ast.If, ast.While, ast.For, ast.With, ast.Try)):
621
+ max_depth = max(max_depth, depth + 1)
622
+ for child in ast.iter_child_nodes(n):
623
+ visit(child, depth + 1)
624
+ else:
625
+ for child in ast.iter_child_nodes(n):
626
+ visit(child, depth)
627
+
628
+ visit(node)
629
+ return max_depth
630
+
631
+ def _detect_duplication(self, lines: List[str]) -> float:
632
+ """Detecta duplicación de código (simplificado)."""
633
+ if len(lines) < 10:
634
+ return 0
635
+
636
+ # Buscar líneas idénticas
637
+ line_counts = defaultdict(int)
638
+ for line in lines:
639
+ stripped = line.strip()
640
+ if stripped and not stripped.startswith('#'):
641
+ line_counts[stripped] += 1
642
+
643
+ duplicated = sum(count - 1 for count in line_counts.values() if count > 1)
644
+ return duplicated / len(lines) if lines else 0
645
+
646
+ def _collect_files(
647
+ self,
648
+ include_patterns: List[str],
649
+ exclude_patterns: List[str]
650
+ ) -> List[Path]:
651
+ """Recolecta archivos del proyecto."""
652
+ files = []
653
+
654
+ for pattern in include_patterns:
655
+ for file_path in self.project_root.rglob(pattern):
656
+ # Verificar exclusiones
657
+ rel_path = str(file_path.relative_to(self.project_root))
658
+ if any(exc in rel_path for exc in exclude_patterns):
659
+ continue
660
+ files.append(file_path)
661
+
662
+ return files
663
+
664
+ def _create_issue(
665
+ self,
666
+ type: DebtType,
667
+ severity: DebtSeverity,
668
+ file_path: str,
669
+ line_start: int,
670
+ line_end: int,
671
+ description: str,
672
+ suggestion: str,
673
+ effort_minutes: int
674
+ ) -> DebtIssue:
675
+ """Crea un issue de deuda técnica."""
676
+ self._issue_counter += 1
677
+ return DebtIssue(
678
+ id=f"debt_{self._issue_counter}",
679
+ type=type,
680
+ severity=severity,
681
+ file_path=file_path,
682
+ line_start=line_start,
683
+ line_end=line_end,
684
+ description=description,
685
+ suggestion=suggestion,
686
+ effort_minutes=effort_minutes,
687
+ )
688
+
689
+
690
+ # ══════════════════════════════════════════════════════════════════════════════
691
+ # INSTANCIA GLOBAL
692
+ # ══════════════════════════════════════════════════════════════════════════════
693
+
694
+ _analyzer_instance: Optional[DebtAnalyzer] = None
695
+
696
+
697
+ def get_debt_analyzer(project_root: Path = None) -> DebtAnalyzer:
698
+ """Obtiene la instancia global del analizador de deuda."""
699
+ global _analyzer_instance
700
+ if _analyzer_instance is None or (project_root and _analyzer_instance.project_root != project_root):
701
+ _analyzer_instance = DebtAnalyzer(project_root or Path.cwd())
702
+ return _analyzer_instance