# Analisis de bugs y refactorizacion Fecha de revision: 2026-06-08 ## Resumen ejecutivo El proyecto compila y la app importa correctamente, pero hay fallos funcionales y de seguridad que pueden hacer que la interfaz prometa capacidades que el backend no ejecuta, que algunas busquedas fallen silenciosamente, y que datos externos se rendericen como HTML sin escape. Hallazgos mas importantes: - La configuracion de fuentes esta desalineada: la UI usa IDs en mayusculas y el motor usa IDs en minusculas. - Scopus, CORE y SerpAPI no reciben API keys aunque existan en `.env`. - La normalizacion de anos puede romper busquedas con `ValueError` o `TypeError`. - Hay varias rutas de XSS/HTML injection al insertar metadatos externos en `gr.HTML`. - Los controles de sistema pueden matar todos los procesos `python.exe`. - La app crea y muestra credenciales por defecto `admin/admin123`. - Las API keys se devuelven al frontend en la pestana de modelos. - El mecanismo de detener pipeline usa `StopAsyncIteration` de forma insegura para async generators. - La refactorizacion prioritaria debe centralizar fuentes/providers, sanitizar salidas HTML y separar configuracion sensible del cliente. ## Verificaciones realizadas - `python -m compileall -q .`: OK. - `venv\Scripts\python.exe -c "import app; print('app import ok')"`: OK. - `python -m pytest -q`: falla porque `pytest` no esta instalado. - `venv\Scripts\python.exe -m pytest -q`: falla porque `pytest` no esta instalado en el `venv`. - Se reprodujeron fallos con providers simulados: - filtro de fuentes con `OPENALEX` devuelve 0 resultados, con `openalex` devuelve 1. - `year='s.f.'` rompe el filtro de anos. - mezcla `year='2020'` y `year=2019` rompe el ordenamiento. ## Severidad - P0: riesgo de seguridad o accion destructiva. - P1: rompe flujo principal o fuente importante. - P2: inconsistencia funcional, deuda tecnica con alto costo futuro. - P3: mejora de robustez, UX o mantenibilidad. ## Hallazgos detallados ### 1. Configuracion de fuentes inconsistente Severidad: P1 Archivos: - `modules/config/sources_config_tab.py` - `backend/tools/search_engine.py` - `backend/providers/sources.py` - `config.py` - `modules/research_tab.py` Sintoma: La pestana de configuracion guarda fuentes como `OPENALEX`, `SCOPUS`, `LA_REFERENCIA`, pero el motor de busqueda filtra contra `openalex`, `scopus`, `lareferencia`. Causa raiz: Hay varias fuentes de verdad: - `modules/config/sources_config_tab.py` define IDs en mayusculas. - `backend/providers/sources.py` define grupos reales usados por `search_engine`. - `config.py` define otro mapa mas amplio con fuentes que no estan implementadas. - `modules/research_tab.py` define `ALL_SOURCES` manualmente. Ademas, en `sources_config_tab.py`, la linea que inicializa `_enabled_sources` dentro de `create_sources_config_tab()` no declara `global`, por lo que crea una variable local. Al arrancar, la configuracion queda ignorada. Cuando el usuario cambia un checkbox, `_update_enabled()` si usa `global`, pero guarda IDs en mayusculas y puede filtrar todo. Impacto: - La UI puede decir que una fuente esta activa, pero el backend no la usa. - El usuario puede desactivar/activar fuentes y dejar la busqueda sin providers efectivos. - Dificulta diagnosticar por que "no hay resultados". Correccion recomendada: - Crear un registro unico de fuentes, por ejemplo `backend/providers/registry.py`. - Usar siempre IDs canonicos en minusculas. - Hacer que la UI derive sus opciones desde ese registro. - Normalizar aliases antes de guardar configuracion. - Eliminar o consolidar `config.py` si no es la fuente real. Criterio de aceptacion: - Activar `OpenAlex` en UI guarda `openalex`. - Desactivar `openalex` realmente evita llamadas a OpenAlex. - Seleccionar `all` expande solo providers implementados o marca claramente los no implementados. Pruebas minimas: - `expand_sources(["all"])` solo devuelve IDs canonicos. - `enabled_sources=["OPENALEX"]` se normaliza a `["openalex"]`. - `enabled_sources=["openalex"]` permite resultados de OpenAlex. ### 2. Scopus, CORE y SerpAPI no reciben API keys Severidad: P1 Archivos: - `backend/tools/search_engine.py` - `backend/providers/scopus.py` - `backend/providers/core_.py` - `backend/providers/serpapi.py` Sintoma: Los providers premium o con clave devuelven lista vacia si `api_key` no se pasa como argumento. El motor los llama asi: ```python PROVIDERS[src](query, limit=min(max_results, 50)) ``` Causa raiz: Las claves estan en `.env`, pero no existe una capa que lea `SCOPUS_API_KEY`, `CORE_API_KEY` o `SERPAPI_API_KEY` y las inyecte al provider correspondiente. Impacto: - Scopus, CORE y SerpAPI aparecen configurables pero no funcionan. - El sistema no distingue "sin resultados" de "no habia credencial". Correccion recomendada: - En el registro unico, cada provider debe declarar `env_key`, `requires_key` y `callable`. - `search_engine.search()` debe resolver la clave por provider y pasarla como `api_key`. - Si falta una clave requerida, devolver metadata tipo `sourceStatus` en lugar de silencio. Criterio de aceptacion: - Con `SCOPUS_API_KEY` en `.env`, `search(..., sources=["scopus"])` llama a Scopus con la clave. - Sin clave, el resultado indica `scopus: missing_api_key`. ### 3. Filtro y ordenamiento por ano rompen con datos heterogeneos Severidad: P1 Archivo: - `backend/tools/search_engine.py` Sintoma: El filtro usa `int(r.get("year", 0))` sin validar. Si un provider devuelve `s.f.`, `N/A`, una fecha completa o un string no numerico, la busqueda completa falla. Reproduccion: - `year='s.f.'` + `year_start='2020'` lanza `ValueError`. - `year='2020'` y `year=2019` sin filtro lanza `TypeError` al ordenar. Causa raiz: No hay normalizacion de metadatos a la entrada del motor. Correccion recomendada: - Crear `parse_year(value) -> Optional[int]`. - Normalizar todos los resultados inmediatamente despues de recibirlos. - Ordenar con una key que siempre devuelva int: `parse_year(x.get("year")) or 0`. - Los filtros deben ignorar o conservar documentos sin ano segun politica explicita. Criterio de aceptacion: - `year="2020"`, `year=2020`, `year="2020-05-01"` se tratan como 2020. - `year="s.f."`, `None`, `"N/A"` no rompen. ### 4. HTML injection / XSS en resultados, referencias y grafo Severidad: P0 Archivos: - `modules/search_tab.py` - `modules/research_tab.py` - `modules/graph_module.py` - `assets/custom.js` Sintoma: Campos de proveedores externos se insertan directo en HTML: - titulo - autores - abstract - DOI - PDF URL - fuente - contenido generado por IA Tambien se usan `onclick` inline y `innerHTML`. Impacto: Un resultado academico malicioso o un dato corrupto puede inyectar HTML/JS en la app. Aunque sea local, el riesgo aumenta si se comparte con usuarios, se usa `share=True`, o se abre en red. Correccion recomendada: - Escapar texto con `html.escape`. - Validar URLs con `urllib.parse`; permitir solo `http` y `https`. - Construir atributos JS con `json.dumps`, no con reemplazos manuales. - Evitar `onclick` inline; delegar eventos desde JS con `data-*`. - En grafo, usar `textContent` para texto y crear nodos DOM en vez de concatenar strings con `innerHTML`. - Revisar salida Markdown a HTML; si se usa `markdown`, sanitizar o restringir tags. Criterio de aceptacion: - Un titulo como `` se muestra como texto, no ejecuta codigo. - Una URL `javascript:alert(1)` no se renderiza como link. ### 5. Controles destructivos: reiniciar y matar procesos Severidad: P0 Archivo: - `app.py` Sintoma: Los botones de control ejecutan: - `taskkill /F /IM python.exe /T` - `taskkill` con `shell=True` Impacto: - Puede matar la app actual. - Puede matar otros procesos Python del usuario. - Puede interrumpir trabajos no relacionados. Correccion recomendada: - Eliminar el boton "Matar Procesos" de la UI normal. - Si se requiere restart, reiniciar solo el proceso actual con un supervisor controlado. - Evitar `shell=True`. - No matar por nombre de proceso global. Criterio de aceptacion: - Ningun boton de UI mata todos los `python.exe`. - Reinicio, si existe, afecta solo a la instancia actual. ### 6. Credenciales por defecto y hash debil Severidad: P0/P1 segun despliegue Archivo: - `app.py` Sintoma: La app crea `admin/admin123` y lo muestra en el mensaje de login. La contrasena se almacena con SHA-256 simple, sin sal ni factor de coste. Impacto: - Cualquier usuario que vea el login sabe la credencial. - Si se filtra la base, el hash es barato de romper. Correccion recomendada: - Exigir `LETXIPU_ADMIN_PASSWORD` o crear usuario en un comando setup. - No mostrar credenciales en UI. - Usar `passlib` con bcrypt/argon2 o `werkzeug.security`. - Forzar cambio de password inicial. Criterio de aceptacion: - Sin password configurado, la app no crea admin debil. - El mensaje de login no contiene credenciales. ### 7. API keys expuestas al frontend Severidad: P0/P1 Archivo: - `modules/config/ai_tab.py` Sintoma: La pestana de modelos precarga `MISTRAL_API_KEY` en un textbox y al cambiar provider devuelve la clave al cliente. Impacto: - Cualquier persona autenticada puede inspeccionar el HTML/estado y extraer secretos. - Aumenta el riesgo si la app se comparte. Correccion recomendada: - Mostrar solo estado: configurada/no configurada. - Si se permite actualizar claves, hacerlo con un flujo de escritura al `.env` cuidadosamente autorizado. - Nunca devolver claves existentes al navegador. Criterio de aceptacion: - El frontend no recibe valores completos de API keys. - El endpoint/evento solo devuelve mascara, por ejemplo `sk-...abcd`. ### 8. Detener pipeline puede terminar como error generico Severidad: P1/P2 Archivos: - `backend/pipeline.py` - `modules/research_tab.py` Sintoma: `_checkpoint()` levanta `StopAsyncIteration`. En async generators, Python convierte esto en `RuntimeError: async generator raised StopAsyncIteration`. Impacto: - El boton detener puede mostrar error generico en vez de estado "detenido". - La limpieza final puede no ejecutarse como se esperaba. Correccion recomendada: - Crear excepcion propia: ```python class PipelineStopped(Exception): pass ``` - Levantar `PipelineStopped`. - Capturar `PipelineStopped` en handlers. Criterio de aceptacion: - Pulsar detener muestra estado detenido y no un traceback/error generico. ### 9. Cambio de proveedor IA en Research devuelve forma incorrecta Severidad: P2 Archivo: - `modules/research_tab.py` Sintoma: `update_models()` devuelve un solo `gr.update`, pero esta conectado a tres outputs: busqueda, sintesis y traduccion. Impacto: - Al cambiar proveedor, Gradio puede fallar o actualizar solo un componente. Correccion recomendada: - Devolver tres updates: ```python update = gr.update(choices=models, value=models[0]) return update, update, update ``` o una lista de tres updates. Criterio de aceptacion: - Cambiar de `mistral` a `groq` actualiza los tres dropdowns. ### 10. Descarga de PDFs: SSRF, TLS deshabilitado y sin limite de tamano Severidad: P0/P1 Archivos: - `backend/tools/pdf_tools.py` - `backend/tools/pdf_processor.py` - `modules/pdf_tab.py` - `modules/chat_tab.py` Sintoma: Se descargan URLs ingresadas por el usuario desde el servidor. Algunas descargas usan `verify=False`. No hay limite de tamano ni allowlist/bloqueo de IPs internas. Impacto: - SSRF contra servicios internos si la app se expone. - Descarga de archivos enormes que agotan memoria o disco. - TLS sin verificar permite MITM. Correccion recomendada: - Activar verificacion TLS. - Bloquear IPs privadas/locales: `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, link-local, metadata cloud. - Limitar tamano por `Content-Length` y streaming con max bytes. - Permitir solo `http`/`https`. - Reusar un downloader comun. Criterio de aceptacion: - URL `http://127.0.0.1/...` se rechaza. - PDF mayor al limite se corta con error claro. ### 11. Catalogo de fuentes promete providers no implementados Severidad: P2 Archivos: - `README.md` - `config.py` - `modules/config/sources_config_tab.py` - `backend/providers/sources.py` - `backend/tools/search_engine.py` Sintoma: Se mencionan fuentes como SciELO, CONAHCyT, UNAM, ANID, OasisBR, SNRD, MinCiencias, OpenReview, PapersWithCode o HuggingFace, pero el mapa real `PROVIDERS` no contiene implementaciones para muchas de ellas. Impacto: - La UI genera expectativas falsas. - Los grupos `all`, `latam`, `ai_ml` pueden incluir fuentes que no hacen nada. Correccion recomendada: - El registro unico debe marcar `implemented=True/False`. - La UI debe ocultar fuentes no implementadas o mostrarlas como "proximamente". - Los grupos operativos deben incluir solo implementadas. Criterio de aceptacion: - No se puede seleccionar una fuente no implementada como si estuviera activa. ### 12. Manejo de errores silencioso en providers Severidad: P2 Archivos: - `backend/providers/*.py` - `backend/providers/base.py` - `backend/tools/search_engine.py` Sintoma: Muchos providers hacen `except Exception: return []`. `fetch_json()` tambien convierte cualquier error en `{"error": str(e)}`. Impacto: - Timeouts, credenciales faltantes, 403/429 y errores de parseo se ven igual que "0 resultados". - Dificulta depurar fuentes rotas. Correccion recomendada: - Devolver estructura por fuente: ```python { "source": "openalex", "ok": true, "results": [], "error": None, "status": "ok" } ``` - `search()` debe conservar `sourceErrors` y mostrarlos en UI. Criterio de aceptacion: - Si PubMed falla por timeout, la UI muestra "PubMed timeout" y no solo "sin resultados". ## Plan de refactorizacion recomendado ### Fase 1: estabilizacion funcional Objetivo: que las busquedas basicas sean confiables. Tareas: - Crear `backend/providers/registry.py`. - Consolidar grupos y aliases en un solo lugar. - Normalizar IDs de fuentes a minusculas. - Pasar API keys por provider desde `.env`. - Agregar `parse_year()` y normalizacion de resultados. - Corregir `update_models()` para multiples outputs. - Reemplazar `StopAsyncIteration` por `PipelineStopped`. Resultado esperado: - `search()` no se cae por anos raros. - Fuentes activadas en UI coinciden con providers usados. - Scopus/CORE/SerpAPI usan claves si existen. - Detener pipeline funciona sin error generico. ### Fase 2: seguridad de interfaz y secretos Objetivo: eliminar riesgos P0. Tareas: - Escapar HTML en `search_tab`, `research_tab` y `graph_module`. - Validar links antes de renderizar. - Quitar `onclick` inline donde sea posible. - No devolver API keys al frontend. - Remover credenciales por defecto o exigir password por env. - Eliminar controles `taskkill` globales. Resultado esperado: - Datos externos no ejecutan HTML/JS. - Las claves no viajan al navegador. - La app no mata procesos ajenos. ### Fase 3: downloader PDF seguro Objetivo: robustecer lectura/vectorizacion/chat con PDFs. Tareas: - Crear `backend/tools/downloader.py`. - Bloquear IPs internas y esquemas no permitidos. - Descargar en streaming con limite de tamano. - Activar TLS verification. - Unificar `pdf_tools.py`, `pdf_processor.py` y `chat_tab.py`. Resultado esperado: - El PDF local funciona sin abrir SSRF ni OOM. ### Fase 4: arquitectura de resultados y errores Objetivo: dejar de mezclar "sin resultados" con "fuente rota". Tareas: - Definir `SearchResult` y `SourceSearchOutcome`. - Hacer que providers devuelvan resultados normalizados. - Agregar `sourcesUsed`, `sourcesSkipped`, `sourceErrors`. - Mostrar estado por fuente en UI. Resultado esperado: - La UI puede decir: "OpenAlex OK, PubMed timeout, Scopus falta API key". ### Fase 5: pruebas y CI local Objetivo: evitar regresiones. Tareas: - Agregar `pytest` a `requirements-dev.txt` o `requirements.txt`. - Tests unitarios para: - expansion de fuentes - normalizacion de aliases - parseo de anos - providers con/sin API key - escape HTML - detener pipeline - Tests de integracion con providers simulados. Resultado esperado: - `pytest -q` corre sin depender de red. - Los bugs reproducidos quedan cubiertos. ## Orden de implementacion sugerido 1. `registry.py` de fuentes/providers. 2. Normalizacion de resultados y anos. 3. API keys por provider. 4. Correccion de `update_models()` y `PipelineStopped`. 5. Escape HTML y validacion de URLs. 6. Retiro de `taskkill`, credenciales por defecto y exposicion de secrets. 7. Downloader PDF seguro. 8. Tests. ## Archivos mas importantes para tocar - `backend/tools/search_engine.py` - `backend/providers/sources.py` - `backend/providers/registry.py` nuevo - `modules/config/sources_config_tab.py` - `modules/research_tab.py` - `modules/search_tab.py` - `modules/graph_module.py` - `modules/config/ai_tab.py` - `backend/tools/pdf_tools.py` - `backend/tools/pdf_processor.py` - `modules/chat_tab.py` - `app.py` - `requirements.txt` o `requirements-dev.txt` ## Nota sobre el estado del repo El arbol de trabajo ya tenia cambios modificados y archivos sin seguimiento antes de este informe. No se debe hacer `reset` ni revertir esos cambios sin revisar su origen.