# Enriquecer el RAG a nivel de pentesting profesional

El RAG del naranja ya funcionaba, pero se dejaba fuera lo mejor que hay en el
disco. Medido antes de empezar: **de las 72 notas del `OSCP_Vault` —la
metodologia OSCP escrita a mano, con comandos reales— había 0 en el índice.**
`indexa.py` las descartaba por dos motivos a la vez: el nombre (`SQL
injection.md` no lleva "notas" ni "guia") y la profundidad (viven a 5 niveles).

Este pipeline las rescata, multiplica su recuperabilidad con preguntas
generadas, y **mide** que el resultado es mejor antes de tocar nada.

## Correrlo

```bash
./correr_noche.sh              # las cuatro fases, y decide si promociona
./correr_noche.sh --sin-promo  # igual, pero deja el vivo intacto
```

Es reanudable: cada fase se salta si ya está hecha, así que si se corta,
relanzarlo continúa. La fase larga (síntesis) es reanudable fragmento a
fragmento.

## Las cuatro fases

| | Script | Qué hace | Coste |
|---|---|---|---|
| 1 | `cosecha.py` | rescata la metodología de COFRE que faltaba | segundos, CPU |
| 2 | `sintetiza.py` | preguntas en castellano por cada fragmento | **horas**, modelo local |
| 3 | `reindexa.py` | une todo y calcula vectores → `saber.jsonl.nuevo` | minutos, CPU |
| 4 | `evalua.py` | mide recuperación vivo vs candidato | segundos |

### 1 · Cosecha

Donde `indexa.py` es prudente (solo ficheros que *parecen* documentación, 3
niveles), esto es agresivo con el oro: las carpetas de metodología (OSCP vaults,
apuntes, cheatsheets) enteras, sin límite de profundidad. Fuera de ahí pone un
**tope por herramienta**: si un repo tiene más de 25 documentos, es una base de
datos (exploitdb son 29.925 ficheros), no unos apuntes, y solo se coge su
README. Así el oro no se ahoga en ruido.

### 2 · Síntesis — indexación multi-representación

El punto flaco de los embeddings estáticos: "cómo saco una shell reversa" no se
parece vectorialmente a `bash -i >& /dev/tcp/...`. La solución es **embeber la
pregunta y devolver el fragmento**. Por cada fragmento, el modelo local escribe
las preguntas que responde; se indexa la pregunta, pero lo que se le muestra al
cerebro es el fragmento **literal**.

Clave: el modelo **solo escribe preguntas, nunca reescribe comandos**. Si
inventa una pregunta rara, esa entrada recupera peor y ya está; no corrompe una
respuesta. Y el fragmento crudo sigue en el índice por su lado.

### 3 · Reindexado

Une el índice vivo (comandos Linux, glosario, fichas), la cosecha y la síntesis;
deduplica por prefijo; calcula los vectores con el mismo `model2vec` de siempre.
Escribe a `saber.jsonl.nuevo`. **No toca el vivo.** `busca.py` no cambia.

### 4 · Evaluación

`eval_set.jsonl` son ~45 preguntas de pentesting con las palabras que un
fragmento correcto debe contener. Mide `hit@1`, `hit@5` y similitud media, del
índice vivo contra el candidato. Determinista y reproducible, sin juez-LLM.

## La decisión, y por qué es segura

El índice vivo **solo se sustituye si la evaluación dice que el candidato es
mejor**, y antes se guarda `saber.jsonl.antes-<fecha>`. Si empeora, se queda el
vivo y el candidato espera revisión. Siempre reversible con un `cp`.

## Ficheros que genera (en `../../datos/`)

    cosecha.jsonl        la metodología rescatada, sin vectores
    sintesis.jsonl       las preguntas generadas (+ .hecho, el marcador)
    saber.jsonl.nuevo    el índice candidato, con vectores
    informe_rag.txt      los números de la evaluación
    noche_rag.log        el registro de la noche
    saber.jsonl.antes-*  copia del índice anterior, si se promocionó

## Iteraciones posteriores a la primera noche

- **Troceado que no tira comandos cortos** (`cosecha.py`). El troceado por
  encabezado descartaba toda sección de menos de 60 caracteres, y eso perdía
  el oro: `## Detección time-based` + `' AND SLEEP(5)-- -` son 50. Ahora una
  sección corta se pega a la siguiente y cada fragmento lleva delante el título
  de la nota. `SLEEP` pasó de 0 a 13 apariciones en el índice; hit@1 80→82 %.
  El salto en la métrica es pequeño porque las preguntas que quedan son
  cross-lingual, y de eso no salva el troceado.

- **`experimento_embedder.py`** — mide, sin tocar nada, si un transformer (e5,
  bge) supera al model2vec estático. **Resultado (16 ago): NO compensa.**
  `intfloat/multilingual-e5-base` empató con el estático (hit@1 86 %, hit@5
  95 % los dos): arregla las 2 preguntas que el estático fallaba, pero rompe
  otras 2 distintas. A cambio pediría ~50-150 ms por consulta en CPU y una
  dependencia de torch en cada búsqueda. Medido antes de cambiar → **el
  estático se queda.** El trabajo de recuperación lo hace la búsqueda híbrida
  (coseno + bono por palabras), no el embedder, y por eso subir el embedder no
  mueve la aguja. Reproducible: `./experimento_embedder.py`.

- **Re-síntesis limpia** sobre el troceado nuevo (`SABER_BASE` en `reindexa.py`
  reconstruye desde el índice original en vez de apilar, para no inflar). Da a
  los comandos rescatados (SLEEP y demás) su gancho-pregunta en castellano, que
  es lo que de verdad ataca las consultas cross-lingual —no el embedder—.

## Nota sobre la tarjeta

La síntesis usa el modelo local por `127.0.0.1:11434`. Fija `qwen3.5:4b-jarvis`
(el naranja, que no inventa comandos). Si dejas el TUI verde abierto, ollama
tiene que cambiar de modelo en cada petición y la noche va más lenta: para la
síntesis más rápida, cierra el verde.
