Pinky Brain — Plan de arquitectura (local)

Sistema de gestión de conocimiento para agentes + humanos, escalable a varios proyectos durante 2+ años. Evolución de quack-brain. Decisión tomada: modo LOCAL. SQLite embebido, sin servidor, sin red. Sin abstracción de storage (un solo backend real → no se abstrae, YAGNI).

1. Visión y objetivos

  • Fuente de verdad legible y versionada: el conocimiento vive en markdown + git. Lo edita el agente con sus tools nativas y lo revisa un humano con git diff.
  • Retrieval real: búsqueda híbrida (full-text + semántica), no grep manual.
  • Multi-proyecto: conocimiento de proyecto + conocimiento global entre proyectos.
  • Durable 2+ años: el índice es 100% regenerable desde el markdown; ningún dato crítico vive solo en la DB.
  • Cero ops: un binario + un archivo brain.db. Sin Docker, sin daemon, offline.

2. Principios de diseño

  1. Markdown = fuente de verdad. SQLite = índice derivado, desechable. Si la DB se corrompe → pinky reindex la reconstruye desde los .md.
  2. SOLID sin sobre-ingeniería. Un módulo store concreto sobre SQLite, **sin trait de abstracción**: no hay un segundo backend, así que no se abstrae. Si algún día apareciera (improbable), se extrae el trait entonces — no antes.
  3. Pull semántico sobre push ciego. El agente busca cuando necesita (tool MCP), en vez de inyectar todo el contexto en cada hook.
  4. Offline-first. Funciona sin red. Embeddings locales. La red solo entra en el git pull/push del brain global, y es opcional.
  5. Append-only donde se pueda (diary) para que git haga merge sin conflictos.

3. Arquitectura

