snapcontext 3.3.0__tar.gz → 3.5.0__tar.gz

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 (46) hide show
  1. {snapcontext-3.3.0/snapcontext.egg-info → snapcontext-3.5.0}/PKG-INFO +85 -3
  2. {snapcontext-3.3.0 → snapcontext-3.5.0}/README.md +84 -2
  3. {snapcontext-3.3.0 → snapcontext-3.5.0}/agentes.py +210 -45
  4. {snapcontext-3.3.0 → snapcontext-3.5.0}/orquestador.py +6 -1
  5. {snapcontext-3.3.0 → snapcontext-3.5.0}/pyproject.toml +1 -1
  6. {snapcontext-3.3.0 → snapcontext-3.5.0/snapcontext.egg-info}/PKG-INFO +85 -3
  7. {snapcontext-3.3.0 → snapcontext-3.5.0}/snapcontext.egg-info/SOURCES.txt +2 -0
  8. {snapcontext-3.3.0 → snapcontext-3.5.0}/snapcontext.py +573 -5
  9. snapcontext-3.5.0/tests/test_asesor_350.py +265 -0
  10. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_auto_017.py +1 -1
  11. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_diagnostico_310.py +1 -1
  12. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_diff_editor_210.py +1 -1
  13. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_editor_330.py +1 -1
  14. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_editor_ast_220.py +1 -1
  15. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_editor_propio_200.py +1 -1
  16. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_exe_150.py +1 -1
  17. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_jetbrains_170.py +1 -1
  18. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_mcp_014.py +1 -1
  19. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_mcp_140.py +1 -1
  20. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_memoria_015.py +1 -1
  21. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_memoria_300.py +1 -1
  22. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_permisos_013.py +1 -1
  23. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_plan_012.py +1 -1
  24. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_plan_mcp_230.py +1 -1
  25. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_v010.py +1 -1
  26. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_validacion_130.py +1 -1
  27. snapcontext-3.5.0/tests/test_validacion_sintaxis_340.py +195 -0
  28. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_vscode_016.py +1 -1
  29. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_web_160.py +1 -1
  30. {snapcontext-3.3.0 → snapcontext-3.5.0}/web/app.py +7 -0
  31. {snapcontext-3.3.0 → snapcontext-3.5.0}/LICENSE +0 -0
  32. {snapcontext-3.3.0 → snapcontext-3.5.0}/setup.cfg +0 -0
  33. {snapcontext-3.3.0 → snapcontext-3.5.0}/snapcontext.egg-info/__pycache__/top_level.cpython-313.pyc +0 -0
  34. {snapcontext-3.3.0 → snapcontext-3.5.0}/snapcontext.egg-info/dependency_links.txt +0 -0
  35. {snapcontext-3.3.0 → snapcontext-3.5.0}/snapcontext.egg-info/entry_points.txt +0 -0
  36. {snapcontext-3.3.0 → snapcontext-3.5.0}/snapcontext.egg-info/requires.txt +0 -0
  37. {snapcontext-3.3.0 → snapcontext-3.5.0}/snapcontext.egg-info/top_level.txt +0 -0
  38. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_agentes.py +0 -0
  39. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_bienvenida_311.py +0 -0
  40. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_deteccion.py +0 -0
  41. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_embeddings_110.py +0 -0
  42. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_orquestador.py +0 -0
  43. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_vscode_ts_320.py +0 -0
  44. {snapcontext-3.3.0 → snapcontext-3.5.0}/tests/test_web_120.py +0 -0
  45. {snapcontext-3.3.0 → snapcontext-3.5.0}/web/__init__.py +0 -0
  46. {snapcontext-3.3.0 → snapcontext-3.5.0}/web/static/index.html +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: snapcontext
3
- Version: 3.3.0
3
+ Version: 3.5.0
4
4
  Summary: SnapContext: Asistente de IA con contexto automático para desarrollo
5
5
  Author-email: SnapContext Contributors <tu@email.com>
6
6
  License-Expression: MIT
@@ -45,7 +45,7 @@ Dynamic: license-file
45
45
 
46
46
  # SnapContext
47
47
 
