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,626 @@
1
+ # hanus/context/selective.py
2
+ """
3
+ Cargador selectivo de contexto por archivo.
4
+
5
+ Analiza imports/dependencias y carga solo las partes relevantes
6
+ de archivos grandes, ahorrando tokens y mejorando precisión.
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, Set, Any, Tuple
14
+ from enum import Enum
15
+ from collections import defaultdict
16
+ import time
17
+
18
+
19
+ class SymbolType(Enum):
20
+ """Tipo de símbolo en código."""
21
+ CLASS = "class"
22
+ FUNCTION = "function"
23
+ METHOD = "method"
24
+ VARIABLE = "variable"
25
+ CONSTANT = "constant"
26
+ IMPORT = "import"
27
+ MODULE = "module"
28
+
29
+
30
+ @dataclass
31
+ class Symbol:
32
+ """Un símbolo de código (clase, función, etc.)."""
33
+ name: str
34
+ type: SymbolType
35
+ file_path: str
36
+ line_start: int
37
+ line_end: int
38
+ docstring: Optional[str] = None
39
+ signature: Optional[str] = None # Para funciones/métodos
40
+ parent: Optional[str] = None # Para métodos (clase padre)
41
+ dependencies: List[str] = field(default_factory=list) # Símbolos que usa
42
+ exported: bool = True # Si es parte de la API pública
43
+
44
+ def to_dict(self) -> Dict:
45
+ return {
46
+ "name": self.name,
47
+ "type": self.type.value,
48
+ "file_path": self.file_path,
49
+ "line_start": self.line_start,
50
+ "line_end": self.line_end,
51
+ "docstring": self.docstring,
52
+ "signature": self.signature,
53
+ "parent": self.parent,
54
+ "dependencies": self.dependencies,
55
+ "exported": self.exported,
56
+ }
57
+
58
+
59
+ @dataclass
60
+ class FileIndex:
61
+ """Índice de un archivo analizado."""
62
+ path: str
63
+ language: str
64
+ symbols: List[Symbol]
65
+ imports: List[str]
66
+ exports: List[str]
67
+ last_modified: float
68
+ content_hash: str
69
+
70
+ def get_symbol(self, name: str) -> Optional[Symbol]:
71
+ """Obtiene un símbolo por nombre."""
72
+ for sym in self.symbols:
73
+ if sym.name == name:
74
+ return sym
75
+ return None
76
+
77
+ def get_symbols_by_type(self, symbol_type: SymbolType) -> List[Symbol]:
78
+ """Obtiene símbolos por tipo."""
79
+ return [s for s in self.symbols if s.type == symbol_type]
80
+
81
+
82
+ class SelectiveLoader:
83
+ """
84
+ Carga contexto de forma selectiva, extrayendo solo lo relevante.
85
+
86
+ Features:
87
+ - Análisis de imports/dependencias
88
+ - Extracción de símbolos específicos
89
+ - Lazy loading de dependencias
90
+ - Cache de índices por archivo
91
+ """
92
+
93
+ # Extensiones soportadas por lenguaje
94
+ LANGUAGE_EXTENSIONS = {
95
+ '.py': 'python',
96
+ '.js': 'javascript',
97
+ '.ts': 'typescript',
98
+ '.jsx': 'javascript',
99
+ '.tsx': 'typescript',
100
+ '.java': 'java',
101
+ '.go': 'go',
102
+ '.rs': 'rust',
103
+ '.c': 'c',
104
+ '.cpp': 'cpp',
105
+ '.h': 'c',
106
+ '.hpp': 'cpp',
107
+ }
108
+
109
+ def __init__(self, project_root: Path, cache_dir: Optional[Path] = None):
110
+ self.project_root = project_root
111
+ self.cache_dir = cache_dir or (Path.home() / ".hanus" / "context_cache")
112
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
113
+
114
+ # Cache de índices
115
+ self._index_cache: Dict[str, FileIndex] = {}
116
+ self._symbol_index: Dict[str, List[Symbol]] = defaultdict(list) # name -> [symbols]
117
+
118
+ # Cache de contenido parcial
119
+ self._content_cache: Dict[str, str] = {}
120
+
121
+ def load_relevant_context(
122
+ self,
123
+ file_path: str,
124
+ query: str,
125
+ max_tokens: int = 4000
126
+ ) -> str:
127
+ """
128
+ Carga contexto relevante para una consulta sobre un archivo.
129
+
130
+ Args:
131
+ file_path: Archivo objetivo
132
+ query: Consulta del usuario
133
+ max_tokens: Límite de tokens
134
+
135
+ Returns:
136
+ Contexto relevante como string
137
+ """
138
+ path = Path(file_path)
139
+ if not path.exists():
140
+ return f"File not found: {file_path}"
141
+
142
+ # Obtener índice del archivo
143
+ index = self._get_or_create_index(path)
144
+
145
+ # Identificar símbolos relevantes basados en la query
146
+ relevant_symbols = self._find_relevant_symbols(index, query)
147
+
148
+ # Construir contexto
149
+ context_parts = []
150
+
151
+ # Añadir imports primero
152
+ if index.imports:
153
+ imports_text = "## Imports\n```python\n" + "\n".join(index.imports[:20]) + "\n```\n"
154
+ context_parts.append(imports_text)
155
+
156
+ # Añadir símbolos relevantes
157
+ content = self._read_file(path)
158
+ lines = content.split('\n')
159
+
160
+ for sym in relevant_symbols:
161
+ # Extraer código del símbolo
162
+ symbol_code = "\n".join(lines[sym.line_start - 1:sym.line_end])
163
+
164
+ header = f"## {sym.type.value}: {sym.name}"
165
+ if sym.docstring:
166
+ header += f"\n{sym.docstring[:200]}"
167
+
168
+ context_parts.append(f"{header}\n```python\n{symbol_code}\n```\n")
169
+
170
+ # Verificar límite de tokens
171
+ current_length = sum(len(p) for p in context_parts)
172
+ if current_length > max_tokens * 4: # ~4 chars per token
173
+ break
174
+
175
+ return "\n".join(context_parts)
176
+
177
+ def get_file_symbols(self, file_path: str) -> List[Symbol]:
178
+ """
179
+ Obtiene todos los símbolos de un archivo.
180
+
181
+ Args:
182
+ file_path: Ruta del archivo
183
+
184
+ Returns:
185
+ Lista de símbolos
186
+ """
187
+ path = Path(file_path)
188
+ if not path.exists():
189
+ return []
190
+
191
+ index = self._get_or_create_index(path)
192
+ return index.symbols
193
+
194
+ def build_dependency_graph(self, file_path: str) -> Dict[str, List[str]]:
195
+ """
196
+ Construye el grafo de dependencias de un archivo.
197
+
198
+ Args:
199
+ file_path: Archivo objetivo
200
+
201
+ Returns:
202
+ Dict con dependencias {symbol: [dependencies]}
203
+ """
204
+ path = Path(file_path)
205
+ if not path.exists():
206
+ return {}
207
+
208
+ index = self._get_or_create_index(path)
209
+
210
+ graph = {}
211
+ for sym in index.symbols:
212
+ graph[sym.name] = sym.dependencies
213
+
214
+ return graph
215
+
216
+ def find_symbol_definition(
217
+ self,
218
+ symbol_name: str,
219
+ from_file: Optional[str] = None
220
+ ) -> Optional[Tuple[str, Symbol]]:
221
+ """
222
+ Encuentra la definición de un símbolo.
223
+
224
+ Args:
225
+ symbol_name: Nombre del símbolo
226
+ from_file: Archivo desde donde se busca (para resolver imports)
227
+
228
+ Returns:
229
+ (file_path, Symbol) si se encuentra
230
+ """
231
+ # Buscar en índice global
232
+ if symbol_name in self._symbol_index:
233
+ symbols = self._symbol_index[symbol_name]
234
+ if symbols:
235
+ sym = symbols[0]
236
+ return (sym.file_path, sym)
237
+
238
+ # Si hay archivo origen, buscar en sus imports
239
+ if from_file:
240
+ from_path = Path(from_file)
241
+ if from_path.exists():
242
+ index = self._get_or_create_index(from_path)
243
+ for imp in index.imports:
244
+ # Intentar resolver import
245
+ if symbol_name in imp:
246
+ resolved = self._resolve_import(imp, from_path)
247
+ if resolved:
248
+ resolved_index = self._get_or_create_index(resolved)
249
+ sym = resolved_index.get_symbol(symbol_name)
250
+ if sym:
251
+ return (str(resolved), sym)
252
+
253
+ return None
254
+
255
+ def extract_symbol_content(
256
+ self,
257
+ file_path: str,
258
+ symbol_name: str
259
+ ) -> Optional[str]:
260
+ """
261
+ Extrae el contenido de un símbolo específico.
262
+
263
+ Args:
264
+ file_path: Ruta del archivo
265
+ symbol_name: Nombre del símbolo
266
+
267
+ Returns:
268
+ Código del símbolo o None
269
+ """
270
+ path = Path(file_path)
271
+ if not path.exists():
272
+ return None
273
+
274
+ index = self._get_or_create_index(path)
275
+ sym = index.get_symbol(symbol_name)
276
+
277
+ if not sym:
278
+ return None
279
+
280
+ content = self._read_file(path)
281
+ lines = content.split('\n')
282
+
283
+ return "\n".join(lines[sym.line_start - 1:sym.line_end])
284
+
285
+ def get_index_stats(self) -> Dict[str, Any]:
286
+ """Obtiene estadísticas del índice."""
287
+ return {
288
+ "files_indexed": len(self._index_cache),
289
+ "total_symbols": sum(len(idx.symbols) for idx in self._index_cache.values()),
290
+ "symbols_by_type": {
291
+ t.value: len([s for s in self._symbol_index.values()
292
+ for s in s if s.type == t])
293
+ for t in SymbolType
294
+ },
295
+ }
296
+
297
+ # ══════════════════════════════════════════════════════════════════════════
298
+ # MÉTODOS PRIVADOS
299
+ # ══════════════════════════════════════════════════════════════════════════
300
+
301
+ def _get_or_create_index(self, file_path: Path) -> FileIndex:
302
+ """Obtiene o crea el índice de un archivo."""
303
+ path_str = str(file_path.resolve())
304
+
305
+ # Verificar cache
306
+ if path_str in self._index_cache:
307
+ cached = self._index_cache[path_str]
308
+ # Verificar si el archivo cambió
309
+ if file_path.stat().st_mtime <= cached.last_modified:
310
+ return cached
311
+
312
+ # Crear nuevo índice
313
+ index = self._create_index(file_path)
314
+ self._index_cache[path_str] = index
315
+
316
+ # Actualizar índice global de símbolos
317
+ for sym in index.symbols:
318
+ self._symbol_index[sym.name].append(sym)
319
+
320
+ return index
321
+
322
+ def _create_index(self, file_path: Path) -> FileIndex:
323
+ """Crea el índice de un archivo."""
324
+ content = self._read_file(file_path)
325
+ ext = file_path.suffix.lower()
326
+ language = self.LANGUAGE_EXTENSIONS.get(ext, 'unknown')
327
+
328
+ symbols = []
329
+ imports = []
330
+ exports = []
331
+
332
+ if language == 'python':
333
+ symbols, imports, exports = self._analyze_python(file_path, content)
334
+ elif language in ('javascript', 'typescript'):
335
+ symbols, imports, exports = self._analyze_javascript(file_path, content)
336
+
337
+ content_hash = str(hash(content))
338
+
339
+ return FileIndex(
340
+ path=str(file_path),
341
+ language=language,
342
+ symbols=symbols,
343
+ imports=imports,
344
+ exports=exports,
345
+ last_modified=time.time(),
346
+ content_hash=content_hash,
347
+ )
348
+
349
+ def _analyze_python(
350
+ self,
351
+ file_path: Path,
352
+ content: str
353
+ ) -> Tuple[List[Symbol], List[str], List[str]]:
354
+ """Analiza código Python."""
355
+ symbols = []
356
+ imports = []
357
+ exports = []
358
+
359
+ try:
360
+ tree = ast.parse(content)
361
+ lines = content.split('\n')
362
+ except SyntaxError:
363
+ return symbols, imports, exports
364
+
365
+ # Extraer imports
366
+ for node in ast.walk(tree):
367
+ if isinstance(node, ast.Import):
368
+ for alias in node.names:
369
+ name = alias.asname if alias.asname else alias.name
370
+ imports.append(f"import {alias.name}" + (f" as {alias.asname}" if alias.asname else ""))
371
+ elif isinstance(node, ast.ImportFrom):
372
+ module = node.module or ""
373
+ names = ", ".join(alias.name for alias in node.names)
374
+ imports.append(f"from {module} import {names}")
375
+
376
+ # Extraer clases y funciones
377
+ for node in ast.iter_child_nodes(tree):
378
+ if isinstance(node, ast.ClassDef):
379
+ # Docstring
380
+ docstring = ast.get_docstring(node)
381
+
382
+ # Métodos
383
+ methods = []
384
+ for item in node.body:
385
+ if isinstance(item, ast.FunctionDef):
386
+ method_sym = self._create_function_symbol(
387
+ item, str(file_path), parent=node.name
388
+ )
389
+ symbols.append(method_sym)
390
+ methods.append(item.name)
391
+
392
+ # Símbolo de clase
393
+ class_sym = Symbol(
394
+ name=node.name,
395
+ type=SymbolType.CLASS,
396
+ file_path=str(file_path),
397
+ line_start=node.lineno,
398
+ line_end=node.end_lineno or node.lineno,
399
+ docstring=docstring,
400
+ dependencies=self._extract_class_dependencies(node),
401
+ )
402
+ symbols.append(class_sym)
403
+ exports.append(node.name)
404
+
405
+ elif isinstance(node, ast.FunctionDef):
406
+ func_sym = self._create_function_symbol(node, str(file_path))
407
+ symbols.append(func_sym)
408
+ exports.append(node.name)
409
+
410
+ return symbols, imports, exports
411
+
412
+ def _create_function_symbol(
413
+ self,
414
+ node: ast.FunctionDef,
415
+ file_path: str,
416
+ parent: Optional[str] = None
417
+ ) -> Symbol:
418
+ """Crea un símbolo de función/método."""
419
+ # Construir firma
420
+ args = []
421
+ for arg in node.args.args:
422
+ arg_str = arg.arg
423
+ if arg.annotation:
424
+ arg_str += f": {ast.unparse(arg.annotation)}"
425
+ args.append(arg_str)
426
+
427
+ defaults = node.args.defaults
428
+ if defaults:
429
+ for i, default in enumerate(defaults):
430
+ idx = len(args) - len(defaults) + i
431
+ if idx < len(args):
432
+ args[idx] += f" = {ast.unparse(default)}"
433
+
434
+ signature = f"{node.name}({', '.join(args)})"
435
+ if node.returns:
436
+ signature += f" -> {ast.unparse(node.returns)}"
437
+
438
+ sym_type = SymbolType.METHOD if parent else SymbolType.FUNCTION
439
+
440
+ return Symbol(
441
+ name=node.name,
442
+ type=sym_type,
443
+ file_path=file_path,
444
+ line_start=node.lineno,
445
+ line_end=node.end_lineno or node.lineno,
446
+ docstring=ast.get_docstring(node),
447
+ signature=signature,
448
+ parent=parent,
449
+ dependencies=self._extract_function_dependencies(node),
450
+ )
451
+
452
+ def _extract_function_dependencies(self, node: ast.FunctionDef) -> List[str]:
453
+ """Extrae dependencias de una función."""
454
+ deps = set()
455
+
456
+ for child in ast.walk(node):
457
+ if isinstance(child, ast.Name):
458
+ deps.add(child.id)
459
+ elif isinstance(child, ast.Attribute):
460
+ if isinstance(child.value, ast.Name):
461
+ deps.add(child.value.id)
462
+
463
+ # Filtrar builtins y parámetros
464
+ builtins = {'True', 'False', 'None', 'print', 'len', 'str', 'int', 'list', 'dict', 'set'}
465
+ params = {arg.arg for arg in node.args.args}
466
+
467
+ return list(deps - builtins - params)
468
+
469
+ def _extract_class_dependencies(self, node: ast.ClassDef) -> List[str]:
470
+ """Extrae dependencias de una clase."""
471
+ deps = set()
472
+
473
+ # Herencia
474
+ for base in node.bases:
475
+ if isinstance(base, ast.Name):
476
+ deps.add(base.id)
477
+
478
+ # Contenido
479
+ for child in ast.walk(node):
480
+ if isinstance(child, ast.Name):
481
+ deps.add(child.id)
482
+
483
+ return list(deps)
484
+
485
+ def _analyze_javascript(
486
+ self,
487
+ file_path: Path,
488
+ content: str
489
+ ) -> Tuple[List[Symbol], List[str], List[str]]:
490
+ """Analiza código JavaScript/TypeScript."""
491
+ symbols = []
492
+ imports = []
493
+ exports = []
494
+
495
+ # Regex patterns para JS/TS
496
+ import_patterns = [
497
+ r'import\s+.*?\s+from\s+[\'"]([^\'"]+)[\'"]',
498
+ r'import\s+[\'"]([^\'"]+)[\'"]',
499
+ r'require\s*\(\s*[\'"]([^\'"]+)[\'"]\s*\)',
500
+ ]
501
+
502
+ for pattern in import_patterns:
503
+ for match in re.finditer(pattern, content):
504
+ imports.append(match.group(0))
505
+
506
+ # Funciones
507
+ func_pattern = r'(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\([^)]*\)'
508
+ for match in re.finditer(func_pattern, content):
509
+ name = match.group(1)
510
+ line_num = content[:match.start()].count('\n') + 1
511
+ symbols.append(Symbol(
512
+ name=name,
513
+ type=SymbolType.FUNCTION,
514
+ file_path=str(file_path),
515
+ line_start=line_num,
516
+ line_end=line_num, # Simplificado
517
+ ))
518
+ exports.append(name)
519
+
520
+ # Clases
521
+ class_pattern = r'(?:export\s+)?class\s+(\w+)'
522
+ for match in re.finditer(class_pattern, content):
523
+ name = match.group(1)
524
+ line_num = content[:match.start()].count('\n') + 1
525
+ symbols.append(Symbol(
526
+ name=name,
527
+ type=SymbolType.CLASS,
528
+ file_path=str(file_path),
529
+ line_start=line_num,
530
+ line_end=line_num,
531
+ ))
532
+ exports.append(name)
533
+
534
+ return symbols, imports, exports
535
+
536
+ def _find_relevant_symbols(
537
+ self,
538
+ index: FileIndex,
539
+ query: str
540
+ ) -> List[Symbol]:
541
+ """Encuentra símbolos relevantes basados en la query."""
542
+ query_lower = query.lower()
543
+ query_words = set(query_lower.split())
544
+
545
+ scored = []
546
+ for sym in index.symbols:
547
+ score = 0
548
+
549
+ # Nombre del símbolo en query
550
+ if sym.name.lower() in query_lower:
551
+ score += 10
552
+
553
+ # Palabras del query en nombre o docstring
554
+ name_lower = sym.name.lower()
555
+ doc_lower = (sym.docstring or "").lower()
556
+
557
+ for word in query_words:
558
+ if word in name_lower:
559
+ score += 3
560
+ if word in doc_lower:
561
+ score += 1
562
+
563
+ # Símbolos exportados son más relevantes
564
+ if sym.exported:
565
+ score += 2
566
+
567
+ if score > 0:
568
+ scored.append((score, sym))
569
+
570
+ # Ordenar por score
571
+ scored.sort(key=lambda x: -x[0])
572
+
573
+ return [sym for _, sym in scored[:10]]
574
+
575
+ def _read_file(self, path: Path) -> str:
576
+ """Lee el contenido de un archivo con cache."""
577
+ path_str = str(path)
578
+ if path_str in self._content_cache:
579
+ return self._content_cache[path_str]
580
+
581
+ try:
582
+ content = path.read_text(encoding="utf-8", errors="replace")
583
+ # Solo cachear archivos pequeños
584
+ if len(content) < 100000:
585
+ self._content_cache[path_str] = content
586
+ return content
587
+ except Exception:
588
+ return ""
589
+
590
+ def _resolve_import(self, import_stmt: str, from_file: Path) -> Optional[Path]:
591
+ """Intenta resolver un import a un archivo."""
592
+ # Simplificado - busca archivos con el mismo nombre
593
+ # TODO: Implementar resolución completa de imports
594
+
595
+ # Extraer nombre del módulo
596
+ match = re.search(r'(?:from\s+(\S+)\s+import|import\s+(\S+))', import_stmt)
597
+ if not match:
598
+ return None
599
+
600
+ module = match.group(1) or match.group(2)
601
+ if not module:
602
+ return None
603
+
604
+ # Buscar archivo
605
+ module_path = module.replace('.', '/')
606
+ for ext in ['.py', '.js', '.ts']:
607
+ candidate = self.project_root / f"{module_path}{ext}"
608
+ if candidate.exists():
609
+ return candidate
610
+
611
+ return None
612
+
613
+
614
+ # ══════════════════════════════════════════════════════════════════════════════
615
+ # INSTANCIA GLOBAL
616
+ # ══════════════════════════════════════════════════════════════════════════════
617
+
618
+ _loader_instance: Optional[SelectiveLoader] = None
619
+
620
+
621
+ def get_selective_loader(project_root: Path = None) -> SelectiveLoader:
622
+ """Obtiene la instancia global del cargador selectivo."""
623
+ global _loader_instance
624
+ if _loader_instance is None or (project_root and _loader_instance.project_root != project_root):
625
+ _loader_instance = SelectiveLoader(project_root or Path.cwd())
626
+ return _loader_instance
@@ -0,0 +1,5 @@
1
+ # hanus/error_recovery/__init__.py
2
+ """Error recovery and auto-fix system for HanusCode."""
3
+ from .auto_fix import ErrorRecovery, FixSuggestion, ErrorPattern
4
+
5
+ __all__ = ["ErrorRecovery", "FixSuggestion", "ErrorPattern"]