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.
- hanus/__init__.py +5 -0
- hanus/__main__.py +10 -0
- hanus/action_handlers.py +76 -0
- hanus/action_parser.py +82 -0
- hanus/agent_runner.py +1445 -0
- hanus/analysis/__init__.py +5 -0
- hanus/analysis/debt.py +702 -0
- hanus/analysis/dependencies.py +475 -0
- hanus/cache/__init__.py +5 -0
- hanus/cache/response_cache.py +560 -0
- hanus/config.py +401 -0
- hanus/connectors/__init__.py +19 -0
- hanus/connectors/base.py +114 -0
- hanus/connectors/claude_connector.py +146 -0
- hanus/connectors/gemini_connector.py +141 -0
- hanus/connectors/glm_connector.py +160 -0
- hanus/connectors/ollama_connector.py +174 -0
- hanus/connectors/openai_connector.py +122 -0
- hanus/connectors/registry.py +26 -0
- hanus/context/__init__.py +7 -0
- hanus/context/manager.py +837 -0
- hanus/context/selective.py +626 -0
- hanus/error_recovery/__init__.py +5 -0
- hanus/error_recovery/auto_fix.py +605 -0
- hanus/hooks/__init__.py +5 -0
- hanus/hooks/manager.py +247 -0
- hanus/instincts/__init__.py +44 -0
- hanus/instincts/cli.py +372 -0
- hanus/instincts/detector.py +281 -0
- hanus/instincts/evolver.py +361 -0
- hanus/instincts/manager.py +343 -0
- hanus/instincts/types.py +253 -0
- hanus/logger.py +81 -0
- hanus/memory/__init__.py +8 -0
- hanus/memory/manager.py +265 -0
- hanus/memory/types.py +119 -0
- hanus/monitor.py +341 -0
- hanus/parallel/__init__.py +5 -0
- hanus/parallel/executor.py +300 -0
- hanus/permissions.py +182 -0
- hanus/plan/__init__.py +8 -0
- hanus/plan/mode.py +267 -0
- hanus/plan/models.py +152 -0
- hanus/plugin_manager.py +754 -0
- hanus/plugin_registry.py +391 -0
- hanus/plugins/__init__.py +1 -0
- hanus/plugins/arena.py +630 -0
- hanus/plugins/code_review.py +123 -0
- hanus/plugins/cortex.py +1750 -0
- hanus/plugins/deps_check.py +27 -0
- hanus/plugins/git_ops.py +33 -0
- hanus/plugins/metasploit.py +530 -0
- hanus/plugins/notes.py +583 -0
- hanus/plugins/search_code.py +59 -0
- hanus/plugins/searchsploit.py +495 -0
- hanus/plugins/strategist.py +175 -0
- hanus/plugins/webui.py +5200 -0
- hanus/profiles.py +479 -0
- hanus/profiles_builtin/__init__.py +0 -0
- hanus/profiles_builtin/architect/profile.yaml +12 -0
- hanus/profiles_builtin/architect/system_prompt.txt +71 -0
- hanus/profiles_builtin/deep/profile.yaml +12 -0
- hanus/profiles_builtin/deep/system_prompt.txt +66 -0
- hanus/profiles_builtin/developer/__init__.py +0 -0
- hanus/profiles_builtin/developer/profile.yaml +9 -0
- hanus/profiles_builtin/developer/system_prompt.txt +176 -0
- hanus/profiles_builtin/speed/profile.yaml +12 -0
- hanus/profiles_builtin/speed/system_prompt.txt +51 -0
- hanus/project_tools.py +177 -0
- hanus/query_engine.py +1594 -0
- hanus/rules/__init__.py +237 -0
- hanus/search/__init__.py +5 -0
- hanus/search/semantic.py +596 -0
- hanus/session_manager.py +547 -0
- hanus/skill_manager.py +702 -0
- hanus/skills/__init__.py +4 -0
- hanus/subagent/__init__.py +8 -0
- hanus/subagent/agents/__init__.py +253 -0
- hanus/subagent/manager.py +309 -0
- hanus/subagent/types.py +266 -0
- hanus/suggestions/__init__.py +5 -0
- hanus/suggestions/proactive.py +451 -0
- hanus/tasks/__init__.py +8 -0
- hanus/tasks/manager.py +330 -0
- hanus/tasks/models.py +106 -0
- hanus/terminal_prompt.py +166 -0
- hanus/tools.py +1849 -0
- hanus/ui.py +939 -0
- hanuscode-1.0.0.dist-info/METADATA +1151 -0
- hanuscode-1.0.0.dist-info/RECORD +93 -0
- hanuscode-1.0.0.dist-info/WHEEL +5 -0
- hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
- 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
|