48
- ![v2.1.0](https://img.shields.io/badge/version-2.1.0-blue.svg)
48
+ ![v3.5.0](https://img.shields.io/badge/version-3.5.0-blue.svg)
49
49
  [![PyPI](https://badge.fury.io/py/snapcontext.svg)](https://pypi.org/project/snapcontext/)
50
50
  [![CI](https://img.shields.io/github/actions/workflow/status/NicolasBruna24/snapcontext/ci.yml?branch=main&label=tests)](https://github.com/NicolasBruña24/snapcontext/actions)
51
51
  ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)
@@ -199,8 +199,81 @@ snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
199
199
 
200
200
  # Refactorización estructural con AST
201
201
  snapcontext "renombrar la variable 'usuario' a 'cuenta'" --editor propio --modo-edicion ast
202
+
203
+ # Validación de sintaxis desactivada (comportamiento previo a v3.4.0)
204
+ snapcontext "corregir tipado" --editor propio --no-validar-sintaxis
205
+ ```
206
+
207
+ ### Validación de sintaxis antes de guardar (v3.4.0)
208
+
209
+ Cuando uses `--editor propio`, SnapContext **valida la sintaxis** del código que
210
+ ha generado la IA **antes de escribirlo en disco**. Si la validación falla, el
211
+ cambio se rechaza y se le envía el error al proveedor para que lo corrija,
212
+ repitiéndose hasta `--max-intentos-validacion` intentos (por defecto 3). Si tras
213
+ esos intentos el código sigue roto, se **cancela la edición** y el archivo queda
214
+ intacto.
215
+
216
+ - Se detecta el lenguaje del archivo y se usa el validador adecuado:
217
+ Python (`py_compile`), JavaScript/TypeScript (`node --check`), Dart
218
+ (`dart analyze` → `dart format`), Go (`go build -n` → `gofmt -e`), Rust
219
+ (`rustc --parse-only`), Java (`javac -Xlint:none`), C/C++ (`gcc`/`clang
220
+ -fsyntax-only`). Si no hay validador o comando disponible, se omite la
221
+ validación (solo aviso en logs con `--depurar`).
222
+ - La validación se hace sobre un **archivo temporal**, nunca toca el original.
223
+ - Flag `--validar` (por defecto activado) / `--no-validar-sintaxis` para
224
+ desactivarla. Nota: el nombre usa `--no-validar-sintaxis` porque
225
+ `--no-validar` ya es alias de `--iniciar-proyecto`.
226
+
227
+ ```bash
228
+ # Desactivar la validación por completo (comportamiento anterior)
229
+ snapcontext "..." --editor propio --no-validar-sintaxis
230
+
231
+ # Ajustar el número de reintentos
232
+ snapcontext "..." --editor propio --max-intentos-validacion 5
202
233
  ```
203
234
 
235
+ ### 🧠 Asesor de código proactivo (v3.5.0)
236
+
237
+ SnapContext no solo ejecuta órdenes: también **analiza tu código por su cuenta**
238
+ y te sugiere mejoras (refactorizaciones, deuda técnica, optimizaciones).
239
+
240
+ ```bash
241
+ # Análisis informativo (nunca modifica código)
242
+ snapcontext --asesor
243
+ snapcontext --sugerir # alias
244
+
245
+ # Aplicar automáticamente las mejoras seguras (renombrar símbolos);
246
+ # cada cambio se valida antes de guardarse y se descarta si rompe la sintaxis
247
+ snapcontext --asesor-auto
248
+
249
+ # Ajustar la sensibilidad del detector de funciones largas (defecto: 20)
250
+ snapcontext --asesor --asesor-umbral 30
251
+ ```
252
+
253
+ **Qué detecta:**
254
+
255
+ - 🔴 **Prioridad alta**: `except:` desnudos (capturan todo sin querer).
256
+ - 🟡 **Prioridad media**: funciones largas (> 20 líneas), clases con demasiadas
257
+ responsabilidades (> 10 métodos), bloques de código duplicados entre archivos,
258
+ comparaciones `== None` y patrones obsoletos (`.has_key()`, Python 2).
259
+ - 🔵 **Prioridad baja**: nombres poco descriptivos (`d`, `tmp`...) con una
260
+ sugerencia concreta de renombrado.
261
+
262
+ Cada sugerencia incluye descripción, ubicación `archivo:línea`, solución
263
+ propuesta y prioridad. Los umbrales se personalizan en
264
+ `~/.snapcontext/config.json`:
265
+
266
+ ```json
267
+ { "asesor": { "funcion_larga": 30, "clase_metodos": 15, "duplicado_lineas": 8 } }
268
+ ```
269
+
270
+ El asesor también está disponible:
271
+
272
+ - **En el chat**: comando `/asesor` (o `/sugerir`) dentro de `--chat`.
273
+ - **En el planificador**: paso `{"accion": "asesor", "descripcion": "..."}` —
274
+ cada sugerencia se presenta para aceptarla o rechazarla individualmente.
275
+ - **En la web**: acción *Asesor* que muestra las sugerencias en un panel.
276
+
204
277
  ## 🧭 Modos y alias
205
278
 
206
279
  | Modo | Comando |
@@ -475,7 +548,7 @@ snapcontext --plan "..." # commits '
475
548
  (requiere `ANTHROPIC_API_KEY` y `pip install snapcontext[anthropic]`). Modelo por
476
549
  defecto: `claude-3-5-sonnet-20241022`.
477
550
  - **Modo chat interactivo**: `snapcontext --chat` abre un REPL con los comandos
478
- `/salir`, `/archivos`, `/limpiar`, `/seleccion <consulta>`,
551
+ `/salir`, `/archivos`, `/limpiar`, `/seleccion <consulta>`, `/asesor`,
479
552
  `/provider <proveedor>`, `/historial` y `/ayuda`. Cualquier otro texto se envía
480
553
  al proveedor actual manteniendo la conversación.
481
554
  - **Memoria persistente**: cada tarea se guarda en `~/.snapcontext/historial.json`
@@ -1013,6 +1086,15 @@ locales y te deja elegir con las flechas:
1013
1086
  | `--url-defecto` | `http://localhost:5000` | URL para abrir el navegador si Flutter no reporta una |
1014
1087
  | `--comando-test` | `"flutter test"` | Comando del bucle de pruebas |
1015
1088
  | `--max-iteraciones` | `3` | Iteraciones máximas del bucle |
1089
+ | `--validar` | on | Valida la sintaxis del código antes de guardar en el editor propio |
1090
+ | `--no-validar-sintaxis` | off | Desactiva la validación de sintaxis del editor propio |
1091
+ | `--max-intentos-validacion` | `3` | Intentos de validación de sintaxis antes de cancelar la edición |
1092
+ | `--asesor` (`--sugerir`) | off | Asesor proactivo: analiza y sugiere mejoras sin modificar código |
1093
+ | `--asesor-auto` | off | Aplica automáticamente las mejoras seguras del asesor |
1094
+ | `--asesor-umbral` | `20` | Líneas máximas por función para el asesor |
1095
+
1096
+ > **Nota:** el flag negativo de validación de sintaxis se llama `--no-validar-sintaxis`
1097
+ > porque `--no-validar` ya existe como alias de `--iniciar-proyecto`.
1016
1098
 
1017
1099
  ---
1018
1100
 
@@ -1,6 +1,6 @@
1
1
  # SnapContext
2
2
 
3
- ![v2.1.0](https://img.shields.io/badge/version-2.1.0-blue.svg)
3
+ ![v3.5.0](https://img.shields.io/badge/version-3.5.0-blue.svg)
4
4
  [![PyPI](https://badge.fury.io/py/snapcontext.svg)](https://pypi.org/project/snapcontext/)
5
5
  [![CI](https://img.shields.io/github/actions/workflow/status/NicolasBruna24/snapcontext/ci.yml?branch=main&label=tests)](https://github.com/NicolasBruña24/snapcontext/actions)
6
6
  ![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue.svg)
@@ -154,8 +154,81 @@ snapcontext "corregir tipado en login" --editor propio --modo-edicion parche
154
154
 
155
155
  # Refactorización estructural con AST
156
156
  snapcontext "renombrar la variable 'usuario' a 'cuenta'" --editor propio --modo-edicion ast
157
+
158
+ # Validación de sintaxis desactivada (comportamiento previo a v3.4.0)
159
+ snapcontext "corregir tipado" --editor propio --no-validar-sintaxis
160
+ ```
161
+
162
+ ### Validación de sintaxis antes de guardar (v3.4.0)
163
+
164
+ Cuando uses `--editor propio`, SnapContext **valida la sintaxis** del código que
165
+ ha generado la IA **antes de escribirlo en disco**. Si la validación falla, el
166
+ cambio se rechaza y se le envía el error al proveedor para que lo corrija,
167
+ repitiéndose hasta `--max-intentos-validacion` intentos (por defecto 3). Si tras
168
+ esos intentos el código sigue roto, se **cancela la edición** y el archivo queda
169
+ intacto.
170
+
171
+ - Se detecta el lenguaje del archivo y se usa el validador adecuado:
172
+ Python (`py_compile`), JavaScript/TypeScript (`node --check`), Dart
173
+ (`dart analyze` → `dart format`), Go (`go build -n` → `gofmt -e`), Rust
174
+ (`rustc --parse-only`), Java (`javac -Xlint:none`), C/C++ (`gcc`/`clang
175
+ -fsyntax-only`). Si no hay validador o comando disponible, se omite la
176
+ validación (solo aviso en logs con `--depurar`).
177
+ - La validación se hace sobre un **archivo temporal**, nunca toca el original.
178
+ - Flag `--validar` (por defecto activado) / `--no-validar-sintaxis` para
179
+ desactivarla. Nota: el nombre usa `--no-validar-sintaxis` porque
180
+ `--no-validar` ya es alias de `--iniciar-proyecto`.
181
+
182
+ ```bash
183
+ # Desactivar la validación por completo (comportamiento anterior)
184
+ snapcontext "..." --editor propio --no-validar-sintaxis
185
+
186
+ # Ajustar el número de reintentos
187
+ snapcontext "..." --editor propio --max-intentos-validacion 5
157
188
  ```
158
189
 
190
+ ### 🧠 Asesor de código proactivo (v3.5.0)
191
+
192
+ SnapContext no solo ejecuta órdenes: también **analiza tu código por su cuenta**
193
+ y te sugiere mejoras (refactorizaciones, deuda técnica, optimizaciones).
194
+
195
+ ```bash
196
+ # Análisis informativo (nunca modifica código)
197
+ snapcontext --asesor
198
+ snapcontext --sugerir # alias
199
+
200
+ # Aplicar automáticamente las mejoras seguras (renombrar símbolos);
201
+ # cada cambio se valida antes de guardarse y se descarta si rompe la sintaxis
202
+ snapcontext --asesor-auto
203
+
204
+ # Ajustar la sensibilidad del detector de funciones largas (defecto: 20)
205
+ snapcontext --asesor --asesor-umbral 30
206
+ ```
207
+
208
+ **Qué detecta:**
209
+
210
+ - 🔴 **Prioridad alta**: `except:` desnudos (capturan todo sin querer).
211
+ - 🟡 **Prioridad media**: funciones largas (> 20 líneas), clases con demasiadas
212
+ responsabilidades (> 10 métodos), bloques de código duplicados entre archivos,
213
+ comparaciones `== None` y patrones obsoletos (`.has_key()`, Python 2).
214
+ - 🔵 **Prioridad baja**: nombres poco descriptivos (`d`, `tmp`...) con una
215
+ sugerencia concreta de renombrado.
216
+
217
+ Cada sugerencia incluye descripción, ubicación `archivo:línea`, solución
218
+ propuesta y prioridad. Los umbrales se personalizan en
219
+ `~/.snapcontext/config.json`:
220
+
221
+ ```json
222
+ { "asesor": { "funcion_larga": 30, "clase_metodos": 15, "duplicado_lineas": 8 } }
223
+ ```
224
+
225
+ El asesor también está disponible:
226
+
227
+ - **En el chat**: comando `/asesor` (o `/sugerir`) dentro de `--chat`.
228
+ - **En el planificador**: paso `{"accion": "asesor", "descripcion": "..."}` —
229
+ cada sugerencia se presenta para aceptarla o rechazarla individualmente.
230
+ - **En la web**: acción *Asesor* que muestra las sugerencias en un panel.
231
+
159
232
  ## 🧭 Modos y alias
160
233
 
161
234
  | Modo | Comando |
@@ -430,7 +503,7 @@ snapcontext --plan "..." # commits '
430
503
  (requiere `ANTHROPIC_API_KEY` y `pip install snapcontext[anthropic]`). Modelo por
431
504
  defecto: `claude-3-5-sonnet-20241022`.
432
505
  - **Modo chat interactivo**: `snapcontext --chat` abre un REPL con los comandos
433
- `/salir`, `/archivos`, `/limpiar`, `/seleccion <consulta>`,
506
+ `/salir`, `/archivos`, `/limpiar`, `/seleccion <consulta>`, `/asesor`,
434
507
  `/provider <proveedor>`, `/historial` y `/ayuda`. Cualquier otro texto se envía
435
508
  al proveedor actual manteniendo la conversación.
436
509
  - **Memoria persistente**: cada tarea se guarda en `~/.snapcontext/historial.json`
@@ -968,6 +1041,15 @@ locales y te deja elegir con las flechas:
968
1041
  | `--url-defecto` | `http://localhost:5000` | URL para abrir el navegador si Flutter no reporta una |
969
1042
  | `--comando-test` | `"flutter test"` | Comando del bucle de pruebas |
970
1043
  | `--max-iteraciones` | `3` | Iteraciones máximas del bucle |
1044
+ | `--validar` | on | Valida la sintaxis del código antes de guardar en el editor propio |
1045
+ | `--no-validar-sintaxis` | off | Desactiva la validación de sintaxis del editor propio |
1046
+ | `--max-intentos-validacion` | `3` | Intentos de validación de sintaxis antes de cancelar la edición |
1047
+ | `--asesor` (`--sugerir`) | off | Asesor proactivo: analiza y sugiere mejoras sin modificar código |
1048
+ | `--asesor-auto` | off | Aplica automáticamente las mejoras seguras del asesor |
1049
+ | `--asesor-umbral` | `20` | Líneas máximas por función para el asesor |
1050
+
1051
+ > **Nota:** el flag negativo de validación de sintaxis se llama `--no-validar-sintaxis`
1052
+ > porque `--no-validar` ya existe como alias de `--iniciar-proyecto`.
971
1053
 
972
1054
  ---
973
1055
 
@@ -232,9 +232,55 @@ class AgenteEditorPropio:
232
232
  return sc._editor_ast(archivo, tarea, directorio=directorio,
233
233
  proveedor=proveedor, modelo=modelo)
234
234
 
235
+ @staticmethod
236
+ def _aplicar_parche_preview(parche: str, contenido_actual: str):
237
+ """Aplica ``parche`` a ``contenido_actual`` **en memoria** para poder
238
+ validar la sintaxis del resultado sin tocar el archivo real.
239
+
240
+ Reutiliza la segmentación de hunks de ``_parsear_hunks`` y aplica cada
241
+ hunk con tolerancia a pequeños desfases. Devuelve ``(resultado, n)``
242
+ donde ``n`` es el número de hunks aplicados (0 ⇒ no se pudo reproducir).
243
+ """
244
+ import snapcontext as sc
245
+
246
+ resultado = (contenido_actual or "").splitlines()
247
+ desplazamiento = 0
248
+ aplicados = 0
249
+ for inicio_orig, cambios in sc._parsear_hunks(parche):
250
+ base = max(inicio_orig - 1 + desplazamiento, 0)
251
+ n_borrados = sum(1 for marca, _ in cambios if marca != "+")
252
+ nuevos = [texto for marca, texto in cambios if marca != "-"]
253
+
254
+ posicion = -1
255
+ for delta in (0, 1, -1, 2, -2, 3, -3, 5, -5, 10, -10):
256
+ candidato = base + delta
257
+ if candidato < 0 or candidato > len(resultado):
258
+ continue
259
+ idx = candidato
260
+ encaja = True
261
+ for marca, texto in cambios:
262
+ if marca == "+":
263
+ continue
264
+ if idx >= len(resultado) or resultado[idx] != texto:
265
+ encaja = False
266
+ break
267
+ idx += 1
268
+ if encaja:
269
+ posicion = candidato
270
+ break
271
+ if posicion < 0:
272
+ continue
273
+
274
+ resultado[posicion:posicion + n_borrados] = nuevos
275
+ desplazamiento += len(nuevos) - n_borrados
276
+ aplicados += 1
277
+
278
+ return "\n".join(resultado) + "\n", aplicados
279
+
235
280
  def _aplicar_modo_parche(self, archivo: str, mensaje: str,
236
281
  contenido_actual: str, modelo: Optional[str],
237
- directorio: str) -> bool:
282
+ directorio: str, validar: bool = True,
283
+ max_intentos_validacion: int = 3) -> bool:
238
284
  """Intenta editar `archivo` pidiendo un parche unificado al proveedor."""
239
285
  import snapcontext as sc
240
286
 
@@ -243,57 +289,134 @@ class AgenteEditorPropio:
243
289
  lenguaje = sc._lenguaje_archivo(archivo, contenido_actual) or "?"
244
290
  num_lineas = (contenido_actual.count("\n") + 1
245
291
  if contenido_actual else 0)
246
- prompt = (
247
- f"Genera un parche unificado (unified diff) que modifique el archivo "
248
- f"para cumplir con la tarea con precisión máxima.\n"
249
- f"El parche debe ser aplicable con 'patch -p1' o 'git apply' "
250
- f"(encabezados --- a/{archivo} y +++ b/{archivo}).\n\n"
251
- f"Tarea: {mensaje}\n"
252
- f"Archivo: {archivo} (lenguaje: {lenguaje}, {num_lineas} líneas)\n\n"
253
- f"Instrucciones:\n"
254
- f"- Incluye suficiente contexto (3 líneas) para anclar cada hunk.\n"
255
- f"- Conserva el estilo existente (indentación, comillas).\n"
256
- f"- Modifica SOLO lo necesario; no reorganices el resto del archivo.\n\n"
257
- f"Contenido actual completo:\n```{lenguaje}\n{contenido_actual}\n```\n\n"
258
- f"Devuelve ÚNICAMENTE el diff unificado, sin explicaciones ni rodeos."
259
- )
260
- respuesta = sc._enviar_al_proveedor(
261
- proveedor, modelo, [{"role": "user", "content": prompt}])
262
- diff_limpio = respuesta
263
- if "--- " in diff_limpio and "+++ " in diff_limpio:
264
- idx_inicio = diff_limpio.find("--- ")
265
- diff_limpio = diff_limpio[idx_inicio:]
266
- if "```" in diff_limpio:
267
- diff_limpio = diff_limpio[:diff_limpio.find("```")]
268
- if "--- " in diff_limpio and "+++ " in diff_limpio and "@@" in diff_limpio:
269
- # v3.3.0: validación previa + resolución de conflictos.
270
- return self.aplicar_parche(diff_limpio, directorio,
271
- contenido_esperado=contenido_actual)
292
+ max_val = max(1, int(max_intentos_validacion))
293
+
294
+ error_msj = ""
295
+ for intento in range(1, max_val + 1):
296
+ prompt = (
297
+ f"Genera un parche unificado (unified diff) que modifique el archivo "
298
+ f"para cumplir con la tarea con precisión máxima.\n"
299
+ f"El parche debe ser aplicable con 'patch -p1' o 'git apply' "
300
+ f"(encabezados --- a/{archivo} y +++ b/{archivo}).\n\n"
301
+ f"Tarea: {mensaje}\n"
302
+ f"Archivo: {archivo} (lenguaje: {lenguaje}, {num_lineas} líneas)\n\n"
303
+ f"Instrucciones:\n"
304
+ f"- Incluye suficiente contexto (3 líneas) para anclar cada hunk.\n"
305
+ f"- Conserva el estilo existente (indentación, comillas).\n"
306
+ f"- Modifica SOLO lo necesario; no reorganices el resto del archivo.\n"
307
+ )
308
+ if error_msj:
309
+ prompt += (
310
+ f"\nTu intento anterior produjo un error de sintaxis que debes "
311
+ f"corregir:\n{error_msj}\n\n"
312
+ f"Mantén el objetivo de la tarea pero arregla ese error.\n"
313
+ )
314
+ prompt += (
315
+ f"\nContenido actual completo:\n```{lenguaje}\n{contenido_actual}\n```\n\n"
316
+ f"Devuelve ÚNICAMENTE el diff unificado, sin explicaciones ni rodeos."
317
+ )
318
+ respuesta = sc._enviar_al_proveedor(
319
+ proveedor, modelo, [{"role": "user", "content": prompt}])
320
+ diff_limpio = respuesta
321
+ if "--- " in diff_limpio and "+++ " in diff_limpio:
322
+ idx_inicio = diff_limpio.find("--- ")
323
+ diff_limpio = diff_limpio[idx_inicio:]
324
+ if "```" in diff_limpio:
325
+ diff_limpio = diff_limpio[:diff_limpio.find("```")]
326
+ if not ("--- " in diff_limpio and "+++ " in diff_limpio
327
+ and "@@" in diff_limpio):
328
+ return False
329
+
330
+ if not validar:
331
+ # v3.3.0: validación previa + resolución de conflictos.
332
+ return self.aplicar_parche(diff_limpio, directorio,
333
+ contenido_esperado=contenido_actual)
334
+
335
+ # v3.4.0: validar la sintaxis del contenido resultante.
336
+ preview, aplicados = self._aplicar_parche_preview(
337
+ diff_limpio, contenido_actual)
338
+ if aplicados == 0:
339
+ # No se pudo reproducir en memoria → se omite la validación.
340
+ return self.aplicar_parche(diff_limpio, directorio,
341
+ contenido_esperado=contenido_actual)
342
+
343
+ sc.info(f"Validando sintaxis de {archivo}...")
344
+ exito, err = sc._validar_sintaxis(archivo, preview, directorio)
345
+ if exito:
346
+ sc.exito("Sintaxis válida.")
347
+ return self.aplicar_parche(diff_limpio, directorio,
348
+ contenido_esperado=contenido_actual)
349
+
350
+ error_msj = err
351
+ if intento < max_val:
352
+ sc.error(
353
+ f"Error de sintaxis: {err}. "
354
+ f"Reintentando ({intento}/{max_val})...")
355
+ else:
356
+ sc.error(
357
+ f"No se pudo validar tras {max_val} intentos. "
358
+ f"Edición cancelada.")
359
+ return False
272
360
  return False
273
361
 
274
362
  def _aplicar_modo_sobrescribir(self, archivo: str, mensaje: str,
275
363
  contenido_actual: str, modelo: Optional[str],
276
- directorio: str) -> bool:
364
+ directorio: str, validar: bool = True,
365
+ max_intentos_validacion: int = 3) -> bool:
277
366
  """Sobrescribe `archivo` con el código completo que devuelve el proveedor."""
278
367
  import snapcontext as sc
279
368
 
280
369
  pref = sc.cargar_configuracion()
281
370
  proveedor = pref.get("provider") or sc.PROVEEDOR_DEFECTO
282
371
  lenguaje = sc._lenguaje_archivo(archivo, contenido_actual) or "?"
283
- prompt = (
284
- f"Modifica el siguiente archivo ({lenguaje}) para cumplir con la "
285
- f"tarea, conservando el estilo existente y cambiando solo lo necesario.\n\n"
286
- f"Tarea: {mensaje}\n"
287
- f"Archivo: {archivo} (lenguaje: {lenguaje})\n\n"
288
- f"Contenido actual completo:\n```{lenguaje}\n{contenido_actual}\n```\n\n"
289
- f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
290
- )
291
- nuevo_contenido = sc._enviar_al_proveedor(
292
- proveedor, modelo, [{"role": "user", "content": prompt}])
293
- if nuevo_contenido.startswith("```"):
294
- lineas = nuevo_contenido.splitlines()
295
- if len(lineas) >= 2 and lineas[-1].startswith("```"):
296
- nuevo_contenido = "\n".join(lineas[1:-1])
372
+ max_val = max(1, int(max_intentos_validacion))
373
+
374
+ error_msj = ""
375
+ nuevo_contenido = ""
376
+ for intento in range(1, max_val + 1):
377
+ prompt = (
378
+ f"Modifica el siguiente archivo ({lenguaje}) para cumplir con la "
379
+ f"tarea, conservando el estilo existente y cambiando solo lo necesario.\n\n"
380
+ f"Tarea: {mensaje}\n"
381
+ f"Archivo: {archivo} (lenguaje: {lenguaje})\n"
382
+ )
383
+ if error_msj:
384
+ prompt += (
385
+ f"\nTu intento anterior produjo un error de sintaxis que debes "
386
+ f"corregir:\n{error_msj}\n\n"
387
+ f"Mantén el objetivo de la tarea pero arregla ese error.\n"
388
+ )
389
+ prompt += (
390
+ f"\nContenido actual completo:\n```{lenguaje}\n{contenido_actual}\n```\n\n"
391
+ f"Devuelve ÚNICAMENTE el código completo resultante, sin explicaciones ni markdown."
392
+ )
393
+ respuesta = sc._enviar_al_proveedor(
394
+ proveedor, modelo, [{"role": "user", "content": prompt}])
395
+ nuevo_contenido = respuesta
396
+ if nuevo_contenido.startswith("```"):
397
+ lineas = nuevo_contenido.splitlines()
398
+ if len(lineas) >= 2 and lineas[-1].startswith("```"):
399
+ nuevo_contenido = "\n".join(lineas[1:-1])
400
+
401
+ if not validar:
402
+ break
403
+
404
+ sc.info(f"Validando sintaxis de {archivo}...")
405
+ exito, err = sc._validar_sintaxis(archivo, nuevo_contenido, directorio)
406
+ if exito:
407
+ sc.exito("Sintaxis válida.")
408
+ break
409
+ error_msj = err
410
+ if intento < max_val:
411
+ sc.error(
412
+ f"Error de sintaxis: {err}. "
413
+ f"Reintentando ({intento}/{max_val})...")
414
+ else:
415
+ sc.error(
416
+ f"No se pudo validar tras {max_val} intentos. "
417
+ f"Edición cancelada.")
418
+ return False
419
+
297
420
  return self.sobrescribir(archivo, nuevo_contenido, directorio)
298
421
 
299
422
  def ejecutar(
@@ -303,6 +426,8 @@ class AgenteEditorPropio:
303
426
  directorio: str = ".",
304
427
  modo_edicion: str = "auto",
305
428
  modelo: Optional[str] = None,
429
+ validar: bool = True,
430
+ max_intentos_validacion: int = 3,
306
431
  ) -> bool:
307
432
  """Aplica las modificaciones para `archivos` con el `mensaje` especificado.
308
433
 
@@ -353,10 +478,14 @@ class AgenteEditorPropio:
353
478
  str(raiz), modelo)
354
479
  elif estrategia == "parche":
355
480
  conseguido = self._aplicar_modo_parche(
356
- arch, mensaje, contenido_actual, modelo, str(raiz))
481
+ arch, mensaje, contenido_actual, modelo, str(raiz),
482
+ validar=validar,
483
+ max_intentos_validacion=max_intentos_validacion)
357
484
  elif estrategia == "sobrescribir":
358
485
  conseguido = self._aplicar_modo_sobrescribir(
359
- arch, mensaje, contenido_actual, modelo, str(raiz))
486
+ arch, mensaje, contenido_actual, modelo, str(raiz),
487
+ validar=validar,
488
+ max_intentos_validacion=max_intentos_validacion)
360
489
  else:
361
490
  conseguido = False
362
491
  except Exception as exc:
@@ -539,4 +668,40 @@ class AgenteAprendizaje:
539
668
  """Lista los skills almacenados en la memoria persistente."""
540
669
  import snapcontext as sc
541
670
 
542
- return sc._skill_listar(incluir_archivados=incluir_archivados)
671
+ return sc._skill_listar(incluir_archivados=incluir_archivados)
672
+
673
+
674
+ class AgenteAsesor:
675
+ """Asesor de código proactivo (v3.5.0).
676
+
677
+ Analiza el proyecto sin que el usuario lo pida explícitamente y propone
678
+ mejoras (refactorizaciones, deuda técnica, patrones obsoletos...). Por
679
+ defecto solo informa; las sugerencias marcadas como seguras pueden
680
+ aplicarse con ``--asesor-auto`` y siempre se validan antes de guardarse.
681
+ """
682
+
683
+ def analizar(self, directorio: str = ".",
684
+ umbral_funcion: Optional[int] = None) -> List[dict]:
685
+ """Ejecuta el análisis estático y devuelve la lista de sugerencias."""
686
+ import snapcontext as sc
687
+
688
+ return sc._asesor_analizar(directorio, umbral_funcion=umbral_funcion)
689
+
690
+ def mostrar(self, sugerencias: List[dict]) -> None:
691
+ """Muestra las sugerencias en la CLI con colores."""
692
+ import snapcontext as sc
693
+
694
+ sc._asesor_mostrar(sugerencias)
695
+
696
+ def aplicar_automaticas(self, sugerencias: List[dict],
697
+ directorio: str = ".") -> int:
698
+ """Aplica las mejoras seguras (auto=True); devuelve cuántas se aplicaron."""
699
+ import snapcontext as sc
700
+
701
+ return sc._asesor_aplicar_automaticas(sugerencias, directorio)
702
+
703
+ def ejecutar(self, args) -> int:
704
+ """Flujo completo del modo CLI (--asesor / --asesor-auto)."""
705
+ import snapcontext as sc
706
+
707
+ return sc._ejecutar_asesor(args)
@@ -21,7 +21,8 @@ import sys
21
21
  from typing import List, Optional, Tuple
22
22
 
23
23
  from agentes import (AgenteContexto, AgenteEditor, AgenteEditorAST,
24
- AgenteEditorPropio, AgenteAprendizaje, AgenteTester)
24
+ AgenteEditorPropio, AgenteAprendizaje, AgenteTester,
25
+ AgenteAsesor)
25
26
 
26
27
  # Centinela diferenciado del None de "aborto": _planificar lo devuelve cuando
27
28
  # el pipeline termina de forma exitosa y temprana (p. ej. --vista-previa).
@@ -44,6 +45,7 @@ class Orquestador:
44
45
  self.agente_editor_ast = AgenteEditorAST()
45
46
  self.agente_tester = AgenteTester()
46
47
  self.agente_aprendizaje = AgenteAprendizaje()
48
+ self.agente_asesor = AgenteAsesor()
47
49
  self.evento_callback = evento_callback
48
50
 
49
51
  def _on_evento(self, evento: dict) -> None:
@@ -190,6 +192,9 @@ class Orquestador:
190
192
  directorio=str(raiz),
191
193
  modo_edicion=modo_ed,
192
194
  modelo=getattr(args, "modelo", None),
195
+ validar=getattr(args, "validar", True),
196
+ max_intentos_validacion=getattr(
197
+ args, "max_intentos_validacion", 3),
193
198
  )
194
199
  self._emitir_tipo("editor", accion="fin", ok=ok)
195
200
  else:
@@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"
6
6
  # Nombre verificado disponible en PyPI (18/08/2026): pypi.org/pypi/snapcontext → 404.
7
7
  # Alternativas si algún día estuviera ocupado: "snapcontext-cli" o "snapcontext-tool".
8
8
  name = "snapcontext"
9
- version = "3.3.0"
9
+ version = "3.5.0"
10
10
  description = "SnapContext: Asistente de IA con contexto automático para desarrollo"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.9"