Volver a la wiki

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:

  1. Scan recursivo de .md files.
  2. Parse de front-matter YAML (tipo, slug, título, tags, related, etc.).
  3. 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) → metadata JSON.

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:

  1. 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.

  2. 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_chunks recibe prune[] en el último lote.
    • Selecciona chunk IDs con chunk_index >= keep_count por nodo.
    • Borra de Vectorize (lotes de 500 IDs) → D1 (batch).
    • Retorna chunks_pruned (métrica).
  3. 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:

  1. Valida coherencia: nodos registrados vs chunks indexados.
  2. Calcula métricas de pipeline (duración, tokens, coverage).
  3. Registra resumen en tabla D1 bib_wiki_pending_commits (estado completed o failed).

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).

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)

  1. Determinismo: Phase B ahora asigna explícitamente chunk_index por doc (0-indexed). Antes: posición de lote (frágil).

  2. Limpieza: Nueva lista pruneList que el handler bib_index_chunks procesa en el último lote. Borra chunks huérfanos de generaciones anteriores.

  3. Normalización de paths: bib_register_doc stripea public/ para evitar gemelos (footgun s200: 12 docs duplicados).

  4. Métricas mejoradas: Output incluye chunks_pruned para 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

Subir