┌─ Fuente de verdad (git) ─────────────────────────────┐
│  <proyecto>/documentation/*.md                       │
│  ~/.pinky/brain/*.md            (global cross-proyecto)│
└───────────────────────┬──────────────────────────────┘
                        │  notify (file watcher)
                        ▼
┌─ pinky-core (lib Rust) ──────────────────────────────┐
│  parse(frontmatter) → chunk → embed → upsert         │
│  search híbrido (BM25 + vector, fusión RRF) → rerank  │
│  módulo `store`: SQLite (sqlite-vec + FTS5)          │
└───────────────────────┬──────────────────────────────┘
        ┌───────────────┼────────────────┐
        ▼               ▼                 ▼
   pinky (CLI)    pinky-mcp (server)   pinky-hooks
   reindex/search  brain_search tool   SessionStart/Stop
  • pinky-core: librería con toda la lógica (parse, chunk, embed, search, store).
  • store: módulo concreto sobre rusqlite + sqlite-vec + FTS5. Sin trait.
  • pinky-mcp: expone brain_search, brain_save, brain_stats como MCP server. El agente hace pull semántico en vez de grep.
  • pinky CLI: reindex, search, doctor, stats, gc.
  • pinky-hooks: los 4 hooks de quack-brain (SessionStart/PreRead/PreWrite/Stop), delgados: el retrieval pesado se delega al core/MCP en vez de inyectar todo.

4. Modelo de datos (SQLite)

El frontmatter YAML se mapea a metadata indexada para filtrar:

entry(
  id            TEXT PK,        -- hash estable del path
  path          TEXT,           -- ruta del .md (relativa al root)
  scope         TEXT,           -- 'project:<name>' | 'global'
  type          TEXT,           -- gotcha | pattern | bug | decision | diary | guide
  project       TEXT,
  tags          TEXT,           -- JSON array
  created       TEXT,           -- ISO date
  last_verified TEXT,           -- para staleness/decay (§9)
  title         TEXT,
  body          TEXT,
  content_hash  TEXT            -- re-index incremental (skip si no cambió)
)
chunk_fts        FTS5 virtual table (text)        -- BM25
chunk_vec        sqlite-vec virtual table (embedding float[384])
chunk(
  id, entry_id, ord, text                          -- une fts + vec con metadata
)
backlink(           -- grafo código ↔ conocimiento desde `// Brain: {slug}`
  entry_id, file_path, line, repo
)
usage(              -- telemetría: qué entradas se recuperan/usan de verdad
  entry_id, retrieved_at, query, was_useful
)

5. Stack tecnológico

NecesidadElecciónPor qué
LenguajeRustLatencia <50ms en hooks, un binario, sin runtime
Índicerusqlite + sqlite-vec + FTS5Un archivo, híbrido en una query, in-process
Embeddingsfastembed (ONNX, multilingual-e5-small)Local, sin API, sin costo por hook, ES+IT
Rerank (opcional)cross-encoder ONNX (bge-reranker-base)Precisión tras el retrieval híbrido
Frontmattergray_matterYAML + body en un paso
File watchingnotifyRe-index incremental
MCPrmcp (SDK oficial Rust)brain_search como tool
Async runtimetokioServer MCP + watcher

Embeddings multilingües: multilingual-e5-small (384 dims) cubre español e italiano (cursos SGSVP) sin costo por query. Si más adelante hace falta más recall, bge-m3. El modelo se versiona en la metadata del chunk → reindex global al cambiarlo.

6. Retrieval híbrido

No vector solo. BM25 (FTS5) + vector (sqlite-vec), fusionados con Reciprocal Rank Fusion, luego rerank opcional:

  1. BM25: términos exactos (slugs, nombres de función, error strings).
  2. Vector: similitud semántica ("algo parecido a este problema").
  3. RRF: fusiona ambos rankings sin calibrar pesos.
  4. Rerank (cross-encoder) sobre el top-N para precisión final.
  5. Filtros de metadata: por scope, project, type, tags, frescura.

El stack híbrido (FTS5 + sqlite-vec + embeddings) va desde la Fase 0, sin etapas intermedias de solo-texto.

7. Sync y multi-proyecto

  • Conocimiento de proyecto: vive en el repo del proyecto (documentation/), versionado con el código.
  • Conocimiento global: repo git dedicado clonado en ~/.pinky/brain. Sync entre tus máquinas = git pull/push (esto NO es "modo compartido": sigue siendo local, git es solo el transporte). Append-only en diary → merges sin conflicto.
  • El índice brain.db nunca se commitea; se reconstruye en cada máquina.

8. Mejoras que me gustaría hacer (sobre quack-brain)

  1. Staleness decay: penalizar en el ranking entradas con last_verified viejo; recordatorios de re-verificación. El conocimiento que envejece se degrada solo.
  2. Grafo código ↔ conocimiento: indexar los breadcrumbs // Brain: {slug} como backlinks. "¿Qué código depende de este gotcha?" y viceversa.
  3. Dedup semántico: al guardar, detectar entradas casi-duplicadas (cosine > umbral) y proponer merge. Evita que el brain se llene de gotchas repetidos.
  4. Auto-tagging / clasificación vía LLM al guardar (type + tags consistentes).
  5. Query rewriting / HyDE: expandir la query antes de buscar para mejorar recall.
  6. Telemetría de uso: registrar qué entradas se recuperan y si fueron útiles → podar conocimiento muerto (las que nunca se usan en 6 meses).
  7. Rollups de diary: resúmenes semanales/mensuales autogenerados desde los diarios diarios. El changelog de alto nivel se mantiene solo.
  8. Eval harness: set de "golden queries" para medir calidad de retrieval a lo largo del tiempo (regresiones de relevancia al cambiar modelo/chunking).
  9. Citas/provenance: toda respuesta del agente referencia el slug + path de la entrada que usó. Trazabilidad.
  10. CLAUDE.md evergreen automatizado: validador que rechaza datos volátiles (LOC, números de línea) en CLAUDE.md, como ya pide la regla 7 de quack-brain.

9. Roadmap por fases

  • Fase 0 — Scaffold + retrieval híbrido completo (workspace Cargo con 4 crates: pinky-core, pinky, pinky-mcp, pinky-hooks). Modelo de datos + store SQLite con FTS5 y sqlite-vec + fastembed (multilingual-e5-small) + parse de frontmatter + reindex incremental + búsqueda híbrida (BM25 + vector, RRF). Embeddings desde el día 1, sin etapa de solo-texto.
  • Fase 1 — MCP + hooks: pinky-mcp con brain_search/brain_save/brain_stats. Los 4 hooks delgados (SessionStart/PreRead/PreWrite/Stop) apoyados en el core/MCP.
  • Fase 2 — Diary + rollups: diary automático en el hook Stop, rollups semanales/ mensuales, breadcrumbs // Brain: → backlinks.
  • Fase 3 — Mejoras de calidad: dedup semántico, staleness decay, telemetría de uso, rerank cross-encoder, eval harness de golden queries.

10. Escalabilidad y performance (horizonte 2 años)

  • Volumen real: varios proyectos × diary diario × gotchas ≈ decenas de miles de entradas en 2 años. Es poco dato: SQLite maneja millones de filas y la búsqueda vectorial sobre decenas de miles de vectores es trivial (<10ms). El cuello de botella no es el volumen, es la organización y el retrieval.
  • Re-index incremental por content_hash: solo se re-embebe lo que cambió.
  • Caché de embeddings en disco para no recalcular.
  • Particionado por scope/proyecto para acotar búsquedas.
  • WAL mode en SQLite para lecturas concurrentes (CLI + MCP + hooks) sin bloquear.

11. Riesgos y mitigaciones

  • Drift markdown ↔ índice → el índice es desechable + reindex idempotente + hash.
  • Calidad de embeddings multilingüe → eval harness mide regresiones (mejora 8).
  • Ruido de conocimiento (entra basura) → las 4 condiciones de guardado + dedup + telemetría de poda.
  • Lock-in de modelo de embeddings → versionar el modelo en la metadata del chunk; reindex global al cambiarlo.