|
Download docs/DECISIONS.md from CamiloS1111/ragsolver: direct link, hf CLI and curl.
- Browser
- Download file 115 kB
-
https://huggingface.co/CamiloS1111/ragsolver/resolve/main/docs/DECISIONS.md
- Command line
-
hf download hf://CamiloS1111/ragsolver/docs/DECISIONS.md
-
curl -L -o DECISIONS.md https://huggingface.co/CamiloS1111/ragsolver/resolve/main/docs/DECISIONS.md
115 kB
| # Registro de decisiones — RagSolver / módulo RAG | |
| Formato: fecha, decisión, alternativa descartada, motivo, evidencia. Ver `docs/PLAN_RAG.md §0`. | |
| ## 2026-09-18 — Decisiones de arquitectura (§4, §9 del plan) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 1 | Base vectorial: **LanceDB** embebida | FAISS, Chroma, Qdrant | FAISS no tiene metadatos/BM25/filtros nativos; habría que reimplementar una base de datos alrededor. Qdrant exige servidor/Docker. | `docs/PLAN_RAG.md §9.2` | | |
| | 2 | Corpus formal: reusar el JSONL informalizado de **LeanSearch v2** (`FrenzyMath/lsv2-mathlib-v4.28.0-rc1-jsonl`, Apache 2.0) | Generar informalización propia con LLM sobre las 310k declaraciones | Ahorra ~310k llamadas LLM; misma receta que el SOTA publicado (nDCG@10=0.62) | arXiv:2605.13137 | | |
| | 3 | Mathlib pinneado a **v4.28.0-rc1** en índice y verificador | Última tag de Mathlib al momento de instalar | Debe coincidir exactamente con la tag del corpus de LeanSearch v2 (decisión #2), si no los `decl_name` recuperados pueden no existir en la versión que verifica el kernel | `configs/versions.yaml`; tag verificada contra la API de GitHub (`api.github.com/repos/leanprover-community/mathlib4/git/refs/tags/v4.28.0-rc1`, commit `5352afc...`) | | |
| | 4 | OCR de PDFs: **MinerU**, backend VLM, siempre desde imagen de página (nunca la capa de texto del PDF) | Confiar en la capa de texto del PDF | La capa de texto de `problemas resueltos olimpiadas.pdf` corrompe fórmulas (`2^2001` → `22001`, sumatorias desordenadas); confirmado con PyMuPDF sobre el PDF real | `docs/PLAN_RAG.md §3` | | |
| | 5 | Embeddings/reranker: **Qwen3-Embedding-0.6B** / **Qwen3-Reranker-0.6B**, sin fine-tuning | Fine-tuning propio; modelos de mayor tamaño por defecto | Sin fine-tuning ya alcanza SOTA en LeanSearch v2; el alcance de la propuesta excluye entrenar modelos desde cero (§5.3 de la propuesta) | `docs/PLAN_RAG.md §4` | | |
| | 6 | Conjunto de evaluación formal: **miniF2F-v2** | miniF2F original | El original tiene discrepancias formal/informal en >50% de problemas (paper de revisión); v2 trae pruebas verificadas → premisas gold gratis | arXiv:2511.03108, `roozbeh-yz/miniF2F_v2` | | |
| | 7 | Retriever con **0 llamadas LLM** en tiempo de ejecución | Recuperación asistida por LLM (p. ej. reformulación de consultas) | RNF-3 limita a ≤20 llamadas LLM/problema; el enriquecimiento con LLM ocurre sólo offline en la ingesta | `docs/PLAN_RAG.md §6.3` | | |
| ## 2026-09-18 — GPU AMD (ROCm), tarjeta del equipo | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 8 | ~~Asumir gfx1201 (RX 9070 XT)~~ **CORREGIDO 2026-09-19**: la tarjeta real es una **AMD Radeon RX 9060 XT** (confirmado vía `Get-CimInstance Win32_VideoController` desde PowerShell — este sandbox comparte máquina con la GPU real del usuario). La 9060 XT es **gfx1200** (RDNA4), no gfx1201 — es un chip distinto (más chico) al que se había asumido | gfx1201/RX 9070 XT (dato inventado por falta de info) | El nombre comercial exacto ahora es verificable; gfx1200 y gfx1201 son variantes RDNA4 distintas, no intercambiables para efectos de soporte de ROCm/MinerU | PowerShell `Win32_VideoController` en esta sesión: `Name: AMD Radeon RX 9060 XT` | | |
| | 9 | Instalar PyTorch ROCm ≥ 7.2 (misma rama que para gfx1201) contra **gfx1200**, a confirmar con `rocminfo` una vez instalado | Workaround `HSA_OVERRIDE_GFX_VERSION` | ROCm 7.2+ también lista gfx1200 (RX 9060 XT) como soportado oficialmente; aún no se instaló en este sandbox (torch actual es build CUDA, no ROCm; no hay `rocminfo`/`rocm-smi` en el sistema) | `torch.__version__` = `2.14.0+cu130`, `torch.cuda.is_available()=False` en esta sesión; sin `rocminfo`/`dpkg -l rocm*` | | |
| | 10 | MinerU sobre esta GPU: **verificar en Fase 0 (tarea 7) antes de construir la Fase 2**, no asumir que funciona | Instalar y descubrir en medio de la ingesta | MinerU sólo soporta oficialmente NVIDIA/Apple; sin evidencia pública conocida de MinerU corriendo sobre gfx1200 (ni sobre gfx1201) | issue `opendatalab/MinerU#5288`; `docs/PLAN_RAG.md §9.4` | | |
| | 11 | **Smoke test tarea 7, paso (a) — CONCLUIDO: ROCm en WSL2 no es viable hoy para esta tarjeta.** `rocminfo` falla con `HSA_STATUS_ERROR_OUT_OF_RESOURCES` de forma persistente (sobrevive a `wsl --shutdown` + reinicio completo). Se decide **no perseguir GPU local para esta fase**; usar CPU (ya en uso) + proveedor LLM/VLM por API para todo lo que necesite visión/enriquecimiento | Instalar `librocdxg` y seguir iterando drivers/versiones de ROCm | Es un bug conocido y sin resolver de AMD específico de esta tarjeta, no un error de configuración nuestro: **`ROCm/ROCm#6323`** reporta el mismo error exacto (`HSA_STATUS_ERROR_OUT_OF_RESOURCES`) en la misma GPU (RX 9060 XT / gfx1200) bajo WSL2, probando `apt`, `amdgpu-install --usecase=wsl,rocm` y el paquete manual `hsa-runtime-rocr4wsl-amdgpu`, con múltiples versiones de driver (Adrenalin 26.2.2/26.5.2) y de ROCm (7.1.0/7.2.1) — **cerrado por AMD como "not planned", sin fix**. Además, probamos `HSA_ENABLE_DXG_DETECTION=1` (mencionado en `ROCm/librocdxg#22` como forma de sortear este error): falla porque falta el paquete `librocdxg` (no viene con `rocminfo` de apt); y aunque se instalara, ese mismo issue reporta que en esta tarjeta específica la detección de VRAM (`GetSegmentId`) queda rota downstream incluso cuando `rocminfo` sí ve la GPU — es decir, hay un segundo bug esperando. No es cuestión de reintentar con más paciencia: son bugs abiertos de AMD sobre gfx1200 en WSL2. **Re-evaluar cuando AMD publique un fix** (o correr Lean/embeddings/MinerU en Linux nativo o Windows nativo con ROCm/DirectML fuera de WSL, si se vuelve prioritario) | `dmesg`: `dxgk: dxgkio_query_adapter_info: Ioctl failed: -22`; `rocminfo`: `HSA_STATUS_ERROR_OUT_OF_RESOURCES` (antes y después de `wsl --shutdown`); GitHub `ROCm/ROCm#6323` (mismo hardware, mismo error, cerrado "not planned"); GitHub `ROCm/librocdxg#22` (VRAM detection rota en RX 9060 XT incluso pasando el primer error) | | |
| | 12 | Segundo intento (a pedido del usuario) — instalado `rocdxg-roct` **v1.2.2** (release más nueva de `ROCm/librocdxg`, Aug 2026, lista explícitamente RX 9060/9060 XT como soportada) vía el `.deb` oficial, más reciente que la v1.1.1 usada en los issues reportados. **Mismo resultado: falla igual.** Se confirma que no es un problema de versión de paquete | Seguir probando versiones/combinaciones | Con `HSA_ENABLE_DXG_DETECTION=1`, `rocminfo` ahora imprime `Load librocdxg.so successully! Load all DTIF APIs OK!` (antes ni cargaba la librería) pero igual termina en el mismo `HSA_STATUS_ERROR_OUT_OF_RESOURCES` — la falla está en la respuesta del servicio de paravirtualización de GPU de Windows al host, no en el lado Linux. Dato que refuerza la correlación con `ROCm/ROCm#6323`: ese reporte también era una **combinación iGPU+dGPU** (Ryzen 5600G con Radeon integrada + RX 9060 XT discreta), igual que esta máquina (`AMD Radeon(TM) Graphics` integrada + RX 9060 XT discreta vistas ambas en `Win32_VideoController`) — patrón consistente de que el bug afecta específicamente a sistemas con dos adaptadores AMD bajo WSL2. **Se cierra la investigación de GPU local para esta fase; se sigue en CPU + proveedor LLM/VLM por API** | `dpkg -i rocdxg-roct_1.2.2_amd64.deb` exitoso; `rocminfo` con `HSA_ENABLE_DXG_DETECTION=1`: carga la librería pero mismo `HSA_STATUS_ERROR_OUT_OF_RESOURCES` | | |
| ## 2026-09-18 — Ejecución (esta sesión) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | E1 | Python del proyecto: **3.12** vía `uv python install 3.12` en vez del `python3` del sistema (3.14.4) | Usar Python 3.14 del sistema | 3.14 es demasiado nuevo (semanas de salida); riesgo real de que torch/lancedb/mineru no tengan wheels aún. El plan ya fijaba 3.11–3.12 (`docs/PLAN_RAG.md §9.1`) | `uv python list` mostró sólo "download available" para 3.10–3.13 y binario de sistema en 3.14 | | |
| | E2 | Ejecutar la Fase 0 y arrancar la Fase 1 **en este sandbox** (CPU, sin GPU AMD visible: `rocminfo`/`lspci` no la detectan aquí) | Esperar a tener la GPU real conectada | El sandbox tiene red y CPU/disco suficientes para scaffolding, esquemas, tests, descarga del corpus e indexado de una muestra acotada; el embebido completo de 310k declaraciones y el smoke test de ROCm quedan pendientes de correr en la máquina real del usuario (ver estado en el reporte de la sesión) | recon del entorno al inicio de esta tarea | | |
| | E3 | `elan default leanprover/lean4:v4.28.0-rc1` (toolchain por defecto de elan, no sólo el `lean-toolchain` del proyecto) | Dejar `--default-toolchain none` | `lean-interact` (usado por el Verificador en Fase 4) invoca `lake` fuera del cwd del proyecto para chequear la versión; sin default falla con "no default toolchain configured" | prueba directa en esta sesión | | |
| ## Fase 1 — resultados reales de `evaluate.py` (2026-09-19) | |
| Corrida sobre la muestra acotada (13 736 filas: 116 gold de QR/MPR + 3000 distractores, ver entrada anterior) contra las 946 consultas de MathlibQR. Números crudos en `data/eval/results/fase1_mathlibqr_sample13736.json`. | |
| | Variante | Recall@10 (crudo) | nDCG@10 | MRR | | |
| |---|---|---|---| | |
| | Denso | 0.241 | 0.168 | 0.150 | | |
| | BM25 | 0.222 | 0.144 | 0.124 | | |
| | **Híbrido (RRF)** | **0.282** | **0.227** | **0.211** | | |
| | Híbrido + rerank | 0.280 | 0.214 | 0.193 | | |
| Cobertura: sólo 282/946 consultas (29.8%) tienen su declaración gold presente en esta tabla de 13 736 filas (la muestra no incluye el corpus completo de 278 560). El criterio de aceptación del plan ("Recall@10 ≥ 0.55") **no aplica directamente** a un recall crudo acotado por esa cobertura — el número justo es el **recall condicionado a cobertura** (crudo ÷ 0.298): Denso 0.809, BM25 0.745, **Híbrido 0.947**, Híbrido+rerank 0.940. Es decir: cuando la declaración correcta SÍ está en el índice, el sistema la encuentra en el top-10 el 94.7% de las veces con fusión híbrida — muy por encima del umbral del 55%. | |
| **Hallazgo no esperado, reportado tal cual**: el rerank (Qwen3-Reranker-0.6B) empeoró levemente las 3 métricas frente a híbrido sin rerank, en vez de mejorarlas. No se investigó a fondo la causa (candidatos posibles: ventana de candidatos ya buena a esta escala reducida de 13 736 filas con pocos distractores "difíciles"; posible desajuste de plantilla de instrucción del cross-encoder vía el wrapper genérico de `sentence-transformers`; ruido estadístico con sólo 282 consultas con señal positiva). **No se cambia la decisión de usar rerank en el diseño** porque: (a) la literatura de referencia (LeanSearch v2, Anthropic) consistentemente muestra mejoras con rerank a escala completa; (b) esta muestra tiene 20x menos distractores que el corpus real, así que no es representativa de la dificultad real de discriminar entre miles de candidatos similares. Repetir esta medición cuando se embeba el corpus completo (278 560) es la forma correcta de confirmar o descartar el rerank, no esta corrida acotada. | |
| ## Fase 2 — recalibración de la red de seguridad de figuras vectoriales (2026-09-19) | |
| `vector_cluster_min_paths: 6` (valor puesto a priori en el plan) resultó **demasiado estricto**: medido sobre el PDF real, sólo 4 de 115 clusters vectoriales del documento tienen ≥6 trazos — se perdían diagramas reales confirmados visualmente (problema 37: detectaba 1 de 6 sub-figuras "tipo uno".."tipo seis"; problemas 22/24 no se perdían de milagro porque coincidían justo en el borde). Cambiado a un filtro por **tamaño mínimo** (`vector_cluster_min_size_pt: 15.0`, ambas dimensiones del cluster ≥15pt), calibrado con los datos reales: reglas/subrayados decorativos con 1 trazo son ~12-15pt de alto y anchos; diagramas reales (incluso con sólo 2 trazos, p.ej. un círculo + una línea) miden ≥18pt en ambas dimensiones. Con el filtro nuevo: **7/7 de los problemas del QA gate del plan (8, 16, 17, 22, 24, 37, 99) tienen su figura detectada** — combinando el detector vectorial (22, 24, 37, 99 usan dibujo vectorial) con imágenes raster embebidas (8, 16, 17, 99 usan imágenes raster; MinerU debe cubrirlas como bloques `image`, no la red de seguridad). Tests de regresión en `tests/test_ingest_pdf.py`. | |
| ## Fase 2 — MinerU real y parser estructural (2026-09-19) | |
| - MinerU 4.0.3, modo **local** explícito (`parse_server.local.mode=managed`, tier `standard`). El default de MinerU trae un modo remoto (`mineru.net`) — **no se usó**, para no mandar el PDF del usuario a un servicio externo sin que lo autorice; se levantó el motor local (modelos ONNX de layout/OCR/fórmulas/tablas + VLM `MinerU2.5-Pro-1.2B-GGUF` vía llama.cpp) y se corrió todo ahí. Documento completo (77 páginas) parseado en 6m47s en CPU. | |
| - Calidad verificada a ojo: fórmulas en LaTeX limpio (`$$1+3+5+\dots+(2n-1)=n^2$$`), imágenes recortadas correctamente (verificado visualmente el recorte del problema 8 — círculos tangentes con ángulo 60°, coincide exactamente con el enunciado). | |
| - **Bug real encontrado y corregido en el parser estructural** (`parse_structure.py`): el primer intento emparejaba por coincidencia de línea `^N\.`, pero una solución posterior (con sub-casos "1. Prolongar el segmento DC...", "2. Prolongar DA...") pisaba silenciosamente a la solución 1 real con contenido de otro problema — **corrupción silenciosa**, peor que un faltante. Corregido exigiendo numeración no-decreciente (rechaza números menores al último visto); test de regresión en `tests/test_parse_structure.py::test_embedded_subcase_numbering_does_not_hijack_an_earlier_item`. | |
| - **Resultado final del emparejamiento determinista (sin LLM): 92/100** problema↔solución correctos. Los 8 restantes tienen el número de ítem atrapado dentro de una fórmula LaTeX (`\tag{100.}`, `"2 7. A = B"` dentro de un bloque `$$`) o, en el caso de la solución 1, el documento original simplemente no repite el numeral — ninguno es recuperable con regex; el plan ya preveía esto ("casos ambiguos → una llamada LLM"), pendiente de proveedor de LLM. | |
| - `data/corpus/informal.jsonl`: 100 `InformalChunk` reales, con `context_summary=""` (Contextual Retrieval pendiente de LLM) — `text` cae a `content_md` solo. | |
| ## Fase 0 — verificación final (tarea 5) | |
| `lake build` sobre `lean_project/` (Lean 4 `v4.28.0-rc1` + Mathlib `v4.28.0-rc1`, caché precompilado de 7873 archivos) terminó con **"Build completed successfully (3 jobs)"**: el kernel de Lean aceptó `example : 1 + 1 = 2 := rfl`. Es la misma verificación de fondo que exige el criterio de aceptación de Fase 0 (el plan menciona `lean-interact` como herramienta, pero el chequeo real es del kernel, no de qué wrapper lo invoca). `lean-interact` instalado y funcional para consultas simples (`lake --version` vía Python), pero su servidor REPL tarda en compilar la primera vez en este filesystem (`/mnt/c`, montaje 9p) — se retoma cuando se implemente el agente Verificador (Fase 4), no bloquea nada de lo hecho hasta ahora. | |
| ## Fase 2/4 — Proveedor de LLM/VLM: **Gemini** (2026-09-19) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 13 | Proveedor único para `vlm_enrichment` y `llm` (RNF-4): **Gemini**, modelo **`gemini-3.5-flash`** (API key del usuario, AI Studio, tier gratis) | `gemini-3-flash` (nombre inventado en la recomendación inicial — no existe en el API real) | El usuario pidió una opción sin costo por ahora. Se listaron los modelos reales vía `GET /v1beta/models` con la key real: `gemini-2.5-flash` ya no está disponible para usuarios nuevos (la API redirige a `gemini-3.6-flash`); se probó `gemini-3.5-flash` end-to-end: (a) texto simple, (b) salida JSON estructurada (`responseSchema`) — funciona, devuelve JSON válido, (c) visión sobre `data/parsed/_test/problem8_fig.png` — describe correctamente el diagrama (círculos tangentes, ángulo 60°, coincide con la verificación visual ya hecha en Fase 2). `gemini-3.6-flash` devolvió `503 UNAVAILABLE` ("high demand") de forma repetida en esta sesión — se prefiere `3.5-flash` por ser el que respondió consistentemente | Llamadas reales a `generativelanguage.googleapis.com/v1beta/models/*:generateContent` en esta sesión (texto, JSON estructurado, visión) | | |
| | 14 | Aviso de privacidad para el usuario, no bloqueante: en el tier gratis de Gemini, Google puede usar prompts/archivos enviados para mejorar sus productos (revisión humana posible), a diferencia del tier pago | — | El corpus B (`problemas resueltos olimpiadas.pdf`) es un libro publicado, no información sensible del usuario, así que se aceptó seguir; queda registrado por transparencia, siguiendo el mismo criterio que ya se aplicó con MinerU (decisión #4) | — | | |
| ## Fase 2 — cierre de la ingesta del PDF (2026-09-19, segunda sesión) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 15 | **Regenerar el Markdown desde el JSON de bloques de MinerU** (`rag/mineru_blocks.py`, esquema `docvortex.middle` 2.0) en vez de parchear el `full.md` de `mineru-kit` | Editar `full.md` con búsquedas de texto | Con los bloques (tipo, bbox normalizado, LaTeX) se puede insertar contenido en su posición de lectura, corregir LaTeX antes de renderizar y arreglar los numerales absorbidos; el render propio reproduce el de `mineru-kit` salvo diferencias conocidas (tablas en formato pipe, `<sup>/<sub>`, numerales) — verificado página a página en `tests/test_mineru_blocks.py` | 77 páginas, 12 con diferencias, todas en las categorías admitidas | | |
| | 16 | **Tarea 4 (red de seguridad vectorial): MinerU no perdió ninguna figura real.** De 75 clusters vectoriales, 39 caen en bloques `image` (≥60 %); los 36 restantes son radicales/paréntesis del Equation Editor dibujados como paths (caen en bloques `equation`/`text`), 2 tablas correctamente clasificadas (págs. 22 y 75) y 2 clusters que fusionan dos figuras que MinerU recortó por separado (págs. 54 y 76). La cobertura se calcula contra **todos** los bloques (incl. sub-bboxes de pies de figura), no sólo `image` | Umbral por cantidad de trazos | Verificado visualmente sobre los 4 casos ambiguos | `crop_figures()` devuelve 53 figuras y 0 avisos; `tests/test_figures.py` | | |
| | 17 | **Cross-check con la capa de texto del PDF para detectar contenido que el layout de MinerU se saltó** (`find_uncovered_regions`: palabras cuyo centro no cae en ningún bbox de MinerU, ≥4 por página) y **Plan B acotado**: sólo esas regiones se transcriben con el VLM (7 llamadas), no páginas completas | Plan B del plan (página completa por VLM) para todo el documento; confiar ciegamente en MinerU | Hallazgo: **la solución 77 completa** (pág. 64, un bloque de texto+fórmulas de ~10 líneas) **no existe en la salida de MinerU** — cero bloques en esa región — y hay 6 líneas/ecuaciones sueltas más perdidas (págs. 23, 25, 50, 57, 66 y el final de la solución 76). La capa de texto no sirve para leer fórmulas (§3 del plan) pero sí para saber dónde hay texto. Por bloque la señal es ruido (párrafos sueltos cubren sólo 30-50 % de los bloques ajustados de MinerU); por palabra es limpia: 9 páginas, 7 con contenido real | `pages[64]` sin bloques entre y=0.42 y y=0.75; render de la región confirmado visualmente; transcripción VLM recupera `77. 9^2 - 1 = ...` | | |
| | 18 | **Numerales absorbidos en ecuaciones se recuperan sin LLM** al renderizar (`split_item_number`): `2 7. A = B`, `\begin{array}{l} 5 2. ...`, `\tag{88.}`, `\tag{100.}` → línea `N.` + ecuación limpia. Emparejamiento determinista sube de 92 a **98/100**; la solución 1 (sin numeral en el original) se asigna por regla "lo que precede al ítem 2" | Split por LLM para los 8 | Regex acotada (punto pegado al último dígito; `\tag{1}` sin punto y decimales espaciados no matchean) y testeada; sólo 55 (numeral perdido por completo) y 77 (bloque perdido) necesitan VLM/LLM | `tests/test_mineru_blocks.py::test_numeral_fixes_raise_pairing_to_98_without_llm` | | |
| | 19 | **QA de fórmulas (6b) con guardas de aceptación** (`enrich._acceptable_fix`): el VLM propone muchos cambios cosméticos (puntuación, `\dots`→`...`), en fórmulas inline absorbe texto vecino (`A B = 3` → `AB = 3 \text{ cm}`), **recorta contenido real** cuando el OCR fusionó texto y matemática (`C_{100}^{200} \mathrm{es}\ 61` → pierde la respuesta `61`; `a^2+b^2 ~y~ a+b` → pierde `a+b`) y a veces devuelve LaTeX destrozado (`\underset` → `\raw` con caracteres de control por barras sin escapar en el JSON; `\mathbb{R}` → `ℒ`). Reglas: (a) caracteres de control o no-ASCII nuevos → rechazo; (b) borrado puro → sólo si cada trozo borrado es envoltorio/basura (sin dígitos ni operadores: `\mathrm{..}`, `\hat{..}`, «¿Cuál» absorbido); (c) otro cambio → similitud ≥ 0.8 (arreglo local como `r_{l}`→`r_{1}`) y en inline nunca alargar; cambios sólo de puntuación se ignoran | Aplicar todo lo que devuelve el VLM; o descartar 6b | Medido sobre las 70 páginas con ecuaciones del PDF (209 propuestas del VLM): con las reglas finales **76 aceptadas / 133 rechazadas**. Con la primera versión de las guardas (sólo «contenido alfanumérico distinto y no alargar inline») se aceptaban 94, entre ellas ~10 que perdían contenido real. Todo cambio y rechazo queda en `ingest_report.json` (`formula_diffs`) para revisión; las reglas se evalúan sobre la caché sin gastar cuota | `tests/test_enrich.py::test_qa_formulas_guards` (13 casos reales) | | |
| | 20 | **Reintentos**: `503 UNAVAILABLE ("high demand")` de Gemini es transitorio (segundos) → espera fija de 3 s; `429` (cuota) → exponencial 4→90 s. Hasta 20 intentos | Backoff exponencial para todo | Con backoff exponencial para 503 la corrida se quedaba minutos esperando por un error que se resuelve en segundos (medido: 200 OK y 503 alternados en llamadas consecutivas) | primera corrida: 1 llamada/min; `_wait()` en `enrich.py` | | |
| | 21 | **Cuota diaria del tier gratis de Gemini = 20 solicitudes/día por modelo** (`GenerateRequestsPerDayPerProjectPerModel-FreeTier`, `quotaValue: 20`, medido con `gemini-3.5-flash`) — no los 1500/día que reportaban artículos de terceros. Solución: **cadena de modelos** en `configs/versions.yaml` (`fallback_models`): al recibir 429 con `quotaId *PerDay*` el enriquecedor pasa al siguiente modelo y sigue; cada figura/card guarda `description_model` con el modelo real que la produjo, y la caché es independiente del modelo (un resultado ya calculado no se recalcula). Verificado que `gemini-3.5-flash-lite`, `gemini-3.1-flash-lite`, `gemma-4-31b-it`, `gemma-4-26b-a4b-it` y `gemini-3-flash-preview` aceptan imagen + `responseSchema` | Esperar 24 h por modelo; pasar al tier pago | ~230 llamadas por documento no caben en 20/día; las cuotas son por modelo, así que encadenarlos multiplica el presupuesto gratis sin cambiar de proveedor. Trade-off aceptado: heterogeneidad de modelos entre figuras/cards de un mismo documento (queda registrada por registro) | `curl` 429: `Quota exceeded for metric: generate_content_free_tier_requests, limit: 20, model: gemini-3.5-flash`; `tests/test_enrich.py` | | |
| | 22 | La API key va en el header `x-goog-api-key`, no en `?key=` | Query string (como en las pruebas iniciales con `curl`) | Con `?key=` la key aparece en la URL de cualquier traceback/log de `httpx` (pasó en `ingest_run.log` de la corrida fallida) | `enrich.py::_generate_with` | | |
| ## Fase 3 — servicio de recuperación y citación (2026-09-20) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 23 | **Plantillas de consulta por `mode`** (`configs/retrieval.yaml: query_instructions`) como instrucción de Qwen3-Embedding; los documentos siguen embebidos sin instrucción, así que cambiar una plantilla no obliga a re-indexar. La de `nl` es idéntica a la usada en la Fase 1 para que las métricas se reproduzcan | Una sola instrucción fija (Fase 1) | El model card de Qwen3-Embedding recomienda instrucciones específicas por tarea; el costo es cero en el índice | `embed.py::embed_queries(instruction=...)` + caché de embeddings de consulta (el orquestador repite consultas entre intentos) | | |
| | 24 | **Modo `error`**: identificadores Lean del mensaje (`unknown identifier 'X'`, `` `X` ``, nombres con punto) → búsqueda exacta `decl_name IN (...)` **fijada al tope del resultado** (por encima del rerank), más FTS con los tokens del nombre y denso sobre el resto del mensaje/goal | Confiar en FTS/denso para encontrar el nombre | Un identificador que existe es verdad mecánica: no debe depender de que el cross-encoder lo puntúe bien. Verificado con reranker adverso (test) y sobre el índice real | `tests/test_retriever.py::test_error_mode_pins_exact_identifier...`; `tests/test_retriever_integration.py` | | |
| | 25 | **Filtros §8.1 como prefiltro SQL de LanceDB** (`doc_ids`, `unit_types`, `kinds` en Mathlib, `math_area` en informal vía `array_has_any`), aplicados tanto al canal denso como al FTS | Post-filtrar los candidatos | Prefiltro = los N candidatos son todos válidos; post-filtro puede vaciar el resultado. `math_area` como *boost* (no exclusión) queda para el orquestador (Fase 4, tarea 3): el retriever ofrece el filtro estricto y el llamador decide | LanceDB 0.39: `where(..., prefilter=True)` en vector y FTS, `array_has_any` verificado | | |
| | 26 | **Dedupe**: una figura cuyo problem card también fue recuperado se colapsa en el card (que ya lleva `figure_ids` y las figuras adjuntas); una figura sola sigue siendo hit válido. `max_per_module` **desactivado por defecto** (0) | Devolver card y figura como dos hits; límite por módulo activo | Dos hits del mismo problema gastan presupuesto de contexto del LLM sin sumar información; el límite por módulo no se puede calibrar con la tabla de muestra (13 736 filas) — se deja el knob para la Fase 5 | `test_figure_chunk_collapses_into_its_card...` | | |
| | 27 | **Figuras adjuntas** (`include_figures`): tabla LanceDB `figures` (metadatos, sin vectores) construida desde `figures.jsonl`; el hit informal trae `[{figure_id, image_path, description, page, bbox, figure_type, labels}]` | Leer `figures.jsonl` en memoria; meter `image_path` en los chunks | Misma base de datos para todo (publicable como una carpeta en HF Hub, §9.2); `index.build_table(fts_columns=None)` cubre tablas sin FTS | `python -m ragsolver.rag.index --table figures` | | |
| | 28 | **`evaluate.py` pasa por `Retriever.search`** (aceptación de Fase 3: "misma función, sin duplicar lógica"): las 4 variantes sólo cambian `Retriever.channels` (denso / FTS / ambos) y `rerank` | Mantener el bucle propio de Fase 1 | Si la evaluación y los agentes usan código distinto, las métricas no describen lo que corre en producción | test de integración: orden del canal denso == `vector_search` crudo | | |
| | 29 | **CLI `ragsolver`** (`[project.scripts]`): `ragsolver search "..." --corpus informal --mode exemplar`, `ragsolver ingest --pdf ...` | Sólo `python -m ...` | Lo pide el plan (Fase 2 tarea 9, Fase 3 tarea 7); es un despachador de 20 líneas. Ojo: `uv sync` sin `--extra dev --extra ingest` desinstala pytest y MinerU (pasó en esta sesión) | `src/ragsolver/cli.py` | | |
| | 30 | **Ablación `enabled=false`** (Fase 4 tarea 7) ya implementada en el retriever: devuelve vacío y emite la traza igual (`llm_calls=0`, `hits=[]`) | Implementarla en el orquestador | 3 líneas y un test; así la métrica "contribución del RAG" (8.3) no depende de que el orquestador recuerde saltarse el nodo | `test_ablation_disabled_returns_empty_but_still_traces` | | |
| ## Corrección de Fase 1 encontrada en Fase 3 (2026-09-20) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 31 | **Bug de ingesta de Mathlib corregido**: en el JSONL de LeanSearch v2 el campo `name` es la **lista de componentes** del nombre (`["GCDMonoid","dvd_gcd"]` = `GCDMonoid.dvd_gcd`), no una lista de alias. `ingest_mathlib.py` tomaba `name[0]` → el 90 % de las declaraciones (todas las de ≥2 componentes: 280 701 de 310 579) quedaba con `decl_name`, `chunk_id` y `docs_url` incorrectos y **repetidos** (en la tabla de muestra: 13 736 filas, sólo 1 145 `chunk_id` únicos). Ahora `decl_name = ".".join(name)`, `decl_names_alt = []` | — | Detectado por el test de integración de Fase 3 (el orden del canal denso no coincidía con `vector_search` porque el RRF fusionaba filas distintas con el mismo id). **Invalida dos conclusiones de Fase 1**: (a) "sólo 117/540 nombres gold existen en el corpus" era el bug — con el nombre completo se encuentran **503/540**; (b) el "recall condicionado 0.947" estaba inflado: sólo matcheaban los 60/200 gold de un componente (`Lattice`, `GaloisConnection`...) y cualquier fila `Lattice.*` contaba como acierto para el gold `Lattice` | `tests/test_ingest_mathlib.py::test_name_components_are_joined...`; `scripts/build_eval_sample.py`: `gold_found_in_corpus: 503` | | |
| | 32 | **Números corregidos de Fase 1** (tabla nueva: 503 gold QR/MPR + 3000 distractores = 3 503 filas, ids únicos; 946 consultas de MathlibQR; cobertura 793/946 = 83.8 %): recall@10 **denso 0.665 / BM25 0.594 / híbrido 0.656**; nDCG@10 denso 0.565 / BM25 0.472 / híbrido 0.538; MRR denso 0.537 / híbrido 0.504. Recall condicionado a cobertura: denso 0.79, híbrido 0.78. **Con ids correctos el denso solo iguala o supera al híbrido** en esta muestra; BM25 aporta menos de lo que sugería la Fase 1. Se mantiene el híbrido como default del retriever porque el modo `error` (identificadores exactos) y las consultas con nombres de Mathlib dependen del canal léxico; la ablación queda documentada para Fase 5 | Cambiar el default a denso | La diferencia denso/híbrido es pequeña (< 1 punto de recall) y la muestra sigue siendo 80x menor que el corpus real; la decisión de canal se revisa con el corpus completo | `data/eval/results/fase1_redo_mathlibqr_sample3503_norerank.json` | | |
| ## Fase 4 — agentes y verificador (2026-09-20) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 33 | **Cliente LLM único** (`ragsolver/llm.py`): `provider: gemini` (REST) u `openai_compatible` (`base_url` + `api_key_env`: OpenRouter/OpenAI/Ollama/vLLM), misma interfaz `generate(prompt, schema, images, system)`, misma cadena de modelos por cuota diaria. El Enricher de Fase 2 pasa a usarlo (menos código duplicado). OpenRouter queda **preparado por config, sin probar** (no hay key) | Un cliente por proveedor | El usuario quiere poder migrar a OpenRouter cuando la cuota gratis de Gemini no alcance; el JSON Schema estándar se traduce al subconjunto de Gemini en un solo lugar | `configs/versions.yaml: llm`, `vlm_enrichment` (bloque comentado de OpenRouter) | | |
| | 34 | ~~Verificador = `lake env lean` por proceso (sin REPL)~~ **REEMPLAZADA por #40**: el backend por defecto pasó a ser el REPL persistente (`LeanChecker`, `lean-interact`) por el costo del import por proceso; `verify()` (sin REPL) sigue existiendo como respaldo. Lo que sí sigue vigente de esta entrada: **el enunciado nunca lo escribe el LLM** — el Formalizador devuelve sólo las tácticas tras `:= by` y se empalman en el `theorem` del problema (`splice_proof`); ver también #69-71 (validación de tácticas y auditoría de axiomas, que reemplazan la lista `sorry`/`admit`/`axiom`/`native_decide` de esta entrada) | `lean-interact` REPL; dejar que el LLM emita el teorema completo | Un LLM que reescribe el enunciado puede debilitarlo (es el fallo clásico en miniF2F); el empalme lo hace imposible | `src/ragsolver/lean/verify.py`, `tests/test_orchestrator.py` | | |
| | 35 | **Caché de Mathlib en ext4** (`~/ragsolver_cache/lean_project`, misma tag `v4.28.0-rc1`, oleans vía `lake exe cache get`): el verificador la usa si existe (`RAGSOLVER_LEAN_PROJECT` la sobreescribe). El repo del usuario no se mueve | Verificar contra `/mnt/c` | Medido: `import Mathlib` desde `/mnt/c` (9p) tardó 18 min bajo contención (2.6 GB RSS) y 4 min un archivo con imports acotados; copiar `.lake` (6.6 GB) por 9p iba a ~40 MB/min | `verify._lean_project()`; tiempos en `lean_project/scratch/time*.txt` | | |
| | 36 | Grafo LangGraph sin checkpointer persistente; presupuesto RNF-3 (`llm_budget: 20`) aplicado **antes de cada llamada** en el propio grafo; ablación "sin Crítico" = `k_max: 1` sin llamada al Crítico; "sin RAG" = `rag: false` (0 recuperaciones); líneas base A/B de la propuesta §8.2 como modos de una llamada | SqliteSaver; presupuesto sólo en el runner | Reanudar corridas no hace falta todavía; el presupuesto dentro del grafo garantiza el límite aunque el runner cambie | `orchestrator/graph.py`, `configs/agents.yaml: configs` | | |
| | 37 | **5 teoremas triviales de referencia** definidos por el agente (`data/eval/problems/dev_trivial.jsonl`: `n + 0 = n`, conmutatividad, cuadrado del binomio en ℤ, par ⇒ cuadrado par, transitividad de ∣ en ℤ) | — | La propuesta los cita en una "Sección 6.5" que no existe en el texto entregado | `Propuesta_Proyecto_Final_IA_UPTC.pdf` (§6.3 → §6.7) | | |
| ## Fase 5 — evaluación (2026-09-20) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 38 | **Premisas gold desde las pruebas verificadas publicadas por DeepSeek-Prover-V2** (`minif2f-solutions.zip`, 438 pruebas Lean 4/Mathlib sobre los enunciados de miniF2F-**v1**), cruzadas por nombre con los enunciados informales de miniF2F-v2s: 352 problemas, 402 lemas únicos, 5.3 premisas/problema (identificadores de la prueba que existen como `decl_name` en el corpus) | Pruebas de miniF2F-v2 (el plan asumía que las publicaba) | El repo público de miniF2F-v2 trae `formal_proof` vacío en las 488 filas. Las pruebas de DSP-V2 son verificadas y públicas; que sean sobre los enunciados v1 no afecta a "qué lemas usa una prueba real del problema" | `scripts/build_premise_gold.py` → `data/eval/premises_gold.jsonl` | | |
| | 39 | **Gold informal anotado por el asistente** (30 consultas en inglés: 15 dependientes de figura con entidades clave esperadas, 15 sin figura; paráfrasis y análogos de problemas del PDF), campo `annotator` explícito | Anotación humana | No hay anotador humano disponible en esta sesión; la anotación queda auditable en `data/eval/informal_gold.jsonl` para que el equipo la revise/corrija | `scripts/eval_informal_gold.py` | | |
| | 40 | **Presupuesto de RAM del pipeline de Fase 4** (máquina de 7.5 GB): embedder Qwen3-Embedding-0.6B en **bfloat16** (1.8 GB de pico vs 3.6 GB en fp32; métricas idénticas en 40 consultas: denso 0.75/0.62/0.583, híbrido 0.675/0.53/0.491), **sin reranker en la recuperación de los agentes** (`configs/agents.yaml: retrieval.rerank: false`; el cross-encoder son 2.4 GB más y sumó +2.5 puntos de recall@10 en la muestra), REPL de Lean 1.9 GB. Total ≈ 4.2 GB | fp32 + reranker + REPL (≈ 7 GB) | La primera corrida real de Fase 4 fue detenida por el sistema por memoria crítica con la configuración pesada. REPL medido: `import Mathlib` 284 s una vez, luego 0.2-0.4 s por teorema (`lean-interact` 0.11.5, sin `memory_hard_limit_mb` — con límite de espacio de direcciones el REPL muere con "failed to create thread" por el mmap de los .olean) | `RAGSOLVER_EMBED_BF16=0` restaura fp32 | | |
| ## Fase 6 — empaquetado (2026-09-20) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 41 | **Publicación en HF Hub sin el corpus informal**: el dataset lleva el índice de Mathlib (Apache 2.0), el corpus normalizado y la evaluación; el PDF de olimpiadas, sus recortes y su texto quedan fuera (`Document.redistributable=false`, licencia desconocida). El script sube `informal`/`figures` sólo si todos los documentos son redistribuibles | Subir todo | Plan §12 (licencias): "no publicar recortes ni texto de fuentes no redistribuibles; citar igual". Requiere `HF_TOKEN` y repo del usuario — no ejecutado | `scripts/publish_hf.py --dry-run` | | |
| | 42 | Demo Gradio con **rerank apagado por defecto** y carga perezosa de modelos; en un Space, el índice se descarga con `snapshot_download` (`RAGSOLVER_HF_DATASET`) | Cargar todo al arrancar | El cross-encoder son +2.4 GB; un Space gratuito tiene 16 GB pero el embedder + índice ya tardan minutos en arrancar | `demo/app.py` | | |
| | 43 | **Métricas de IR propias** (`rag/metrics.py`: recall@k, nDCG@k, MRR) en vez de `ranx` para las corridas de evaluación; `evaluate.py` y `eval_premises.py` guardan además los hits crudos (`_runs`) para recomputar sin repetir búsquedas | Seguir con `ranx` | En dos corridas largas del CLI (`--sample 200` y `--sample 100`, 4 variantes intercaladas, 30-60 min) `ranx` devolvió recall@10 ≈ 0.11 para todas las variantes; las **mismas listas de hits** guardadas en las trazas JSONL, recomputadas con las métricas propias, dan denso 0.76 / BM25 0.56 / híbrido 0.66 / híbrido+rerank 0.71 (y la misma muestra en proceso con `ranx` también da 0.76). No se pudo aislar la causa (sospecha: caché JIT de numba de `ranx` en procesos largos); las métricas propias se verifican iguales a `ranx` en `tests/test_metrics.py` sobre runs aleatorios | trazas `/tmp/ragsolver-eval-j12vkawz` (400 eventos) vs `fase1_redo_mathlibqr_sample3903_100q.json` original | | |
| | 44 | **Resultado de las ablaciones (n=13 dev)**: el Crítico es el componente que más aporta (+0.23 pass@1 vs. sin Crítico); el RAG no mejora pass@1 en este conjunto (−0.08, McNemar p=1.0) y multiplica los tokens ×2.9; la verificación formal evita 5/13 falsos positivos de la línea base sin verificación. Se reporta tal cual; **no se cambia el diseño** | Quitar el RAG del pipeline | n=13 con problemas triviales/`mathd` que se cierran con `simp`/`omega`/`norm_num`/`ring`/`decide`; el aporte del RAG hay que medirlo en el conjunto de 30-60 problemas estratificados del plan (§8.1), donde hacen falta lemas concretos. Precisión de uso de lemas 0.011: el Autoformalizador rara vez cita nombres; las re-recuperaciones por `lema faltante` (3) no encontraron los identificadores propuestos por el Crítico (nombres inexistentes en Mathlib) | `data/eval/runs/f5-ablations/`, `docs/eval_report.md` | | |
| | 45 | **Runner reanudable + parada limpia por cuota**: con el mismo `--run-id` se saltan los pares (config, problema) ya presentes en `results.jsonl`; si todos los modelos agotan la cuota diaria, la corrida termina con `status: quota_exhausted` (los problemas pendientes no se marcan como fallos) y se relanza al día siguiente con el mismo comando (`scripts/run_eval_v1.sh`) | Marcar los pendientes como error; empezar de cero | El experimento principal (~1 800 llamadas) no cabe en un día de tier gratis y la sesión puede reiniciarse | `tests/test_orchestrator.py::test_run_suite_resumes...` | | |
| | 46 | **Self-consistency del Generador** (propuesta §6.3) implementada como `generator_samples: n` — n bocetos, se elige el medoide por solapamiento de tokens; **apagada por defecto** (n=1) y cada muestra cuenta en el presupuesto RNF-3 | Votación sobre "respuesta final" | Los bocetos de prueba no tienen una respuesta discreta para votar; el medoide es la versión estándar para salidas abiertas. Encendida triplica las llamadas del Generador, por eso se reserva como configuración de ablación opcional | `tests/test_orchestrator.py::test_generator_self_consistency...` | | |
| | 47 | **Experimento principal (`eval_v1`) con OpenRouter y un solo modelo** (`deepseek/deepseek-v4-flash-0731`, $0.04/M entrada, $0.16/M salida; respaldo `google/gemini-2.5-flash-lite`), tope de gasto de US$0.50 fijado por el usuario en la cuenta | Tier gratis de Gemini con cadena de modelos (como en el desarrollo) | (a) 5 configs × 60 problemas ≈ 1.6 M tokens de entrada: con el tier gratis serían ~3 días y **mezclaría modelos entre problemas** (confunde modelo y configuración); (b) costo estimado ≈ US$0.11 (flash-lite ≈ 0.22; `qwen3.7-flash` descartado: 2 500 tokens de "pensamiento" por llamada trivial); (c) DeepSeek V4 Flash es el más fuerte en matemática de los baratos y responde JSON válido con nuestros esquemas (29 s por llamada con razonamiento). Con el índice completo de Mathlib se repiten sólo `full` y `no_critic` (≈ US$0.06) | prueba de esquemas Planner/Formalizer en 3 modelos; `configs/versions.yaml` | | |
| | 48 | **GPU AMD usada desde Windows con Vulkan (llama.cpp) para embeddings**: `llama-server.exe` (build `win-vulkan`) del lado de Windows sirve el GGUF f16 de Qwen3-Embedding-0.6B por HTTP y WSL lo consume (`RAGSOLVER_EMBED_BACKEND=llama`). Vectores intercambiables con sentence-transformers (cos ≥ 0.9996 en 153 chunks informales y 64 declaraciones de Mathlib); **~48 textos/s vs ~5 en CPU** → corpus completo (278k) en ~96 min en vez de ~15 h. El bug de ROCm en WSL2 (#11-12) no aplica: el driver de Windows expone la GPU por Vulkan sin pasar por `dxg`/ROCm | ROCm en WSL2 (bloqueado); PyTorch-ROCm en Windows nativo (más pesado de instalar) | Confirmado en el log del servidor: `Vulkan0 : AMD Radeon RX 9060 XT (16304 MiB)` con el modelo cargado en la GPU (1.8 GB). Alcance desde WSL vía el gateway (`ip route show default`), servidor en `--host 0.0.0.0`. Tope medido ~48 textos/s (8 slots, lotes de 32-64, 8 hilos) | `tools/llama-vulkan/` (no versionado), `scripts/embed_full_mathlib.sh`, `embed.py::_llama_embed` | | |
| | 49 | **Chunks `paper_section` para el texto expositivo** (`mineru_blocks.section_chunks`): el Markdown de MinerU (ya con las correcciones de Fase 2) se corta por títulos y párrafos en trozos de ≤ 3 000 caracteres con `section_path` y páginas; `ingest_informal --sections front|all` los agrega al mismo `informal.jsonl` (por defecto `none`: el PDF de ejemplo es un problemario, sus soluciones ya van como `problem_card`) | Cortar por tamaño fijo con solapamiento | Plan §6.2 paso 7 pide chunks que respeten la estructura; por tamaño fijo parte demostraciones por la mitad. Sólo el front-matter del PDF (prólogo/índice) aplica hoy, así que queda apagado y probado (`tests/test_mineru_blocks.py`) | `src/ragsolver/rag/mineru_blocks.py`, `ingest_informal.run(sections=...)` | | |
| | 50 | **`eval_v1` con DeepSeek V4 Flash sin cadena de razonamiento** (`reasoning_effort: "off"`, `max_tokens: 4000`) y **6 hilos** en el runner (`--workers 6`; lock en el REPL de Lean y en la escritura de trazas) | Razonamiento `medium`/`low`; corrida secuencial | Medido en el planner de `mathd_algebra_209`: 91 s (medium, 2 932 tokens de salida), 76 s (low), **24 s (off, 707 tokens)**; con effort por defecto la primera llamada del experimento llevaba > 15 min sin respuesta. Secuencial con razonamiento: 300 pares × ~6 llamadas × 80 s ≈ 40 h; con 6 hilos y sin razonar ≈ 2 h. Costo estimado desde los tokens del dev run: ≈ US$0.09 (tope US$0.50). `off` va entre comillas: PyYAML lee `off` sin comillas como `False` | `configs/versions.yaml: llm`, `orchestrator/run.py::run_suite(workers=)`, `scripts/run_eval_v1.sh` | | |
| | 51 | **Bug corregido antes de `eval_v1`: sangría de las tácticas**. `strip_code_fences` hacía `code.strip()` (quitaba la sangría sólo de la primera línea) y `splice_proof` añadía 2 espacios a todas; con tácticas ya indentadas (DeepSeek las devuelve a 2 espacios) el bloque quedaba `have` en col. 2, su cuerpo en col. 6 y el resto en col. 4 → `unsolved goals` en los 3 intentos, y el Crítico diagnosticaba bien ("indentation") sin poder arreglarlo. Ahora ambos hacen `textwrap.dedent` (se quita la sangría común, se conserva la relativa). Los 7 pares de la primera corrida quedaron en `data/eval/runs/_invalid/eval-v1-indent-bug/` y se repitieron. Además el runner arranca el REPL (`import Mathlib`) **antes** de lanzar los hilos, para no cargar ese tiempo al `wall_seconds` de los primeros problemas (los 6 primeros marcaban ~1 750 s; en régimen ~180-240 s) | Dejar que el Crítico lo corrigiera | El Crítico no controla el empalme; era un bug determinista del sistema, no del modelo. En las corridas de desarrollo con Gemini no se manifestó (devolvía las tácticas sin sangría), por eso pasó los tests de aceptación | `tests/test_orchestrator.py::test_splice_proof_keeps_nesting...`, `data/eval/results/eval_v1_indent_bug.log` | | |
| | 52 | **Carga y uso del embedder de torch bajo un lock** (`embed._TORCH_LOCK`) y precalentamiento del embedder y del REPL antes de lanzar los hilos del runner | Confiar en `lru_cache` | Con 6 hilos arrancando a la vez, `lru_cache` no evita cargas concurrentes del modelo: el segundo lanzamiento de `eval_v1` falló en los 60 pares `full` con `expected m1 and m2 to have the same dtype (float != BFloat16)` (modelo a medio convertir a bf16 mientras otro hilo codificaba); el primero sobrevivió por suerte. Codificar en serie no cuesta: torch ya usa todos los núcleos por lote. Pares inválidos en `data/eval/runs/_invalid/eval-v1-dtype-race/` (0 llamadas LLM, sin costo) | `src/ragsolver/rag/embed.py`, `orchestrator/run.py` | | |
| | 53 | **`by` suelto al inicio de las tácticas se descarta** en `strip_code_fences` (también `:= by`). Tercer relanzamiento de `eval_v1` (00:56) para que **todos** los pares corran con el mismo código; los 12 pares `full` previos (5 resueltos, 7 fallos; ≈ US$0.02) quedan en `_invalid/eval-v1-stray-by/` | Dejar que el Crítico lo corrija en el intento 2 | En 8 de 30 intentos DeepSeek empezó el bloque con `by` → `unexpected token 'by'` y un intento perdido por formato, no por matemática; en dos problemas se repitió en los 3 intentos. Mezclar código viejo y nuevo dentro de una configuración confundiría el análisis | `tests/test_orchestrator.py::test_strip_code_fences_and_repeated_statement` | | |
| | 54 | **`eval_v1` detenida a las 03:15 por el tope de US$0.50 de la key de OpenRouter** (`limit_remaining: 0`, `usage: 0.506`): 42 pares `full` válidos (10 resueltos), el resto devolvía `403`. Costo real ≈ **US$0.005 por par `full`** (≈ 100 pares `full` entre válidos e inválidos = US$0.50), 5× la estimación del #47 (los prompts del Generador/Formalizador con 8 lemas + ejemplares pesan 5.3k tokens por llamada, 26k tokens de entrada por par; y OpenRouter enruta a proveedores hasta 2× más caros que el precio base). Para terminar hacen falta ≈ US$0.45 (18 `full` + 60 `no_rag` + 60 `no_critic` + 120 líneas base) y ≈ US$0.40 más para repetir `full`/`no_critic` con el índice completo → subir el tope de la key a ≈ US$1.50 (la cuenta tiene US$3.50 de crédito). Los pares con `error` (403, JSON truncado ×2, carrera en la caché de consultas ×2) se apartaron a `_invalid/eval-v1-errors/` para que el runner los repita al reanudar. Dos bugs más corregidos: reintento único con el doble de `max_tokens` si el JSON llega truncado (`Agents._call`), y `embed_queries` responde desde el dict local en vez de releer la caché FIFO (otro hilo podía desalojar la entrada recién escrita → `KeyError`) | Seguir con la cadena de modelos gratis de Gemini | Mezclaría modelos entre configuraciones (#47) | `data/eval/results/eval_v1.log`, `tests/test_orchestrator.py::test_agent_call_retries_once_on_truncated_json` | | |
| | 55 | **Bug en `build_table` por bloques**: `table.add(batch) or table` — en lancedb ≥ 0.25 `add` devuelve un `AddResult` (truthy) y el segundo bloque llamaba `.add` sobre él. Sólo se manifestaba con más de `chunk` (5 000) filas: la muestra de 3 903 nunca lo tocó. La tabla `mathlib_full` quedó con 10 000 filas y la cadena evaluó contra ella (resultado descartado); se reconstruye completa y se relanza la cadena | — | Test `test_build_table_streams_in_chunks` (chunk=1) | `src/ragsolver/rag/index.py::_write` | | |
| | 56 | **Métricas del retriever sobre el índice completo (278 160 declaraciones)**: MathlibQR (100 consultas, misma semilla que 1b) recall@10 denso 0.41 / BM25 0.25 / híbrido 0.38 / híbrido+rerank **0.45** (vs 0.76 / 0.56 / 0.66 / 0.71 en la tabla de 3 903); premisas gold recall@20 0.010 (informal) / 0.018 (formal) (vs 0.153 / 0.180). Se reportan como la medida realista; los de la muestra quedan etiquetados como inflados por el pool | Reportar sólo la muestra | El orden de las variantes se conserva y el reranker pasa a ser lo que más aporta (+7 puntos sobre el híbrido); premisas por enunciado completo es una cota inferior (unidad real = subobjetivo) | `data/eval/results/fase1_full_mathlibqr_100q.json`, `fase5_premises_full.json`, `docs/eval_report.md` §1c | | |
| | 57 | **Enrutamiento al proveedor más barato en OpenRouter** (`provider_sort: price` → `"provider": {"sort": "price"}`), y **402/403 tratados como cuota agotada** (`DailyQuotaExhausted` → siguiente modelo → parada limpia del runner) | Dejar el enrutamiento por defecto; grabar los 403 como `error` | El precio agregado que muestra OpenRouter para DeepSeek V4 Flash llegó a $0.64/M de salida (4× el base) según el proveedor elegido; la key agotada generó 74 filas `error` en un minuto (#54). GPT Luna, sugerido por el usuario, cuesta $0.20/$1.20 por M (2.5× más por par) — descartado. Reanudado 21-09 21:48 con el tope de la key subido a US$1 | `src/ragsolver/llm.py`, `configs/versions.yaml` | | |
| | 58 | **JSON inválido persistente = `fail` contado, no `error`** (`InvalidLLMOutput` en `Agents._call` tras el reintento; `Orchestrator.solve` lo convierte en `status: fail`, `error_types: llm_output_invalid:<agente>`, con las llamadas y tokens de ese agente) y **las líneas base no pueden tumbar la corrida** (`solve_pair` captura excepciones → fila `error`/`fail`; antes una excepción en `run_baseline` habría abortado `run_suite` y perdido los pares en vuelo) | Dejar `error` con 0 llamadas | `imo_1992_p1` (`full`) generó texto degenerado hasta `max_tokens` dos veces (3 700 s): es un fallo del sistema ante el problema y debe contar con su costo. Relanzado a las 22:56 (51 pares válidos conservados) | `tests/test_orchestrator.py::test_persistently_invalid_json_counts_as_fail_with_calls` | | |
| | 59 | **Causa raíz del sobrecosto: bucle de reintentos oculto**. `LLM._generate_with` reintentaba hasta **20 veces** (tenacity) cuando la respuesta no era JSON válido, y sólo contabilizaba tokens de la respuesta final: cada intento fallido gastaba hasta `max_tokens` (4 000-8 000) de salida **sin aparecer en `tokens_out`**. En problemas donde DeepSeek degenera (IMO: texto repetido hasta el tope), una sola llamada del planner podía costar 20 × 4 000 tokens (+ 20 × 8 000 en el reintento de `_call`): `imo_1992_p1` tardó 3 700 s. Explica que la key gastara US$0.49 en 10 pares tras reanudar (≈ US$0.02 según los tokens contados) y el 5× del #54. Corregido: sin reintento por JSON en el cliente (sólo por errores de transporte/5xx), un único reintento contabilizado en `Agents._call`, y **costo exacto por llamada** desde OpenRouter (`usage: {include: true}` → `usage.cost`, guardado en la traza y sumado en `[cost]` al final de la corrida). Key agotada de nuevo a las 23:06 (US$1.00): quedan 8 `full` + 60 `no_rag` + 60 `no_critic` + 120 líneas base ≈ US$0.10 a precio de lista, y `eval-v1-full` ≈ US$0.16 | Subir `max_tokens`; mantener los reintentos | Los reintentos sobre salida degenerada no la arreglan (misma semilla y temperatura baja) y multiplican el costo | `src/ragsolver/llm.py::_retryable`, `data/eval/results/eval_v1_part1.log` | | |
| | 60 | **Índice ANN en `mathlib_full`**: IVF_SQ (256 particiones, cuantización escalar de 8 bits, métrica L2 sobre vectores normalizados = coseno) con `nprobes 32` y **re-ranking exacto** de los `5 × k` mejores (`refine_factor 5`). Medido con 200 consultas sintéticas (`normalize(e_i + e_j)`): **recall@10 = 0.989 y recall@50 = 0.977 respecto de la búsqueda exacta**, latencia densa p50 **2.71 s → 0.045 s** (p95 2.89 → 0.078 s), construcción 67 s. Los agentes de `eval-v1-full` usan el ANN; las métricas de §1c se midieron con búsqueda exacta y se repiten con ANN al final de la cadena (§1d) | Búsqueda exacta (plana) | La búsqueda plana lee la columna de vectores completa (1.1 GB) por consulta: ~0.8 GB de RSS por búsqueda; con 6 hilos concurrentes excedía la RAM de la máquina (7.5 GB) junto al REPL de Lean y el embedder. Criterio del plan §488: latencia p50 < 12 s en CPU — se cumple de sobra | `scripts/ann_recall.py` → `data/eval/results/ann_mathlib_full.json`; `index.py::build_ann_index` | | |
| | 61 | **JSON inválido persistente se resuelve dentro del grafo** (`Orchestrator._guard` envuelve cada nodo: `InvalidLLMOutput` → `status: fail`, `fail_reason: llm_output_invalid:<agente>`, sumando las llamadas y tokens del agente roto a lo acumulado). Antes la excepción salía de `graph.invoke` y el reporte perdía todo lo anterior: `imo_1965_p1` figuraba con 2 llamadas y 0 lemas cuando había hecho 4 replanificaciones (~12 llamadas). Las 6 filas afectadas (todas `full`, problemas AMC/IMO donde DeepSeek divaga dentro del JSON hasta el tope de tokens) se apartaron a `_invalid/eval-v1-accounting/` y se repiten. Además el runner aparta las trazas de pares interrumpidos a `traces/interrupted/` antes de repetirlos | Estimar lo perdido desde las trazas | Con razonamiento apagado el modelo "piensa" dentro de los campos del JSON en problemas difíciles; es un fallo legítimo del sistema pero su costo debe contarse completo | `tests/test_orchestrator.py::test_invalid_json_mid_run_keeps_accumulated_calls_and_evidence` | | |
| | 62 | **Enrutamiento de OpenRouter por tope de precio + velocidad** (`provider_routing: {max_price: {prompt: 0.10, completion: 0.20}, sort: throughput}`, enviado tal cual en `provider`). Los precios por proveedor de DeepSeek V4 Flash cambiaron desde el #47 (entrada 0.03-0.44, **salida 0.16-1.32 US$/M**); `sort: price` (#57) ordena por precio de *entrada* y eligió OpenInference: $0.03 entrada pero **$0.80/M salida** y 19 tok/s. Medido con `usage.cost`: 66k tokens de salida costaron US$0.054 entre 22:32 y 22:50. Con el tope quedan StreamLake ($0.053/$0.158), DeepInfra ($0.06/$0.18) y Makora ($0.09/$0.195): verificado ~$0.16-0.20/M de salida y 2-4 s por llamada corta. Consecuencia para la validez: los pares se sirvieron por varios proveedores (cuantización fp8 en su mayoría); el modelo es el mismo y se reporta como limitación | `sort: price`; fijar un solo proveedor (`order`, `allow_fallbacks: false`) | Fijar uno solo expone la corrida a su caída (StreamLake: uptime 98 %, latencia p90 44 s) | `configs/versions.yaml: llm.provider_routing`, prueba con `max_price` imposible → 404 (unidades US$/M confirmadas) | | |
| | 63 | **Se repiten los 15 pares `full` afectados por los reintentos ocultos (#59)**. Detectados en las trazas como llamadas cuya latencia no la explica su salida contabilizada (> 180 s y > 0.3 s/token; la mediana de la corrida es 0.033 s/token): p.ej. un formalizador de 812 s que devolvió 10 tokens. 14 de los 15 habían fallado y 1 estaba resuelto (`mathd_algebra_209`); con reintentos ocultos esos pares tuvieron "más oportunidades" que las configuraciones que corrieron después del arreglo, un sesgo a favor de `full` de a lo sumo 1/60 en pass@1 y una subestimación de su costo. Se apartan a `_invalid/eval-v1-hidden-retries/` y la cadena los repite con el código final (≈ US$0.15). Los 39 pares restantes no tuvieron reintentos y se conservan | Conservarlos con una nota; repetir los 54 | Repetir sólo los afectados restaura la consistencia al menor costo; la detección es heurística (un reintento rápido de pocos tokens podría pasar el umbral) y así se reporta | umbral calibrado con la distribución de s/token de 438 llamadas | | |
| | 64 | **Dentro del tope de precio, ordenar por precio** (`sort: price` + `max_price` 0.10/0.20) en vez de por velocidad: para la mezcla real de tokens (≈ 2:1 entrada:salida) StreamLake cuesta 0.26 US$ por "unidad" contra 0.30 DeepInfra y 0.375 Makora (−30 %), a cambio de ~2× más latencia. Medido tras el #62: US$0.046 en 13 pares (0.0035/par, sesgado por 6 problemas IMO que divagan hasta el tope de tokens). Proyección con el cambio: ≈ US$0.45 para lo que falta de `eval_v1` (239 pares) y ≈ US$0.30 para `eval-v1-full`, frente a US$0.29 disponibles → hace falta subir el tope de la key ≈ US$0.5 | Seguir con `sort: throughput` | El presupuesto, no el tiempo, es la restricción activa | `data/eval/results/chain.log` | | |
| | 65 | **Líneas base con un esquema propio y campos obligatorios** (`BaselineAOutput{proof_tactics}`, `BaselineBOutput{natural_language_proof, self_assessed_correct}`) en vez de un `BaselineOutput` común con los tres campos opcionales. Con el común, DeepSeek V4 Flash escribió la prueba en `natural_language_proof` y dejó `proof_tactics` vacío en **58/60** problemas de la línea base A (Lean recibió un cuerpo vacío: `unexpected end of input`), y en la B devolvió a veces una prueba vacía con `self_assessed_correct: true`. En el desarrollo con Gemini no ocurrió (A resolvió 8/13). Las 120 filas se apartaron a `_invalid/eval-v1-baselines-schema/` y se repitieron. La semántica es la misma que la de los agentes principales (campo obligatorio, sin `min_length`): un campo presente pero vacío sigue contando como intento fallido, como en el formalizador (8-13 % de sus llamadas en las tres configuraciones del pipeline, que se reporta como limitación) | Añadir `min_length` y reintento a todos los agentes | Cambiaría el trato de las salidas vacías sólo para las líneas base respecto de las 180 filas ya corridas; se deja como trabajo futuro para todos a la vez | `tests/test_orchestrator.py::test_baseline_schemas_require_the_field_each_baseline_produces` | | |
| | 66 | **`eval_v1` cerrado (300/300 pares, mismo código para todos) y `eval-v1-full` no ejecutado**. Resultado: `full` 9/60, `no_rag` 9/60, `no_critic` 4/60, línea base A 5/60, línea base B se autoevalúa correcta en 55/60 (11 confirmadas por el kernel en alguna configuración); ninguna diferencia pareada sobrevive a Bonferroni. Costo de la cadena final medido por llamada: US$0.314 (eval) + US$0.053 (líneas base repetidas); gasto total de la key en el proyecto ≈ US$1.58 de US$2.00, de los cuales ≈ US$0.9 se perdieron por el defecto de reintentos ocultos (#59) y el proveedor caro (#62). `eval-v1-full` (≈ US$0.30) queda listo en `scripts/run_chain.sh` pero no se corre sin aprobación explícita del usuario, que pidió gastar sólo lo necesario para terminar `eval_v1` | Correrlo con el saldo restante de la key | El usuario autorizó "centavos" para terminar el experimento principal; el extra no lo exige la propuesta | `docs/eval_report.md`, `docs/final_report.md` §6.3 | | |
| | 67 | **PutnamBench fuera del conjunto de evaluación v1** (desviación de la propuesta §1/§8.1, que lo nombra como fuente secundaria junto a miniF2F); el estrato de nivel 3 (IMO) de miniF2F-v2 cumple el papel del nivel más difícil | Portar los enunciados de PutnamBench al pin v4.28.0-rc1 | PutnamBench mantiene sus enunciados contra su propio pin de Lean/Mathlib; portarlos y re-verificarlos contra el pin común del índice y del verificador no cabía en esta iteración, y cada problema adicional cuesta llamadas de LLM en 5 configuraciones. Queda como trabajo futuro (informe §10) | `data/eval/problems/README.md`, `docs/final_report.md` §5 | | |
| | 68 | **ANN validado con consultas reales**: MathlibQR (las mismas 100 consultas de §1c) sobre `mathlib_full` con el índice IVF_SQ da recall@10 denso 0.42 / BM25 0.25 / híbrido 0.38 / híbrido+rerank 0.45 (nDCG@10 0.302 / 0.196 / 0.277 / 0.339) contra 0.41 / 0.25 / 0.38 / 0.45 de la búsqueda exacta: sin costo medible de precisión. La demo sobre el índice completo responde en 0.4-0.5 s por consulta una vez cargado el embedder | — | Confirma #60 con el benchmark real, no sólo con consultas sintéticas | `data/eval/results/fase1_full_ann_mathlibqr_100q.json`, `docs/eval_report.md` §1d | | |
| ## Fase 1 del plan de corrección — endurecimiento del verificador (2026-09-23) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 69 | **Auditoría técnica** (`docs/AUDIT_2026-09-23.md`) encontró SEC-1 y VER-1 (HIGH, confirmados empíricamente): las tácticas del LLM se empalmaban sin validar y el kernel acepta `decide +native` como "verificado" (depende de `Lean.ofReduceBool`, no del kernel — mismo fallo que [15/225 soluciones publicadas de Kimina-Prover](https://github.com/MoonshotAI/Kimina-Prover-Preview/issues/11)). Corregido en `lean/verify.py` (niveles de la [guía oficial de Lean](https://lean-lang.org/doc/reference/latest/ValidatingProofs/)): `find_forbidden_tactic` (nivel 1, antes de tocar Lean) rechaza `#comando` en cualquier posición (no sólo al inicio de línea: `open X in #eval ...` sería un bypass de un chequeo anclado a línea), metaprogramación, declaraciones nuevas, cambios de entorno y `sorry`/`native_decide`/`decide +native`/`ofReduceBool`/`trustCompiler`; `_axiom_check` (nivel 2, tras un veredicto sin errores) exige que `#print axioms <nombre>` sea subconjunto de `propext`/`Classical.choice`/`Quot.sound`. De paso (BUG-2) `theorem_block`/`build_file` pasan a ser la única fuente para el REPL y para el `lean_code` reportado — antes el reporte no incluía los `open` del header del problema | Sandbox de sistema operativo primero | El sandbox (Fase 10 del plan) es más caro y no cierra VER-1 (una prueba con `decide +native` corre igual dentro de un sandbox); la validación de tácticas + auditoría de axiomas cierra ambos hallazgos con ~150 líneas y sin infraestructura nueva | `tests/test_verify.py`; `tests/test_verify_slow.py` (marcador `slow`, Lean real, corrido una vez el 2026-09-23: 4/4 passed en 67 s) | | |
| | 70 | **Exploit confirmado y luego cerrado**: `simp\n#eval IO.println "COMANDO INYECTADO"` empalmado como una prueba normal **ejecutaba** el `#eval` (verificado antes de este arreglo, con Lean real, en el scratchpad de la sesión de auditoría). Tras el arreglo, `find_forbidden_tactic` lo rechaza antes de llegar al REPL (`tests/test_verify_slow.py::test_injected_command_never_reaches_the_repl`) | — | Riesgo real, no teórico: acceso al sistema de archivos con permisos del proceso (incluido `.env` con las claves de API) | `tests/test_verify_slow.py` | | |
| | 71 | **`decide +native` sí lo acepta el kernel** (confirmado con Lean real): `#print axioms` muestra `[Lean.ofReduceBool, Lean.trustCompiler]`. `_axiom_check` lo rechaza aunque el kernel haya aceptado la táctica (`tests/test_verify_slow.py::test_native_decide_is_rejected_by_axiom_audit_if_the_denylist_were_bypassed`, que pasa por alto `find_forbidden_tactic` a propósito para probar la segunda capa de forma aislada) | Confiar sólo en la lista de rechazo de tácticas | Defensa en profundidad: un nombre de axioma nuevo que la lista no conozca (o una forma de invocarlo que la lista no cubra) lo sigue atrapando la auditoría de axiomas, que no depende de reconocer sintaxis | `tests/test_verify_slow.py` | | |
| | 72 | **Re-verificación offline de `eval-v1` con el verificador nuevo** (`scripts/reverify_eval_v1.py`, 0 llamadas LLM: sólo REPL, ~70 s): de las 27 filas `done`, reconstruidas con el `header` del problema (que el `lean_code` original no incluía, BUG-2) — **27/27 siguen `ok=true`, 0 regresiones, todos los axiomas dentro de {propext, Classical.choice, Quot.sound}**. Sobre las 203 filas con `lean_code` extraíble de las 300 totales, 20 quedan marcadas por el denylist nuevo — las 20 contienen `sorry` o `native_decide` **literales** (el LLM se rindió o probó un atajo) y ya estaban en filas `status=fail`; el regex viejo (`_FORBIDDEN`) ya las rechazaba por los mismos tokens, así que no son casos nuevos | Repetir `eval-v1` con el código nuevo | El verificador más estricto no cambia ningún resultado ya reportado (mismos 9/9/4/5/pass@1); no hace falta gastar de nuevo la key ni recalcular `docs/eval_report.md`/`docs/final_report.md` | `data/eval/runs/eval-v1/reverify.json` (no versionado en git, igual que el resto de `data/eval/runs/`) | | |
| | 73 | **`Problem.imports` rechazado explícitamente** (`Orchestrator.solve`, `run_baseline`) en vez de ignorado en silencio (auditoría BUG-4): ningún conjunto de problemas de este proyecto lo usa hoy (`data/eval/problems/*.jsonl` verificado); si algún día un problema lo trae, falla con un motivo claro en vez de verificarse contra un archivo al que le faltan imports que el Autoformalizador pudo haber asumido | Seguir concatenándolo en silencio (comportamiento anterior) | Fallar alto y explícito es más barato que depurar un "unknown identifier" que en realidad era un import faltante | `tests/test_orchestrator.py::test_problem_imports_are_rejected_explicitly_instead_of_ignored`, `::test_run_baseline_rejects_problem_imports_too` | | |
| | 74 | **Test de arranque índice ↔ Lean** (`PLAN_RAG.md §12`, pendiente desde la Fase 1 original): `run_suite` muestrea 20 `decl_name` al azar (semilla 13) de la tabla que sirve `mathlib` y los comprueba con `#check` contra el Lean real del verificador (`LeanChecker.check_identifiers_exist`); sólo avisa en el log, no aborta la corrida | Abortar si algún nombre no existe | Un nombre fantasma es una señal de desalineación índice/Mathlib, no necesariamente un error fatal; el `#check` es aproximado (cuenta errores totales, no mapea 1 a 1) | `tests/test_orchestrator.py::test_check_index_consistency_reports_without_running_lean` | | |
| ## Fase 2 del plan de corrección — licencias y datos, antes del primer commit (2026-09-24) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 75 | **`.gitignore` ampliado** (SEC-2): `problemas resueltos olimpiadas.pdf`, `blobs/` (caché de MinerU con el texto completo del PDF), `data/eval/runs/` (354 citas informales con hasta 300 caracteres del PDF en `snippet`), `lean_project/scratch/`, `lean_project.lock`, `.claude/*.graphify-bak`. Nada se borró del disco — sólo se excluye de git. `Propuesta_Proyecto_Final_IA_UPTC.pdf` (del usuario, sin problema de licencia) queda pendiente de que el usuario confirme si quiere versionarla | Borrar los archivos | Borrar es una acción destructiva; ignorarlos en git ya cierra el riesgo de licencia y es reversible | `.gitignore` | | |
| | 76 | **Fuga nueva encontrada al verificar (no estaba en la auditoría del 23-09): `docs/qa_ingest_edd3436ca148.md`** embebía 16 fragmentos del PDF (fórmulas del gate de QA, transcripciones de huecos, descripciones VLM de figuras — estas últimas también las trata como no publicables `publish_hf.py`, que ya excluye las tablas `informal`/`figures` completas cuando `redistributable=false`). `scripts/qa_ingest_report.py` ahora redacta todo contenido derivado del texto del documento cuando `redistributable=false` (conteos y páginas, sin texto literal); con el documento actual, el reporte pasa de 16 coincidencias a 0 | Excluir el archivo entero de git | El reporte de QA es un entregable citado en `final_report.md` Apéndice D; redactar mantiene el valor documental (gates, conteos, páginas) sin el texto protegido | `scripts/check_no_leaked_content.py`, corrida antes/después sobre `docs/qa_ingest_edd3436ca148.md` | | |
| | 77 | **Comprobación por contenido reutilizable** (`scripts/check_no_leaked_content.py`: huellas de 60 caracteres del `content_md` del corpus informal, buscadas en los archivos que `git add .` incluiría) — **0 coincidencias** sobre el repo completo tras las decisiones #75/#76 (antes: 1 archivo, 16 coincidencias). La misma función la usa `scripts/publish_hf.py`: cuando el documento informal no es redistribuible, `data/eval/runs/<run_id>/` se sube desde una copia temporal saneada (`Citation.snippet` vaciado sólo en citas `corpus=informal`; verificado que las trazas no llevan texto de fuente, sólo ids/scores/consultas — 0 coincidencias en 1443 archivos de trazas de `eval-v1`) y el publish (incluido `--dry-run`) aborta si la comprobación encuentra algo | Confiar sólo en `redistributable` para decidir qué carpetas subir (como ya hacía el script con `informal.lance`/`figures.lance`) | `results.jsonl` no estaba cubierto por ese gate: se sube siempre, sin condicionarlo a `redistributable` — el vector de fuga real que encontró la auditoría (SEC-2) | `tests/test_check_no_leaked_content.py`; `python scripts/publish_hf.py --repo test/x --dry-run` → `"content_check": "OK"` | | |
| ## Fase 3 del plan de corrección — dependencias y documentación de estado (2026-09-24) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 78 | **Dependencias core reducidas** (auditoría DEP-1): `httpx` y `tenacity` pasan a declararse (se usaban directo en `llm.py`/`embed.py` sin estar en `pyproject.toml`, sólo llegaban como transitivas de `langchain-core`/`huggingface-hub`). Nuevo extra `demo = ["gradio"]` (antes sólo llegaba vía `mineru`/`ingest`; con `uv sync` a secas `python demo/app.py` fallaba). `ranx` pasa a `dev` (sólo lo usa `tests/test_metrics.py`). **`pandas` y `accelerate` se quitan de las dependencias core**: 0 referencias directas en `src`/`scripts`/`demo` (grep verificado); confirmado sin ellos instalados que `tests/test_retriever_integration.py` carga el embedder Y el reranker reales (`rerank=True`, cross-encoder) en verde — `accelerate` sólo lo exige `transformers` para cargar GGUF (`transformers/modeling_utils.py`: `"accelerate is required when loading a GGUF file"`), camino que este proyecto no usa vía `transformers` (el GGUF se sirve por `llama-server` externo). Siguen instalados como transitivas en los entornos que ya los necesitan (`pandas` vía `ranx`/`gradio`, `accelerate` no resultó necesario ni siquiera con el extra `ingest` en esta resolución de `mineru`) | Declarar `pandas`/`accelerate` "por si acaso" | Menos superficie de instalación; el entorno con `--extra dev --extra ingest --extra demo` sigue siendo idéntico en la práctica (`uv sync` los trae igual por las extras que sí los necesitan) | `pyproject.toml`; suite completa (128 tests, incluidos los 2 de integración real y los 4 con Lean real) en verde tras `uv lock && uv sync --extra dev --extra ingest --extra demo` | | |
| | 79 | **Desvíos de `PLAN_RAG.md` no registrados hasta ahora** (auditoría §3.A), documentados aquí en vez de en el código: (a) la recuperación informal (`mode=exemplar`) sólo ocurre en `PLAN` (ejemplares para el Planificador); `RETRIEVE` no vuelve a consultar el corpus informal por subobjetivo como sugiere el plan §6.2 Fase 4 tarea 3 — el diseño implementado es "ejemplares una vez al principio", no "ejemplares por subobjetivo"; (b) `math_area` no actúa como *boost* en la búsqueda (plan Fase 4 tarea 4): el campo se guarda y se puede filtrar con `RetrievalFilters.math_area`, pero nada del orquestador lo usa para reordenar resultados; (c) el índice FTS usa una columna derivada `_fts_text` (concatenación de los campos buscables) en vez de una columna `name_tokens` tokenizada por `.`/`_` como sugería el plan §6.1 tarea 5 — LanceDB ≥0.25 sólo indexa FTS de un campo por índice, así que se concatena en vez de mantener dos índices; (d) los embeddings se guardan en float32 en el JSONL intermedio (`data/corpus/*_embedded.jsonl`), no float16 como dice el esquema §7.2 — sólo la columna `embedding` de la tabla LanceDB final importa para el tamaño del índice, y ésa sí es la que se sirve; (e) no existe una tabla `documents` real ni el modelo `Document` se usa para nada — `build_document_record` en `ingest_informal.py` arma un dict con la misma forma pero sin validar contra el modelo Pydantic (relacionado con ING-1, pendiente en Fase 5 del plan de corrección) | Cambiar el código para que coincida con el plan | Cada desvío tiene una razón técnica real (limitaciones de LanceDB, foco del proyecto en Fase 4/5) y ninguno afecta RNF-3 (0 llamadas del retriever) ni la validez de `eval_v1`; registrar es más barato que revertir sin evidencia de que haga falta | `src/ragsolver/orchestrator/graph.py` (retrieve/plan), `src/ragsolver/rag/index.py` (`_FTS_COLUMN`), `src/ragsolver/rag/embed_corpus.py`, `src/ragsolver/rag/ingest_informal.py::build_document_record` | | |
| ## Fase 4 del plan de corrección — citas, trazas y contabilidad (2026-09-24) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 80 | **IDs de cita estables por chunk** (BUG-1): `_assign_citation_ids` (nuevo, `orchestrator/graph.py`) mantiene `state["citation_ids"]: {chunk_id: citation_id}` durante todo el problema; `plan()`/`retrieve()` reutilizan el id existente de un chunk que reaparece (tras un replan o una re-recuperación) y sólo asignan el siguiente número libre del prefijo a los chunks nuevos, en vez de renumerar `M1..Mn`/`I1..In` desde cero en cada llamada. `eval_v1` no se recalcula (las trazas del retriever de esa corrida no se pueden atribuir a un par (config, problema), TRZ-1 no estaba corregido todavía cuando corrió) | Recalcular `used_in_sketch` de `eval_v1` desde las trazas existentes | Sin TRZ-1 corregido en esa corrida, no hay forma de reconstruir con certeza qué búsqueda perteneció a qué intento; se documenta como limitación en vez de fabricar un recálculo dudoso | `tests/test_orchestrator.py::test_citation_ids_stay_stable_and_unique_when_a_chunk_reappears` | | |
| | 81 | **Trazas del retriever agrupadas por corrida** (TRZ-1): `RetrievalContext` gana `run_id`; el orquestador pasa `state["run_id"]` (ya el pid completo `<run>:<config>:<problema>`) en cada `RetrievalRequest.context`; `Retriever.search()` usa ese `run_id` como nombre del archivo de traza en vez de fabricar uno con un uuid aleatorio por búsqueda. Sin `run_id` (CLI, demo, `evaluate.py`) se mantiene el comportamiento anterior (un archivo por búsqueda) — no tienen un "run" real que agrupar. Comprobado que los nombres de archivo con `:` (el formato del pid) ya funcionan en este filesystem: los 300 archivos de eventos de agentes de `eval_v1` ya los usaban sin problema, así que no hizo falta el cambio `:` -> `__` que preveía el plan | Cambiar `:` por `__` en los nombres de archivo | Innecesario: verificado que `/mnt/c` (este WSL2) ya acepta `:` en nombres de archivo — los propios eventos de agentes de `eval_v1` lo prueban | `tests/test_retriever.py::test_run_id_in_context_groups_searches_into_one_trace_file` | | |
| | 82 | **Pin de la tabla `mathlib` servida** (MET-1): `VersionsPin` gana `mathlib_table`; `Retriever.search()` y `Agents._call()` (nuevo `Agents._versions_pin()`, evita duplicar la construcción) lo llenan desde la misma fuente de verdad (`RAGSOLVER_MATHLIB_TABLE`, extraída a `retrieve.mathlib_table_name()`) | Dejarlo sólo en el log de `run_suite` | Así cada evento de traza (no sólo el arranque de la corrida) dice contra qué tabla se buscó — relevante si `RAGSOLVER_MATHLIB_TABLE` cambia a mitad de sesión de trabajo | `tests/test_retriever.py::test_trace_event_pins_the_mathlib_table_being_served` | | |
| | 83 | **Política de reintentos del cliente LLM** (LLM-1): `httpx.ReadTimeout` deja de reintentarse (el proveedor puede haber generado — y cobrado — la respuesta antes del corte; reintentar la duplicaría), tope bajado de 20 a 4 intentos, y cada `LLMResponse` lleva `attempts` (se registra en la traza). `timeout_s` por defecto baja de 600/900 a 300 s (`configs/versions.yaml: llm.timeout_s`); 4000 tokens de salida al proveedor más lento medido son ~210 s (#62), 300 s deja margen. El bucle de reintentos pasa de `@retry` (decorador estático, sin contar intentos de forma segura entre hilos) a `tenacity.Retrying` construido dentro de la llamada (estado local, sin carrera con `--workers`) | Seguir con el decorador `@retry` y `stop_after_attempt(20)` | 20 intentos por llamada era el origen del sobrecosto de `eval_v1` (#59, ya mitigado ahí de otra forma); acotar a 4 y no reintentar timeouts de lectura cierra la misma clase de riesgo de forma más general | `tests/test_llm.py::test_retryable_excludes_read_timeout_but_allows_connect_timeout_and_5xx`, `::test_generate_with_counts_attempts_and_retries_on_503`, `::test_generate_with_does_not_retry_a_read_timeout` | | |
| | 84 | **Contabilidad sin pérdidas ante cualquier excepción** (ACC-1): `Orchestrator._guard` pasa a capturar cualquier `Exception` (no sólo `InvalidLLMOutput`), devolviendo `status: "error"` sin salir del grafo — el estado que LangGraph ya fusionó (llamadas, tokens, evidencia, historial de los nodos previos) se conserva, a diferencia del `except Exception` que tenía `solve()` antes, que reemplazaba todo el estado final por `{**init, fail_reason}` (perdía todo). Las aristas condicionales de `plan`/`retrieve`/`generate`/`formalize`/`verify` se actualizan para tratar `status="error"` igual que `"fail"` (cortan a `END`) en vez de intentar seguir con estado a medio llenar. El evento `invalid_json` de `Agents._call` ahora lleva `llm_calls`/`tokens`/`cost_usd` reales (antes `self.trace()` los ponía siempre en 0). Las filas `fail`/`error` de líneas base llevan `models=[<modelo>]` cuando la excepción es `InvalidLLMOutput` (antes quedaban con `models=[]`) | Seguir descartando el estado en un solo `except` en `solve()` | Capturar a nivel de nodo es más barato que reconstruir el estado perdido, y usa la fusión de estado que LangGraph ya hace gratis | `tests/test_orchestrator.py::test_generic_exception_mid_run_becomes_status_error_and_keeps_accumulated_state`, `::test_baseline_invalid_json_row_records_which_model_produced_it` | | |
| | 85 | **Self-consistency: `cost_usd` y `attempts` de las n muestras se suman** (BUG-5): `type(resp)(text, model, tokens_in, tokens_out, calls=n)` sólo pasaba esos 4 posicionales + `calls`, dejando `cost_usd`/`attempts` de la respuesta agregada en sus valores por defecto (0.0 / 1) aunque las muestras hubiesen costado más o reintentado. Apagado por defecto (`generator_samples: 1`); no afecta a `eval_v1` | — | Encontrado al revisar el mismo constructor para agregar el nuevo campo `attempts` | `tests/test_orchestrator.py::test_self_consistency_sums_cost_and_attempts_across_samples` | | |
| | 86 | **Lock en la caché de embeddings de consulta** (BUG-6): `_QUERY_CACHE` se leía/escribía desde varios hilos (`--workers N`) sin lock; la evicción FIFO (`_QUERY_CACHE.pop(next(iter(_QUERY_CACHE)))`) podía correr a la vez que otro hilo insertaba, con riesgo de `RuntimeError: dictionary changed size during iteration`. Nuevo `_QUERY_CACHE_LOCK` (separado de `_TORCH_LOCK`, que sólo protege la codificación real: así el backend `llama`, que no usa torch, no se serializa de más) | Un solo lock compartido con `_TORCH_LOCK` | Serializaría también las lecturas de caché del backend `llama`, que no tocan torch | `tests/test_embed.py::test_embed_queries_is_safe_under_concurrent_access` (400 llamadas concurrentes, 8 hilos, caché de sólo 8 entradas para forzar evicciones) | | |
| | 87 | **`Problem.imports` no soportado en `run_baseline` tampoco** (BUG-4, continuación de #73 en Fase 1): mismo guard que `Orchestrator.solve` | — | Consistencia: el camino de líneas base no pasaba por el guard de la Fase 1 | `tests/test_orchestrator.py::test_run_baseline_rejects_problem_imports_too` | | |
| | 88 | **Suite completa verificada tras la Fase 4** (138 tests: 132 rápidos + 2 de integración real contra el índice + 4 con Lean real, incluida la re-verificación de axiomas de la Fase 1) — ningún cambio de esta fase afecta los resultados ya reportados de `eval_v1` (no se re-ejecutó nada con LLM real; el smoke opcional de 5 problemas triviales, D6 del plan, queda pendiente de aprobación de gasto) | Correr el smoke sin preguntar (costo ≈ US$0.001) | El plan exige aprobación explícita para cualquier llamada de pago, sin excepción de monto | `uv run pytest -q` (138 passed) | | |
| ## Plan de mejora del RAG — recuperar para el goal de Lean (2026-10-14, `docs/PLAN_MEJORA_RAG.md`) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 89 | **El input del retrieval de Mathlib es el goal de Lean**, no el enunciado ni el subobjetivo en lenguaje natural. El Autoformalizador escribe un esqueleto `have … := by sorry`; `LeanChecker.goals` lo elabora en modo sonda (nunca produce `ok`) y devuelve un goal + `proof_state` por `sorry` (`lean-interact`: `CommandResponse.sorries`). El NL queda sólo para los ejemplares del corpus informal (NL↔NL, recall@5 1.0) | Reescribir mejor las consultas NL; consultar el índice con el enunciado formal | Recall@8 de premisas gold en el índice completo: NL 0.012, formal 0.000; las premisas de competición son herramientas genéricas cuyo enunciado no se parece al problema (§2.2-§3 del plan) | `data/eval/results/probe_close/names.json`; `PLAN_MEJORA_RAG.md` §2.2 | | |
| | 90 | **Canal A — prior de competición**: frecuencia de cada lema en 29 750 pruebas de `Goedel-LM/Lean-workbook-proofs` (MIT, revisión `b731852a`), validado contra el **entorno de Lean** con `LeanChecker.signatures` (`#check`, uno por nombre) y no contra el JSONL de LeanSearch — entran los lemas de núcleo (`Nat.mul_mod`, `Nat.mod_eq_of_lt`, `eq_comm`). Se sirve filtrado por los símbolos del goal (`rag/premises.py::prior_channel`: un lema anclado a `√`/`%`/`ℕ` sólo si el goal los tiene). 778 lemas | Tabla LanceDB `premise_prior` (el plan la proponía) | Un JSONL de 778 filas en memoria basta; no hace falta re-embeber nada | `data/corpus/premise_prior.jsonl` + `.meta.json`; `scripts/build_premise_prior.py` | | |
| | 91 | **`signatures` reintenta sin los `open`**: con `open … Nat`, `#check @sq_nonneg` es ambiguo (`sq_nonneg` vs `Nat.sq_nonneg`) y falla, aunque la prueba lo desambigua por tipo. Sin el reintento el prior perdía su lema más frecuente (16 719 usos) | — | Bug encontrado al inspeccionar la primera versión del prior (766 lemas sin `sq_nonneg`; 778 con el arreglo) | `tests/test_verify_slow.py::test_signatures_resolves_core_lemmas_and_ambiguous_root_names` | | |
| | 92 | **Canal B (selección nativa de Lean) se consume dentro de CLOSE** (`grind +suggestions`), no como lista de nombres para la fusión | Fusionar la salida de la táctica `suggestions` con A y C | Para `0 ≤ (a - b) ^ 2` la táctica `suggestions` devuelve `not_fermat_42`, `BitVec.pow_add`… con score plano 0.88; en cambio `grind +suggestions` cerró 9/60 problemas de `eval_v1` frente a 7/60 de `grind` (§2.3) | `data/eval/results/dev_probe/explore.log` | | |
| | 93 | **Batería CLOSE configurable** (`configs/agents.yaml: close.tactics`, la primera que cierra gana): `grind +lax [premisas]` → `nlinarith [hints]` → `grind +suggestions` → `try?`. Cada táctica pasa por `find_forbidden_tactic` y, después, el sistema la envuelve en `set_option maxHeartbeats 200000 in` (sólo tácticas de una línea de la batería; el LLM sigue sin poder usar `set_option`). `try?` se reemplaza por su script concreto ("Try this: [apply] …"): el kernel nunca acepta una táctica con `?` | Timeout de reloj solo | Un timeout de `ProofStep` reinicia el REPL (~5 min de `import Mathlib`) e invalida los `proof_state` de los demás hilos | `orchestrator/graph.py::close_goal`, `concrete_block`; `LeanChecker.step` | | |
| | 94 | **Registro de goals probados (`proved`) y FILL sólo sobre los abiertos**: un reintento o un replan no vuelve a demostrar un goal ya cerrado (se reutiliza su bloque si sigue cerrando en el nuevo contexto); el Autoformalizador recibe los `have` ya probados; FILL (1 llamada) recibe cada goal abierto con su contexto exacto, sus premisas con firma y el último error de la automatización | Regenerar la prueba entera en cada intento (eval-v1) | En eval-v1, 55 de 99 intentos fallidos con ≥ 2 `have` tenían al menos uno completo antes del primer error y se descartó igual (§2.5) | `tests/test_goal_mode.py::test_fill_only_sees_open_goals_and_ledger_reuses_proved_ones_on_retry` | | |
| | 95 | **Replan con memoria (DES-1)**: el prompt del Planificador recibe `{feedback}` = razón del Crítico + estrategia anterior + goals ya probados; `prompt_version: "2"`. Sustituye a `PLAN_CORRECCIONES.md` 7.1 | `max_replans: 0` | 65 replans con 2 éxitos en eval-v1 (similitud 0.13 con el plan anterior: remuestreo a ciegas) | `tests/test_goal_mode.py::test_replan_carries_critic_feedback_and_proved_claims` | | |
| | 96 | **Ablaciones redefinidas** (R3): `no_rag` = misma batería CLOSE **sin** premisas recuperadas, hints ni `grind +suggestions` (sí `grind`, `nlinarith`, `try?`); `rag_nl` = el flujo de eval-v1 (`goal_mode: false`); `baseline_auto` = 0 LLM, sólo la batería sin premisas sobre el goal raíz (`run.py::run_auto`) | `no_rag` sin automatización | Si `no_rag` no tuviera la batería, la comparación mezclaría el aporte del RAG con el de la automatización de Lean, que sola ya cierra 12/60 | `configs/agents.yaml: configs` | | |
| | 97 | **Filtro de ingesta de Mathlib por regex** `\.eq_(\d+\|def)$` en vez de la subcadena `.eq_`, que también borraba lemas reales como `Nat.Prime.eq_one_or_self_of_dvd`. No cambia el índice actual: la fuente (LeanSearch v2) ya no trae ningún nombre con `.eq_` | — | §2.4 del plan | `tests/test_ingest_mathlib.py::test_equation_lemmas_are_excluded_but_real_eq_lemmas_are_kept` | | |
| | 98 | **Canal C (índice denso con el goal como consulta) apagado por defecto** (`close.dense_channel: false`; plan B de la Fase 3). Por goal (1 295 goals del gold nuevo, muestra de 600 para C): recall@16 **A filtrado 0.327**, A estático 0.261, C 0.028, fusión A+C 0.097 — la RRF intercala los hits densos (irrelevantes) con los del prior y desplaza a los buenos. Por problema, con el gold contra Lean: consulta NL de eval-v1 0.006, formal 0.004, prior 0.229. La Fase 5 (selector LeanHammer) no se dispara: A ≥ 0.30 por goal. La tarea 4b (núcleo en el índice denso) queda sin objeto mientras C esté apagado | Mantener la fusión A+C del plan | Medido; la fusión resta 23 puntos | `data/eval/results/premises_goals.json`; `scripts/eval_premises_goals.py` | | |
| | 99 | **Gold de premisas por goal** (`data/eval/premises_gold_goals.jsonl`, 1 295 goals de 230 problemas, 218 lemas distintos; por problema: `premises_gold_lean.jsonl`): por cada táctica sin bloque `by` anidado de las pruebas verificadas de DeepSeek-Prover-V2, el goal antes de la táctica y las constantes que la táctica **nombra** y que el término **usa** (`used_constants` del REPL ∩ identificadores del texto, con resolución de `open`). Las pruebas que no compilan del todo en el Mathlib fijado (183/438) aportan las tácticas que no contienen la posición de un error. `premises_gold.jsonl` (filtrado contra el JSONL de LeanSearch, sin núcleo) se conserva para reproducir las cifras publicadas | `used_constants` a secas; descartar las pruebas con errores; filtrar tácticas con salto de línea | `used_constants` a secas mete plomería de `norm_num`/`simp`/`decide` (`congrFun'` 2 463 veces, `Nat.decLt`, `of_decide_eq_false`); descartar las pruebas con error perdía el 42 %; DeepSeek parte `nlinarith [sq_nonneg …]` en varias líneas | `scripts/build_premise_gold_goals.py` (+ `.raw.jsonl` para re-filtrar sin re-elaborar) | | |
| | 100 | **Batería CLOSE medida sobre el goal raíz de `eval_v1`** (`scripts/probe_close.py`, 0 LLM): sin premisas 11/60, con premisas (prior + hints + `grind +suggestions`) 12/60; `grind` cierra 6 → 8 con premisas; `algebra_sqineq_2unitcircatblt1` sólo con RAG (`nlinarith [sq_nonneg (a - b), …]`). Dos ajustes de infraestructura salieron de esta medición: `close.timeout_s` 60 → 150 (`try?` llega a 76 s; cada sub-táctica parece tener su propio presupuesto de heartbeats) y **generación del REPL en cada `proof_state`** (`LeanChecker._gen`): tras un reinicio, `step` rechaza el estado viejo sin enviarlo — antes el número podía apuntar a otro goal del REPL nuevo, y la primera corrida perdió 12 filas por reinicios en cascada (se re-midieron sólo esas; la primera corrida queda en `battery.first_run.jsonl`) | Timeout de 60 s | Medido | `data/eval/results/probe_close/battery.jsonl`; `tests/test_verify_slow.py::test_stale_proof_state_is_rejected_after_a_restart` | | |
| | 101 | **`used_in_proof` con las constantes del término aceptado** (`Verdict.used_constants`, REPL `all_tactics=True` en `LeanChecker.check`), no con el texto de la prueba: `grind +lax [16 lemas]` nombra 16 y puede no usar ninguno. En el segundo smoke el criterio textual daba 16/19 en tres problemas; re-verificado con el REPL: **0 premisas recuperadas en los términos** de las 8 pruebas — `grind` cerró esos pasos sin ellas. Sin REPL (backend `lake`) queda el criterio textual | Criterio textual (`identifiers_used`) | Una cita "usada" debe ser una que el kernel usó | `data/eval/results/smoke_used_constants.json`; `tests/test_goal_mode.py::test_used_in_proof_counts_only_constants_the_kernel_term_uses`, `tests/test_verify_slow.py::test_check_reports_the_constants_the_accepted_term_uses` | | |
| | 102 | **El sistema parte la prueba del modelo en goals** (`lean/verify.py::skeletonize`, `prompt_version: "3"`): cada `have … := by <bloque>` de nivel superior pasa a `sorry` y su bloque es el primer candidato de ese goal; lo que sigue al último `have` es el candidato del cierre. CLOSE prueba primero el bloque del modelo, después la batería; FILL ve el intento rechazado. Además, `strip_code_fences` baja al nivel de la primera línea el resto del bloque cuando el modelo entrega la primera sin sangría y la primera no abre un bloque | Exigir el esqueleto con `sorry` en el prompt | Primer smoke (8 problemas, prompt 2): el modelo escribió pruebas completas sin `sorry` en todos los casos — CLOSE nunca recibía goals — y en `triv_dvd_trans` la sangría rota (`unexpected identifier`) tumbó los 3 intentos. Segundo smoke (prompt 3): **8/8** (antes 5/8), 35 llamadas, US$0.0047 | `data/eval/runs/smoke-goal-mode`, `smoke-goal-mode-v3`; `tests/test_goal_mode.py::test_full_proof_without_sorry_is_split_into_goals_and_only_the_bad_step_is_repaired`, `test_skeletonize_shapes`; `tests/test_orchestrator.py::test_strip_code_fences_and_repeated_statement` | | |
| | 103 | **Hallazgo operativo (OpenRouter, 2026-10-14)**: con `provider_routing.max_price {prompt: 0.10, completion: 0.20}` hoy sólo califica **DeepInfra** ($0.06/$0.18; StreamLake, el de eval-v1, subió a $0.132/$0.396), y DeepInfra responde 429 `engine_overloaded` de forma intermitente: el primer smoke perdió 7/8 pares por 429 con 4 hilos. Con 1-2 hilos y los reintentos existentes (4, backoff exponencial) completó. Antes de la evaluación grande hay que decidir: esperar, correr con pocos hilos, o subir el tope (con `completion ≤ 0.30` califican ~8 proveedores) | — | Medido (`GET /models/…/endpoints`, respuesta 429 con `provider_error_code: engine_overloaded`) | `data/eval/results/smoke_goal_mode.log` | | |
| | 104 | **Un 429 se reintenta hasta 10 veces** (espera exponencial con tope de 90 s); 5xx y errores de transporte siguen en 4 (LLM-1) y un timeout de lectura sigue sin reintentarse. Un 429 es una solicitud rechazada que no se cobra; con 4 intentos (≈ 28 s) los 429 de DeepInfra saturado dejaban pares en `error` tras haber gastado 2-3 llamadas | Volver a subir todos los reintentos | Lo que se cobra (timeouts de lectura, JSON inválido) sigue sin reintentos ocultos | `tests/test_llm.py::test_429_gets_more_retries_than_billable_errors` | | |
| | 105 | **eval-v2 en 4 rondas de 15 problemas con guardia de costo** (a pedido del usuario): rondas intercaladas (`eval_v1_round{k}.jsonl` = problemas k, k+4, …; 6 álgebra / 6 teoría de números / 3 combinatoria cada una), configs `full`, `no_rag`, `rag_nl`, `baseline_auto`, `--workers 2`, tope de precio sin cambios (decisión del usuario). Tras cada ronda `round_check.py` mide el gasto real de la key y corta la cadena ante cualquier alerta (> 20 llamadas en un par, > US$0.006 por par, > 10 % de errores, `baseline_auto` con LLM, proyección > saldo). `rag_nl` corre sobre `mathlib_full` (eval-v1 usó la muestra sembrada con el gold, MET-1) | Una sola corrida de 60 | Detectar un bug de costo después de 15 problemas, no de 60 | `data/eval/results/eval_v2_chain.sh`, `round_check.py`, `eval_v2_round{k}_check.json` | | |
| | 106 | **`rag_nl` aislada de eval-v2** (decisión del usuario): la evaluación corre `full`, `no_rag` y `baseline_auto`. Prioridad: probar el sistema nuevo y medir el aporte del RAG nuevo (`full` vs `no_rag`) y de los agentes (`full` vs `baseline_auto`) dentro del saldo de la key; `rag_nl` (el flujo de eval-v1) era la configuración más cara (~35 % del gasto). Al detenerla no había ningún par de `rag_nl` terminado. Queda definida en `configs/agents.yaml` y se puede correr después sobre el mismo `run_id` si sobra saldo | Correr las cuatro configuraciones | Costo y tiempo; la comparación con el sistema viejo queda contra eval-v1 (confundida por proveedor, índice y prompts — se reporta así) | `data/eval/results/eval_v2_chain_b.sh` | | |
| | 107 | **Tope de precio de OpenRouter subido a {prompt: 0.13, completion: 0.30}** (8 proveedores) **y 4 hilos** para eval-v2, a pedido del usuario (tiempo estimado 13-16 h → 2-4 h). Costo real medido del flujo nuevo antes del cambio: US$0.0016 por par (10 300 tokens de entrada / 5 600 de salida; eval-v1 gastaba 25 500 / 9 700). Peor caso para los 113 pares restantes ≈ US$0.34 < saldo US$0.385; `sort: price` usa primero DeepInfra y sólo pasa a los demás cuando rechaza. `full` y `no_rag` corren con la misma mezcla de proveedores (algunos sirven el modelo con más cuantización): la comparación entre ellas no se sesga, pero se anota como limitación | Mantener {0.10, 0.20} (sólo DeepInfra, saturado: 46 rechazos 429 y 16 min de espera en 7 pares) | Tiempo; el costo esperado casi no cambia y el guardia entre rondas sigue activo | `configs/versions.yaml: llm.provider_routing`; `data/eval/results/eval_v2_chain_c.sh` | | |
| | 108 | **Guardia de costo detuvo eval-v2 tras la ronda 1** (proyección US$0.343 > saldo US$0.289). Ronda 1 (15 problemas): `full` 6, `no_rag` 7, `baseline_auto` 5 (eval-v1 en los mismos: `full` 3, `no_rag` 4). `baseline_auto` completada en los 60 a US$0 (key deshabilitada por variable de entorno): **11/60**, más que eval-v1 `full` (9/60). El kernel usó 1 premisa recuperada en 1 problema. Costo real por par US$0.0036 (`full`) / US$0.0032 (`no_rag`), igual que eval-v1 en tokens. No se relanza nada pagado sin decisión del usuario (opciones A-D en `docs/eval_report_v2.md` §6) | Continuar sin preguntar | El guardia existe para esto; la proyección supera el saldo | `docs/eval_report_v2.md`; `data/eval/results/eval_v2_round1_check.json`, `eval_v2_stats.json` | | |
| | 109 | **Opción B del usuario**: eval-v2 continúa sólo con las rondas 2 y 3 (`full`, `no_rag`; `baseline_auto` ya completa), con el saldo actual (US$0.2887) y la misma configuración (tope {0.13, 0.30}, 4 hilos). Resultado final: 45 problemas pareados. El guardia proyecta ahora hasta la ronda 3 (`round_check.py <k> 3`) | A (subir el límite de la key), C, D | Decisión del usuario | `data/eval/results/eval_v2_chain_d.sh` | | |
| | 110 | **Salidas ilegibles del Crítico y de FILL ya no matan el problema**: Crítico inválido → reintento con los errores de Lean como feedback; FILL inválido → sin propuestas (goals abiertos); las llamadas se contabilizan igual | Dejar `llm_output_invalid` como fallo | 12 de 43 fallos de eval-v2 (rondas 1-2) eran `llm_output_invalid` (Crítico 6, FILL 4) | `tests/test_goal_mode.py::test_unreadable_critic_output_retries_instead_of_failing_the_problem` | | |
| | 111 | **Detener la ronda 3 de eval-v2 y rediseñar** (consejo aceptado por el usuario): 11 de 15 pares `full` hechos (2 resueltos), `no_rag` sin empezar; completarla (~US$0.06) no cambiaba la conclusión (dificultad 2-3: 0 resueltos en ambas) | Completar las rondas | El cuello de botella medido es el demostrador, no el RAG; el saldo sirve más para probar el rediseño | `docs/eval_report_v2.md` | | |
| | 112 | **Demostrador especializado local**: Goedel-Prover-V2-8B (Apache-2.0; 84.6 % pass@32 miniF2F según su paper), GGUF `mradermacher/Goedel-Prover-V2-8B-GGUF` revisión `43fab68e`, **Q6_K**, `llama-server` Vulkan en la RX 9060 XT (8 slots × 12 288, KV q8_0, flash attention), escuchando sólo en 172.31.112.1:8082. Etapa 0 sobre el teorema completo (k_root 8 en lotes de 4 con salida temprana, una ronda de autocorrección) y lemas autónomos para los goals abiertos en CLOSE (k_goal 2, max_goals 2). Del modelo se toman SÓLO las tácticas del teorema pedido, empalmadas en el enunciado del problema | DeepSeek-Prover-V2-7B / Kimina-Prover-8B; servicio remoto (no hay demostradores en OpenRouter, verificado) | Mejor número publicado entre los 7-8B; costo de API 0; Q6_K = Q8 en calidad práctica y más rápido (45 vs 36 tok/s; la GPU satura en ~70 tok/s agregados en ambos) | `docs/PLAN_REDISENO_PROVER.md`; `data/eval/results/dev_probe/bench_prover.py`; `tests/test_prover.py` | | |
| | 113 | **Rol del RAG en el rediseño: corregir nombres** — el demostrador se entrenó con Lean 4.9/Mathlib de entonces; ante `unknown identifier/constant`, el RAG (índice, modo `error`) propone declaraciones parecidas validadas con `#check` en el Mathlib fijado, que van a la autocorrección y se citan (`M*`, `used_in_proof` por el término) | Seguir pasando premisas genéricas al prompt | eval-v2: el kernel no usaba las premisas genéricas; el cambio de versión de Mathlib es una necesidad real y medible del demostrador | `tests/test_prover.py::test_correction_round_gets_rag_name_suggestions_and_cites_them` | | |
| | 114 | **Caché en disco de las muestras del demostrador** (`data/eval/prover_cache.jsonl`, clave = mensajes + semilla + parámetros): `baseline_prover` y la etapa 0 de `full` comparten las mismas muestras (pareadas, generadas una vez); `SolutionReport` registra `solved_by` y `prover_samples` | Semillas distintas por configuración | El tiempo de GPU es el recurso escaso (~70 tok/s) | `tests/test_prover.py::test_disk_cache_reuses_identical_samples` | | |
| | 115 | **Resultado del rediseño (eval-v3, 30 problemas pareados con eval-v2)**: `full` v3 **21/30** (dificultad 1/2/3: 13/16 · 6/10 · 2/4) frente a v2 `full` 8/30 (0/14 en dificultad 2-3); McNemar 13 a 0, **p = 0.0002**. Los 21 los resolvió la etapa 0 (demostrador local, 0 API); los agentes por API (US$0.06) y el RAG (2 correcciones con 8 nombres sugeridos, 0 usados por el kernel) no añadieron ningún problema: `full` = `baseline_prover`. Pruebas revisadas: enunciado intacto, axiomas estándar. Servidor de la GPU apagado al terminar. Limitación principal: posible contaminación (miniF2F es el benchmark público del demostrador) | — | Medido | `docs/eval_report_v3.md`; `data/eval/results/eval_v3_analysis.json` | | |
| ## eval-v4 — respaldo por API apagado, DRAFT, más muestras y problemas fuera de miniF2F (2026-10-18) | |
| | # | Decisión | Alternativa descartada | Motivo | Evidencia | | |
| |---|---|---|---|---| | |
| | 116 | **Respaldo por API apagado por defecto** (`prover.api_fallback: false`): si el demostrador no resuelve (etapa 0, o DRAFT si está activo), el problema termina sin correr el bucle PLAN → … → CRITIQUE. Sin demostrador (servidor caído o `enabled: false`) los agentes corren siempre (`Orchestrator._api_loop`) | Dejarlo encendido | eval-v3: el bucle corrió en los 9 problemas que el demostrador no resolvió, costó US$0.061 y no resolvió ninguno (#115) | `tests/test_prover.py::test_api_fallback_off_ends_after_stage0_without_api_calls` | | |
| | 117 | **DRAFT (Draft-Sketch-Prove, `prover.dsp`)**: si la etapa 0 falla, el modelo de la API escribe plan (Planificador) y prueba informal (Generador), 2 llamadas, y el demostrador local las recibe **dentro de su prompt** (`prover.draft_hint`) junto con `k_lemmas` = 10 lemas del canal A del RAG para el goal raíz, con su firma; `k_draft` = 8 muestras (8 de la etapa 0 + 8 con borrador = 16, comparadas con las primeras 16 de `baseline_prover32`, que comparten semillas y caché). Los lemas mostrados se citan (`M*`) y `used_in_proof` mide si el término los usó. El borrador usa la sección `llm_draft` de `versions.yaml`: el mismo DeepSeek V4 Flash **con razonamiento** (`reasoning_effort: high`, 16 000 tokens) | El modelo general como demostrador (eval-v1/v2) o como respaldo (eval-v3); pasar el plan sólo al bucle de agentes | El modelo general razona bien en lenguaje natural y mal en Lean 4.28; el demostrador sabe Lean. Es la forma en que la API y el RAG pueden influir en la prueba que se acepta. El razonamiento se había apagado por latencia (#50), que aquí no importa frente a minutos de GPU por muestra. Smoke real (1 problema, etapa 0 apagada): resuelto por DRAFT con 2 llamadas, US$0.0005 | `tests/test_prover.py::test_draft_puts_plan_sketch_and_rag_lemmas_in_the_prover_prompt`; `orchestrator/graph.py::draft` | | |
| | 118 | **Servidor del demostrador con 6 slots × 16 384 tokens** (`scripts/prover_server.sh`; eval-v3 usó 8 × 12 288). El prompt de DRAFT crece 1-2 k tokens y no cabía junto a los 11 500 de salida. La caché de muestras no depende del servidor (clave = mensajes + semilla + `max_tokens` + temperatura): las muestras de eval-v3 se reutilizan | Bajar `max_tokens` del borrador | El demostrador necesita las salidas largas en los problemas difíciles; la GPU satura en ~70 tok/s agregados con 6 u 8 slots | `tools/logs/prover_server.log`: `n_slots = 6, n_ctx_slot = 16384` | | |
| | 119 | **Conjunto fuera de miniF2F** (`data/eval/problems/pdf_set.jsonl`, 32 problemas de álgebra, teoría de números y combinatoria del PDF de olimpiadas; sin geometría): enunciados informales parafraseados en inglés y formalizados por el asistente; respuestas comprobadas por fuerza bruta antes de formalizar; los 32 elaboran en el Lean fijado con un único goal. **No se versiona** (deriva del PDF de licencia desconocida, SEC-2). Con DRAFT se corre sin ejemplares informales (`dsp8: corpora: [mathlib]`): el corpus informal ES ese PDF y traería la solución del propio problema. Limitaciones: formalización no revisada por un humano; las versiones informales de estos problemas clásicos pueden estar en internet (no hay formalización en Lean conocida). Hallazgo lateral: `LeanChecker.goals` como **primera** llamada de un REPL nuevo falla como si Mathlib no estuviera importado (`unknown namespace BigOperators`); el runner y la demo siempre hacen antes un `check` de calentamiento, así que no afecta, pero queda anotado | PutnamBench (pin de Mathlib distinto, #67); problemas de 2025-2026 escritos de memoria (riesgo de enunciados mal recordados) | Medir si el 21/30 de eval-v3 depende de que miniF2F sea el benchmark público del demostrador | scratchpad `build_pdf_set.py` / `check_pdf_set.py` de la sesión; `errores: 0` | | |
| | 120 | **Demo con el sistema actual**: `demo/app.py` conecta el demostrador (`prover.local_prover`, compartido con el runner), usa un REPL de Lean para toda la sesión (antes abría y cerraba uno por problema: ~5 min de `import Mathlib` cada vez), lo precalienta con el embedder en segundo plano al arrancar, acepta la cabecera `open …` y trae 4 problemas de eval-v3 como ejemplos (sus muestras están en caché: salen en segundos, la verificación corre en vivo). `scripts/demo_up.sh` levanta todo con un comando | Dejar la demo con el flujo de agentes | La demo corría el sistema de 8/30, no el de 21/30 | `demo/app.py`, `scripts/demo_up.sh`, `scripts/prover_server.sh` | | |
| | 121 | **Las sobreescrituras de `configs` se fusionan con los dicts de base** (`effective_config`): `baseline_prover32: {prover: {k_root: 32}}` conserva el resto de `prover`. Antes un dict reemplazaba al de base entero | Repetir todas las claves en cada configuración | Configs de control (`baseline_prover16/32`, `dsp8`) de una línea | `tests/test_prover.py::test_nested_config_overrides_merge_with_base` | | |
| | 122 | **Servidor caído = la corrida se detiene, no "fallos"** (`prover.ProverUnavailable`): si todas las muestras de un lote fallan y el servidor no responde a `/health`, `LocalProver.sample` lanza la excepción; el grafo y `Orchestrator.solve` la dejan pasar (como `DailyQuotaExhausted`) y `run_suite` corta con `status: prover_down`, sin escribir el par en curso, que se repite al reanudar. Además `run_suite` se niega a empezar si alguna configuración pedida necesita el demostrador (`baseline_prover*`, `dsp*`) y no responde, y la cadena de eval-v4 relanza el servidor antes de cada paso. Una muestra suelta que falla se sigue tolerando | Dejar que las configuraciones degraden en silencio (comportamiento anterior) | Primera noche de eval-v4: el servidor murió a las 22:27 (el log se corta en plena generación, causa desconocida) y la cadena siguió: 22 problemas del PDF quedaron como fallos con 0 muestras reales, `baseline_prover32`/`16` como "unsupported" y `dsp`/`dsp8` corrieron el bucle de agentes por API (US$0.023). Todo eso se apartó a `data/eval/runs/_invalid/eval-v4-prover-down/`; se conservan los 9 pares de `pdf_set × baseline_prover` terminados antes de la caída (6 resueltos) | `tests/test_prover.py::test_dead_server_stops_the_run_instead_of_recording_false_failures` | | |
| | 123 | **Más muestras no rinden en los difíciles: 32 → 16 en el control.** `pdf_set × baseline_prover` terminó 21/32 (66 %; miniF2F 21/30): el demostrador generaliza fuera de su benchmark. En los 9 que eval-v3 no resolvió, 32 muestras resolvieron 0/6 a ~65 min por problema, y en `imo_1978_p5` 13 de 32 muestras no dejaron prueba extraíble (cortadas por largo). Se dejó de correr `baseline_prover32` en los 3 que faltaban; el control de DRAFT/REPAIR es `baseline_prover16` (las 8 primeras muestras y la corrección salen de la caché) | Terminar las 32 en los 9 (~3 h más de GPU) | La pregunta es si los agentes/RAG suman sobre el demostrador, no cuánto más da el mismo muestreo; 0/6 ya lo contesta | `data/eval/runs/eval-v4-pdf/`, `data/eval/runs/eval-v4-unsolved/` | | |
| | 124 | **Descomposición por API con razonamiento: descartada.** Config `decomp` (el bucle de agentes con `llm_draft`, DeepSeek V4 Flash con razonamiento alto, y el demostrador en cada paso `have`): en 2 de los difíciles el Formalizador gastó todo su presupuesto de salida razonando (16 000 + 32 000 tokens en el reintento) y devolvió JSON vacío, y el Generador con razonamiento escribió como `sketch` un comentario sobre los ids de lemas en vez de la prueba. US$0.030 por 2 problemas, 0 resueltos. Se quitó la config; el prompt del Generador ahora exige la prueba entera en `sketch` (afecta también a DRAFT) | Bajar el esfuerzo de razonamiento o un modelo más caro (V4 Pro, ~20× el precio) | Gasto sin señal; la variante sin API (#125) ataca lo mismo gratis | `data/eval/runs/smoke-decomp/` | | |
| | 125 | **REPAIR (0 API, `prover.repair`)**: si la etapa 0 falla, las casi-pruebas del demostrador (≤ 2 errores de Lean: Lean sigue elaborando tras un `have` que falla, así que ≈ ≤ 2 pasos malos), con menos errores primero, se parten en sus `have` (`skeletonize`, como la prueba del Formalizador) y pasan por CLOSE: su propio bloque primero, la batería y el demostrador sobre el lema corto (`max_goals` 4 × `k_goal` 4) en los pasos que fallan; hasta 2 candidatas cuyo esqueleto elabora. Lo probado queda en `proved`. Config `repair`, en la cadena de eval-v4 junto a `dsp` y `baseline_prover16`. Sin el filtro de ≤ 2 errores tardaba ~20 min por paso en pruebas con muchos pasos malos (prueba apartada en `_invalid/smoke-repair-sin-filtro`) | Reparar con un LLM por API (#124) | Receta de DeepSeek-Prover-V2 / Goedel-Prover-V2 (cada subobjetivo como lema) sobre la estructura que el demostrador ya escribió, sin costo de API. Primeros resultados en los difíciles: cierra pasos (4 de 12) pero no el central | `tests/test_prover.py::test_repair_closes_the_bad_step_of_a_failed_prover_proof_with_zero_api` | | |
| | 126 | **Los difíciles: 2 de 9 son falsos y SKETCH para el resto.** (a) `mathd_algebra_437` (`1/3` es división de naturales = 0: n = −1 cumple las hipótesis y la tesis dice −4) y `numbertheory_notequiv2i2jasqbsqdiv8` (a = 1, b = 0: `¬(Even a ∧ Even b)` es verdadero y 8 ∤ 1) son falsos tal como están en miniF2F-v2s: nadie puede demostrarlos. Sus negaciones están en `data/eval/results/false_statements/FalseStatements.lean` (se comprueban con Lean entre los pasos de la cadena: un segundo REPL no cabe en la RAM). Los 7 restantes, en `eval_v3_unsolved_provable.jsonl`. (b) **SKETCH** (`prover.sketch` rondas, config `sketch` = REPAIR + SKETCH): el modelo con razonamiento (`llm_draft`) escribe el esqueleto en Lean con pasos `have … := by sorry` en TEXTO (la versión JSON de #124 se quedaba sin presupuesto); CLOSE cierra cada paso con la batería y el demostrador sobre el lema corto; si quedan pasos abiertos o el esqueleto no elabora, la 2.ª ronda recibe los goals abiertos o el error de Lean. Es la receta de DeepSeek-Prover-V2 y el flujo de la propuesta (Planificador → subobjetivos → prueba de cada uno → Lean, RF-1). La cadena de eval-v4 se reordenó para correr primero `sketch`, `dsp` y el control sobre los 7 difíciles demostrables (~3 h) y después los 11 fallos de `pdf_set` (~5 h); la config `sketch` incluye REPAIR (`solved_by` distingue la etapa), así que `repair` ya no corre aparte | Un demostrador local más grande (Goedel-Prover-V2-32B: no cabe en 16 GB salvo en Q3, ~3× más lento, poca ganancia reportada); un modelo frontera escribiendo Lean entero (US$0.3-1 por problema con razonamiento, fuera del saldo) | El demostrador falla en el paso central de los difíciles; partirlo en lemas cortos es lo que funciona en los sistemas de referencia, y la parte por API es una llamada por ronda (~US$0.005) | `tests/test_prover.py::test_sketch_skeleton_steps_are_closed_by_automation_and_the_prover` | | |
| | 127 | **RAG de pruebas** (`rag/proofs.py`, config `rag_proofs`, etapa EXAMPLES del nodo `prove`): el RAG de lemas no le aporta al demostrador (los lemas genéricos ya los conoce; en eval-v3 sugirió 8 nombres y el kernel no usó ninguno); lo que le falta es cómo se resolvió un problema parecido. Si la etapa 0 falla, se recuperan por embeddings (Qwen3-Embedding, el modelo del resto del RAG; consulta = enunciado informal + Lean) las pruebas verificadas de los 3 problemas más parecidos de Lean Workbook (29 750, MIT; 0 coinciden con miniF2F-v2s/v2c), que (1) se prueban tal cual sobre el enunciado (0 GPU) y (2) van como ejemplos en el prompt del demostrador (8 muestras: 8 + 8 como DRAFT, control `baseline_prover16`). Se excluyen los vecinos con similitud ≥ 0.95 (el mismo problema: la prueba sería la respuesta); la traza guarda los ids y similitudes recuperados, y `proofs_preview.txt` (en `runs/`, gitignored) lo que recupera cada problema, para revisar a mano si trae el mismo problema con otra formalización. Así el eje de embeddings de la propuesta vuelve a tener un papel medible (`solved_by = prover_examples` vs control). El índice se construye en la GPU con el demostrador apagado (`eval_v4_part2.sh`, que toma el relevo de la cadena al terminar los 7 difíciles y corre `rag_proofs` antes que el resto de `pdf_set`) | Lemas por paso en el prompt del demostrador (idea más barata, pero DRAFT ya mide si los lemas en el prompt sirven); embeber ya, en paralelo (Windows tiene ~1 GB de RAM libre y la VRAM la ocupa el demostrador) | TacticToe/Tactician (k-NN sobre pruebas) y Rango (recuperar pruebas además de premisas) muestran que lo que más transfiere son pruebas de problemas parecidos | `tests/test_prover.py::test_rag_proofs_tries_retrieved_proofs_then_puts_them_in_the_prover_prompt`, `::test_proof_corpus_parsing_strips_comments_and_keeps_statement` | | |
| | 128 | **Por qué SKETCH casi no funcionó en eval-v4, y su arreglo.** (a) **32 de 38 llamadas del Sketcher se cortaron** en los 16 000 tokens de `llm_draft`: el modelo razonaba hasta el tope y no llegaba a escribir el bloque Lean; aun así resolvió `amc12b_2002_p3` (el control de 16 muestras no). SKETCH tiene ahora su propio tope (`sketch_max_tokens: 40000`, ~US$0.007 por llamada; `llm_draft.timeout_s` 900 → 1800); DRAFT no cambia (1 de 15 llamadas cortadas) para no romper su comparación. (b) **Pasos "stale" sin probar**: un bloque de varias líneas (el del demostrador o del modelo para un paso) no llevaba límite de heartbeats, agotaba el timeout de reloj y reiniciaba el REPL (~5 min); con 3 hilos sobre un REPL, todos los pasos siguientes de todos los hilos quedaban "stale proof state" sin probarse (en `numbertheory_aneqprodakp4…` 9 de 11 pasos; los esqueletos eran correctos: invariante aₙ₊₁ = (aₙ − 2)², período 12 en `amc12a_2002_p21`). Ahora `limit_heartbeats` envuelve los bloques en `·` bajo `set_option maxHeartbeats` y CLOSE re-elabora el esqueleto (proof_state nuevos) y reintenta el paso una vez si el REPL se reinició (`_close_one`) | Volver a 1 hilo (evita que un reinicio afecte a otros, pero no el reinicio, y triplica el tiempo) | Los dos defectos explican casi todos los fallos de SKETCH en los difíciles; repetirlo con los arreglos es lo de mayor valor esperado por hora de GPU | `tests/test_prover.py::test_close_reelaborates_the_skeleton_when_the_repl_restarts_instead_of_leaving_steps_unproved`, `::test_heartbeat_limit_covers_multiline_blocks`, `tests/test_goal_mode_slow.py::test_multiline_step_runs_under_the_heartbeat_limit` (con Lean real, al relanzar) | | |
| | 129 | **Publicación en Hugging Face: código como repositorio de modelo, no como Space.** Crear el Space de Gradio devolvió HTTP 402: desde 2026 alojar Gradio o Docker en `cpu-basic` pide PRO. `publish_space.py --model` sube lo mismo como repositorio de modelo (gratis): app, paquete, configs, docs, notebook, tests, scripts, `pyproject.toml` + `uv.lock`, LICENSE y CITATION, con la comprobación de texto del PDF de `publish_hf.py` (antes el Space no la tenía). La demo se corre en local (`scripts/demo_up.sh`); sin `--model`, el mismo script publica el Space cuando haya PRO. El dataset suma el índice `proofs`, la caché del demostrador y las corridas eval-v2/v3/v4 (sin `eval-v4-pdf` ni `pdf_set.jsonl`: salen del PDF, SEC-2); el saneado de citas cubre ahora también `errors.jsonl` (eval-v2 tenía 6 `snippet` del PDF). Además, la comprobación con Lean previa a eval-v4b abortó la primera vez por un error del **test**, no del envoltorio: suponía 3 goals en un esqueleto con 2 `sorry` | Pagar PRO (US$9/mes); un Space estático sin demo | La propuesta pide el repositorio en HF; la demo interactiva necesita CPU con Python, que ya no es gratis | huggingface.co/CamiloS1111/ragsolver, huggingface.co/datasets/CamiloS1111/ragsolver-index; `data/eval/results/eval_v4b_test_bug.log` | | |
| | 130 | **Resultado de eval-v4b: SKETCH, con los arreglos de #128, es la primera configuración que supera al demostrador solo fuera de miniF2F** (`pdf_set` 22/32 frente a 21/32; gana `pdf_p41` por SKETCH y `pdf_p33` por REPAIR, pierde `pdf_p58`; McNemar 2-1, p = 1.0). En los difíciles se queda en 1/7 (`amc12b_2002_p3`, ahora por REPAIR). Llamadas cortadas: 32/38 → 12/34; 0 pasos "stale". El control `baseline_prover16` resuelve los mismos 21 que `baseline_prover`, así que `pdf_p33` en `dsp8` es mérito del borrador. Las dos pruebas nuevas: axiomas estándar y solo citas de Mathlib. US$0.33 entre eval-v4 y eval-v4b | — | — | `data/eval/runs/eval-v4b-{pdf,unsolved}/`, `data/eval/results/eval_v4_analysis.json`, `docs/eval_report_v4.md` | | |