Script de Ingesta bib_ingest_claude_method — Pipeline de Chunking y Embeddings
bib_ingest_claude_method.mjs
Módulo: scripts/bib_ingest_claude_method.mjs
Responsabilidad: Script Node.js de ingesta 3-fase del Bibliotecario. Orquesta extracción de docs, chunking, embeddings y registro en D1 + Vectorize del grafo de conocimiento Biblioteca.
Consumo: CI workflow ci/bib-ingest.yml, invocación manual vía node scripts/bib_ingest_claude_method.mjs <token> <mcpUrl>.
Pipeline 3-fase
Phase A: Lectura y registro de documentos
Entrada: Directorio src/content/wiki/ (hojas Markdown Supercontexto).
Proceso:
- Scan recursivo de
.mdfiles. - Parse de front-matter YAML (tipo, slug, título, tags, related, etc.).
- Registro en D1 via
bib_register_doc(handler MCP):- Crea o actualiza nodo-doc (
node_type='doc',qualified_name="doc:<path>"). - Normalización: Strip del prefijo
public/para evitar gemelos (fix commit@568494d). - Extrae metadatos (tipo, slug, tags) →
metadataJSON.
- Crea o actualiza nodo-doc (
Retorna: Mapa nodeIdMap: relPath → node_id.
Regla 15 (Guard): Si <80% de docs registrados, aborta Phase B.
Phase B: Chunking, embedding e indexación (actualizado commit@568494d; batching corregido s339)
Entrada: Docs registrados (nodeIdMap) + chunks extraídos previamente (docChunkMap).
Proceso:
-
Asignación de chunk_index determinista:
const allChunks = []; const pruneList = []; // Nueva: lista de poda for (const doc of docs) { const nodeId = nodeIdMap.get(doc.relPath); const rawChunks = docChunkMap.get(doc.relPath) || []; // chunk_index = 0, 1, 2, ... (posicion DENTRO del doc, no del lote) rawChunks.forEach((c, idx) => { allChunks.push({ ...c, node_id: nodeId, chunk_index: idx }); }); // Poda: conservar indices 0..keep_count-1, borrar >= keep_count pruneList.push({ node_id: nodeId, keep_count: rawChunks.length }); }Beneficio: Índices deterministas. Reupsert del mismo chunk por
(node_id, chunk_index)machaca la generación anterior en lugar de acumularla. -
Indexación en lotes por presupuesto de tokens RELLENADOS (corregido s339, 22-09-2026 — ver [[incident—20260921—bib-index-chunks-batch-token-budget]]):
// scripts/_lib/chunk_batches.mjs · splitChunkBatches() // Un lote se cierra cuando el coste RELLENADO (chunk más largo × nº de // chunks del lote) superaría BATCH_TOKEN_BUDGET (40k, UNVERIFIED), o al // llegar a BATCH_MAX_CHUNKS (25) — lo que ocurra antes. const batches = splitChunkBatches(allChunks, { maxChunks: 25, tokenBudget: 40000 }); for (let i = 0; i < batches.length; i++) { const batch = batches[i]; const isLast = i === batches.length - 1; // La poda viaja CON EL ÚLTIMO LOTE (después de upsertar todos los vigentes) const args = isLast && pruneList.length > 0 ? { chunks: batch, prune: pruneList } : { chunks: batch }; const result = await callMCP(mcpUrl, token, 'bib_index_chunks', args); }Por qué un tamaño fijo de 25 no bastaba: Workers AI rellena cada texto del lote hasta la longitud del más largo — el coste real que cobra bge-m3 es
tokens(chunk más largo) × nº de chunks del lote, no la suma de tokens del lote. Un lote de 25 chunks “pequeños” (~400 palabras cada uno) puede superar igualmente el techo de 60k tokens si uno solo de ellos es denso (tablas, código, rutas — hasta ~6 tokens/palabra en el Supercontexto).Flujo de poda:
- El handler
bib_index_chunksrecibeprune[]en el último lote. - Selecciona chunk IDs con
chunk_index >= keep_countpor nodo. - Borra de Vectorize (lotes de 500 IDs) → D1 (batch).
- Retorna
chunks_pruned(métrica).
- El handler
-
Métricas de salida:
{ chunks_created: <number>, chunks_updated: <number>, chunks_skipped: <number>, chunks_pruned: <number>, // Nueva métrica (commit@568494d) errors: <number> }
Regla 15: Si ratio de éxito (created + updated) / total < 70%, aborta Phase C.
Phase C: Auditoría y cleanup
Entrada: Resultados de Phase A + B.
Proceso:
- Valida coherencia: nodos registrados vs chunks indexados.
- Calcula métricas de pipeline (duración, tokens, coverage).
- Registra resumen en tabla D1
bib_wiki_pending_commits(estadocompletedofailed).
Salida: JSON con fases, métricas, y último commit procesado.
Sub-componentes
registerDoc(mcpUrl, token, doc)
Llama a bib_register_doc (handler MCP en biblioteca.ts). Retorna { node_id, created } o null si falla.
indexChunksBatch(mcpUrl, token, chunks, pruneList)
Cambio commit@568494d: Parámetro pruneList opcional (default: []).
Llamadas a bib_index_chunks en lotes repartidos por splitChunkBatches (ver chunk_batches.mjs abajo), ya no un tamaño fijo de 25. La poda se envía con el último lote.
Retorna: { chunks_created, chunks_updated, chunks_skipped, chunks_pruned, errors }.
scripts/_lib/chunk_batches.mjs (módulo compartido, s339 — 22-09-2026)
Reparte allChunks en lotes que caben en una llamada de embeddings de bge-m3 (techo real: 60k tokens por lote). Compartido por los pipelines que llaman a bib_index_chunks (Regla 2: no duplicar).
estimateChunkTokens(chunk)— estima tokens de un chunk como(título.length + contenido.length) / CHARS_PER_TOKEN, conCHARS_PER_TOKEN = 2.5(UNVERIFIED, cota conservadora).paddedBatchTokens(chunks)— coste de un lote tal y como lo cobra el modelo:max(tokens de cada chunk) × nº de chunks(Workers AI rellena cada texto hasta el más largo del lote).splitChunkBatches(chunks, { maxChunks = 25, tokenBudget = 40000 })— cierra el lote cuando añadir el siguiente chunk haría quepaddedBatchTokenssuperasetokenBudget, o al llegar amaxChunks. Un chunk que por sí solo supera el presupuesto viaja en un lote propio.
BATCH_TOKEN_BUDGET = 40000 (UNVERIFIED: dos tercios del techo de 60k de bge-m3, para absorber error de estimación). Motivado por un fallo real del cron supercontext de OPS — ver [[incident—20260921—bib-index-chunks-batch-token-budget]].
Cambios operacionales (commit@568494d)
-
Determinismo: Phase B ahora asigna explícitamente
chunk_indexpor doc (0-indexed). Antes: posición de lote (frágil). -
Limpieza: Nueva lista
pruneListque el handlerbib_index_chunksprocesa en el último lote. Borra chunks huérfanos de generaciones anteriores. -
Normalización de paths:
bib_register_docstripeapublic/para evitar gemelos (footgun s200: 12 docs duplicados). -
Métricas mejoradas: Output incluye
chunks_prunedpara monitorear eficacia de limpieza.
Ejemplos de uso
Ejecución manual
node scripts/bib_ingest_claude_method.mjs "<token>" "http://localhost:8787"
En CI (workflow)
- name: Ingest docs a Biblioteca
run: |
node scripts/bib_ingest_claude_method.mjs "${{ secrets.MCP_TOKEN }}" "${{ secrets.MCP_URL }}"
Troubleshooting
Accumulo de chunks viejos en bib_ask
Síntoma: bib_ask retorna resultados obsoletos (ej. docs de junio en julio).
Causa: Poda no se ejecutó o pruneList estaba vacío.
Solución: Verifica logs de Phase B (chunks_pruned > 0) y retira chunk_index legacy en callers.
Phase A falla >20% de docs
Causa: Posible problema de front-matter o path inválido.
Solución: Revisa logs de bib_register_doc en stderr. Fix path o YAML y rerun.
Timeout en Phase B / “Max context reached … tokens but model supports only 60000”
Causa histórica (hasta el 21-09-2026): un tamaño de lote FIJO de 25 chunks no basta — bge-m3 cobra por el coste RELLENADO (chunk más largo × nº de chunks), y un lote con un chunk denso (tabla, código, ruta larga) puede superar 60k tokens aunque tenga solo 25 piezas “pequeñas”. Así falló el cron supercontext de OPS desde el 21-09-2026 — detalle en [[incident—20260921—bib-index-chunks-batch-token-budget]].
Solución (desde s339, 22-09-2026): el reparto en lotes usa splitChunkBatches (scripts/_lib/chunk_batches.mjs, ver Sub-componentes arriba), que cierra el lote por presupuesto de tokens rellenados (40k, UNVERIFIED) además del máximo de 25 chunks. Si el timeout persiste, revisar si BATCH_TOKEN_BUDGET sigue siendo conservador frente al techo real de bge-m3, o si algún chunk individual supera el presupuesto por sí solo (viaja en lote propio, sin partirse).
Véase también
- [[entity—scripts—service—bib-ingest-claude-method]]
- [[entity—biblioteca—handler—bib-index-chunks]]
- [[entity—biblioteca—tool—bib-pipeline-stats]]
- [[incident—20260921—bib-index-chunks-batch-token-budget]]