---
name: memoria-busqueda
description: >
  Búsqueda progresiva (Progressive Disclosure) sobre la memoria del sistema SWL:
  aprendizajes en APRENDIZAJES.md, sesiones en sessions/*.json e instintos en
  instintos/proyecto.yaml. Cargar cuando se necesite recuperar trabajo pasado,
  buscar decisiones anteriores, entender qué se hizo en sesiones previas, o
  evitar repetir errores ya documentados.
version: "1.1.0"
herramientasPermitidas: [Read]
exclusiones:
  - "No cargar para búsquedas triviales en el codebase (nombre de función, ruta de archivo); `Grep` directo es más barato y no requiere el índice de sesiones."
  - "No cargar si la información ya está en el contexto activo de la sesión — no hacer una búsqueda de memoria para algo que se dijo hace 2 turnos."
  - "No cargar para escribir o actualizar aprendizajes; este skill es de solo lectura. Para escribir, usar `extractor-de-aprendizajes`."
  - "No cargar cuando el proyecto no tiene historial (primer día) — no hay sesiones en `.planning/sessions/` ni entradas en APRENDIZAJES.md que buscar."
evolvable: true  # default para skill estandar
---
# Memoria Búsqueda — Progressive Disclosure Search

## Cuándo cargar esta skill

- Antes de empezar una tarea: verificar si hay aprendizajes relevantes previos.
- Cuando el usuario pregunta "¿ya resolvimos esto antes?" o "¿qué hicimos la semana pasada?".
- Al iniciar una sesión nueva en un proyecto con historial acumulado.
- Para recuperar el contexto de una sesión anterior sin cargar todo a memoria.
- Cuando se detecta un error potencialmente ya visto y documentado.

---

## El problema que resuelve

Cargar APRENDIZAJES.md completo o todas las sesiones en contexto es costoso:
- APRENDIZAJES.md puede tener 200+ entradas = 50,000+ tokens.
- 50 sesiones x 1,000 tokens promedio = 50,000 tokens adicionales.
- Total sin progressive disclosure: ~100,000 tokens para una búsqueda.

Con las 3 capas, el costo típico es:
- 20 índices (Capa 1): ~2,000 tokens.
- 5 timelines (Capa 2): ~2,500 tokens.
- 2 fetch completos (Capa 3): ~2,000 tokens.
- **Total: ~6,500 tokens. Ahorro: ~93%.**

---

## Fusión de fuentes con RRF (desde v1.2.0)

`search()` combina las 3 fuentes (aprendizajes, sesiones, instintos) usando
**Reciprocal Rank Fusion** (`scripts/lib/rrf-fusion.js`). En lugar de
sumar relevancias heterogéneas (que no son comparables — la "relevancia 0.7"
de un aprendizaje no significa lo mismo que en una sesión), RRF combina
basándose en la posición (rank) que cada documento ocupa en su propia lista.

Fórmula: `score(d) = Σ_i  w_i / (k + rank_i(d))` con `k=60` y pesos
empíricos `[aprendizajes 0.4, sesiones 0.4, instintos 0.2]`.

Propiedades:
- Documentos que aparecen en múltiples streams reciben boost natural.
- Documentos que aparecen en un solo stream NO son penalizados.
- Robust ante magnitudes distintas de score por fuente.

Backward compatible: el retorno conserva el campo `relevancia` (calculado
por la fuente). Se agrega `combinedScore` para que el caller pueda razonar
sobre el ranking RRF si lo desea. Si `rrf-fusion.js` no está disponible,
`search()` cae al merge por relevancia simple legado.

---

## Las 3 capas de búsqueda

### Capa 1 — search(): índices compactos

**Cuándo usar**: Siempre primero. Retorna solo id, tipo, título, fecha y relevancia.

```js
const { search } = require('./hooks/lib/memory-search');

// Busqueda basica
const resultados = search(baseDir, "error build classpath");
// → [
//     { id: "apr-5",               tipo: "aprendizaje", titulo: "Fix classpath Maven en CI", fecha: "2026-03-10", relevancia: 0.9 },
//     { id: "ses-20260310-142000", tipo: "sesion",      titulo: "Investigacion build Java",  fecha: "2026-03-10", relevancia: 0.6 },
//   ]

// Con filtros
const soloAprendizajes = search(baseDir, "autenticación JWT", {
  tipo:        'aprendizaje',
  fecha_desde: '2026-01-01',
  limit:       10,
});
```

**Filtros disponibles**:
- `tipo`: `'aprendizaje'` | `'sesion'` | `'instinto'` (sin filtro = todas las fuentes)
- `fecha_desde`: string YYYY-MM-DD — excluye resultados anteriores
- `fecha_hasta`: string YYYY-MM-DD — excluye resultados posteriores
- `limit`: número máximo de resultados (default: 20)

**Costo por resultado**: ~50-100 tokens. Seguro pedir hasta 20 resultados.

---

### Capa 2 — timeline(): contexto cronológico

**Cuándo usar**: Después de search(), para los 3-7 resultados más relevantes.
Agrega contexto resumido sin traer el contenido completo.

```js
const { timeline } = require('./hooks/lib/memory-search');

// Tomar los 5 ids mas relevantes de search()
const ids  = resultados.slice(0, 5).map(r => r.id);
const ctx  = timeline(baseDir, ids);
// → [
//     { id: "apr-5", tipo: "aprendizaje", titulo: "...", contenido: "<primeros 500 chars>", fecha: "2026-03-10" },
//     { id: "ses-...", tipo: "sesion", contenido: "Modo: dev | Tool calls: 47 | Archivos: build.gradle | ...", fecha: "2026-03-10" },
//   ]
// Ordenado descendente por fecha (más reciente primero).
```

**Costo por entrada**: ~200-500 tokens. Seguro para 5-7 entradas simultáneas.

---

### Capa 3 — fetch(): detalle completo

**Cuándo usar**: Solo para los 1-3 resultados definitivamente relevantes.
Retorna el contenido completo sin truncar.

```js
const { fetch } = require('./hooks/lib/memory-search');

// Solo para el resultado más relevante
const detalle = fetch(baseDir, "apr-5");
// → { id: "apr-5", tipo: "aprendizaje", contenido: "<bloque completo del APRENDIZAJES.md>" }

const sesionCompleta = fetch(baseDir, "ses-20260310-142000");
// → { id: "ses-...", tipo: "sesion", contenido: { ...objeto JSON completo... } }
```

**Costo**: ~500-1,000 tokens por resultado. NO hacer fetch de todos los resultados.

---

## Fuentes de datos y formato de IDs

| Fuente | Formato de ID | Ejemplo |
|--------|--------------|---------|
| APRENDIZAJES.md | `apr-{N}` donde N es el índice del bloque H2 | `apr-0`, `apr-42` |
| sessions/*.json | `ses-YYYYMMDD-HHmmss` (del campo `id` del JSON) | `ses-20260325-143000` |
| instintos/proyecto.yaml | el valor del campo `id:` en el YAML | `inst-selectinload-async` |

Los IDs de sesiones son estables (basados en timestamp). Los IDs de aprendizajes
(`apr-N`) son posicionales: si se reordena APRENDIZAJES.md, los índices cambian.
Siempre obtenerlos desde search(), no hardcodearlos.

---

## Protocolo de busqueda recomendado

```
1. search(query, { limit: 20 })
   → Leer títulos y relevancias. Identificar los 3-7 más prometedores.

2. timeline(ids_seleccionados)
   → Leer el contenido resumido. Confirmar cuáles son relevantes.

3. fetch(id_mas_relevante)
   → Solo si el contenido resumido no es suficiente.
   → Máximo 2-3 fetch por consulta.
```

**Regla de oro**: Si la respuesta de timeline es suficiente, no hacer fetch.

---

## Anti-patrones

- **Fetch masivo**: `resultados.forEach(r => fetch(baseDir, r.id))` — anula el ahorro.
- **Búsqueda sin query**: `search(baseDir, "")` — retorna vacío (protección intencional).
- **Términos muy cortos**: "es", "el", "un" se filtran (longitud mínima: 3 caracteres).
- **Ignorar la capa 1**: Ir directo a fetch sin buscar primero.
- **Cargar APRENDIZAJES.md directamente**: Usar siempre esta API en su lugar.

---

## Cuándo NO cargar

- La consulta es una búsqueda de código (patrón, función, clase); `Grep` sobre el codebase es más eficiente y no involucra el índice de sesiones.
- La información pedida ya está en el contexto de la conversación — re-buscarla en la memoria persistente cuando ya está disponible desperdicia tokens y tiempo.
- El objetivo es agregar o actualizar un aprendizaje; este skill es de solo lectura. Usar `extractor-de-aprendizajes` para escrituras.
- El proyecto no tiene sesiones anteriores ni entradas en APRENDIZAJES.md; la búsqueda devolvería vacío. Verificar con `ls .planning/sessions/` antes de cargar.

## Gotchas / Errores comunes no obvios

- **Fetch masivo de todos los resultados de search()**: el agente llama a `fetch()` para cada uno de los 20 resultados de `search()`, consumiendo ~20,000 tokens en lugar de los ~6,500 del protocolo de 3 capas. Causa: se implementó el fetch como iteración directa. Solución: la regla de oro es explícita — hacer fetch solo de los 1-3 resultados definitivamente relevantes; para los demás, el resumen de `timeline()` es suficiente.
- **IDs de aprendizaje (`apr-N`) hardcodeados entre sesiones**: el agente guarda `apr-42` en ESTADO.md y lo usa en la sesión siguiente, pero el índice cambió porque se reordenó APRENDIZAJES.md. Causa: los IDs posicionales son estables solo dentro de una sesión. Solución: nunca hardcodear `apr-N` en persistencia larga; siempre obtenerlos desde `search()` en el momento de uso.
- **Búsqueda con query de 1-2 caracteres retorna vacío sin advertencia**: el agente busca `"CI"` y recibe un array vacío sin entender por qué. Causa: el filtro de longitud mínima de 3 caracteres descarta el término. Solución: usar queries de al menos 3 caracteres o frases descriptivas; para acrónimos cortos, expandirlos ("CI" → "continuous integration" o "integración continua").
- **Capa 3 (fetch) usada para instintos que `context-builder` ya inyectó**: el agente hace fetch del instinto `inst-selectinload-async` cuando ese instinto ya está en el bloque de contexto inyectado al inicio de sesión. Causa: no se verificó si el instinto ya estaba disponible en contexto. Solución: antes de hacer fetch de un instinto, verificar si aparece en el bloque `[SWL Context]` inyectado — si está ahí, no es necesario recuperarlo.

## Integración en hooks y agentes

Para usar desde un hook o agente SWL:

```js
// En cualquier hook dentro de hooks/
const { search, timeline, fetch } = require('./lib/memory-search');
const baseDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();

// Ejemplo: al inicio de sesión, buscar aprendizajes relevantes al contexto
const aprendizajes = search(baseDir, "FastAPI SQLAlchemy async", {
  tipo:  'aprendizaje',
  limit: 5,
});
```

El módulo es síncrono, sin dependencias externas y sin efectos secundarios.
Safe para usar en PreToolUse hooks sin riesgo de bloqueo.
