Pinky Brain — Piano di architettura (locale)

Sistema di gestione della conoscenza per agenti + esseri umani, scalabile a più progetti per 2+ anni. Evoluzione di quack-brain. Decisione presa: modalità LOCALE. SQLite incorporato, senza server, senza rete. Senza astrazione di storage (un solo backend reale → non si astrae, YAGNI).

1. Visione e obiettivi

  • Fonte di verità leggibile e versionata: la conoscenza vive in markdown + git. La modifica l'agente con i suoi tool nativi e la revisiona un essere umano con git diff.
  • Retrieval reale: ricerca ibrida (full-text + semantica), non grep manuale.
  • Multi-progetto: conoscenza di progetto + conoscenza globale tra progetti.
  • Durevole 2+ anni: l'indice è 100% rigenerabile dal markdown; nessun dato critico vive solo nel DB.
  • Zero ops: un binario + un file brain.db. Senza Docker, senza daemon, offline.

2. Principi di design

  1. Markdown = fonte di verità. SQLite = indice derivato, usa e getta. Se il DB si corrompe → pinky reindex lo ricostruisce dai .md.
  2. SOLID senza sovra-ingegneria. Un modulo store concreto su SQLite, **senza trait di astrazione**: non c'è un secondo backend, quindi non si astrae. Se un giorno apparisse (improbabile), si estrae il trait allora — non prima.
  3. Pull semantico invece di push cieco. L'agente cerca quando ne ha bisogno (tool MCP), invece di iniettare tutto il contesto in ogni hook.
  4. Offline-first. Funziona senza rete. Embeddings locali. La rete entra solo nel git pull/push del brain globale, ed è opzionale.
  5. Append-only dove si può (diary) affinché git faccia merge senza conflitti.

3. Architettura

┌─ 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: libreria con tutta la logica (parse, chunk, embed, search, store).
  • store: modulo concreto su rusqlite + sqlite-vec + FTS5. Senza trait.
  • pinky-mcp: espone brain_search, brain_save, brain_stats come MCP server. L'agente fa pull semantico invece di grep.
  • pinky CLI: reindex, search, doctor, stats, gc.
  • pinky-hooks: i 4 hook di quack-brain (SessionStart/PreRead/PreWrite/Stop), sottili: il retrieval pesante si delega al core/MCP invece di iniettare tutto.

4. Modello dei dati (SQLite)

Il frontmatter YAML si mappa a metadata indicizzati per filtrare:

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 tecnologico

NecessitàSceltaPerché
LinguaggioRustLatenza <50ms negli hook, un binario, senza runtime
Indicerusqlite + sqlite-vec + FTS5Un file, ibrido in una query, in-process
Embeddingsfastembed (ONNX, multilingual-e5-small)Locale, senza API, senza costo per hook, ES+IT
Rerank (opzionale)cross-encoder ONNX (bge-reranker-base)Precisione dopo il retrieval ibrido
Frontmattergray_matterYAML + body in un passo
File watchingnotifyRe-index incrementale
MCPrmcp (SDK ufficiale Rust)brain_search come tool
Async runtimetokioServer MCP + watcher

Embeddings multilingue: multilingual-e5-small (384 dims) copre spagnolo e italiano (corsi SGSVP) senza costo per query. Se più avanti serve più recall, bge-m3. Il modello si versiona nel metadata del chunk → reindex globale al cambiarlo.

6. Retrieval ibrido

Non solo vector. BM25 (FTS5) + vector (sqlite-vec), fusi con Reciprocal Rank Fusion, poi rerank opzionale:

  1. BM25: termini esatti (slugs, nomi di funzione, error strings).
  2. Vector: similarità semantica ("qualcosa di simile a questo problema").
  3. RRF: fonde entrambi i ranking senza calibrare pesi.
  4. Rerank (cross-encoder) sul top-N per la precisione finale.
  5. Filtri di metadata: per scope, project, type, tags, freschezza.

Lo stack ibrido (FTS5 + sqlite-vec + embeddings) parte dalla Fase 0, senza tappe intermedie di solo-testo.

7. Sync e multi-progetto

  • Conoscenza di progetto: vive nel repo del progetto (documentation/), versionata con il codice.
  • Conoscenza globale: repo git dedicato clonato in ~/.pinky/brain. Sync tra le tue macchine = git pull/push (questo NON è "modalità condivisa": resta locale, git è solo il trasporto). Append-only nel diary → merge senza conflitto.
  • L'indice brain.db non si committa mai; si ricostruisce su ogni macchina.

8. Miglioramenti che vorrei fare (rispetto a quack-brain)

  1. Staleness decay: penalizzare nel ranking le entry con last_verified vecchio; promemoria di ri-verifica. La conoscenza che invecchia si degrada da sola.
  2. Grafo código ↔ conocimiento: indicizzare i breadcrumb // Brain: {slug} come backlink. "Quale codice dipende da questo gotcha?" e viceversa.
  3. Dedup semantico: al salvare, rilevare entry quasi-duplicate (cosine > soglia) e proporre merge. Evita che il brain si riempia di gotcha ripetuti.
  4. Auto-tagging / classificazione via LLM al salvare (type + tags coerenti).
  5. Query rewriting / HyDE: espandere la query prima di cercare per migliorare il recall.
  6. Telemetria di utilizzo: registrare quali entry si recuperano e se sono state utili → potare la conoscenza morta (quelle che non si usano mai in 6 mesi).
  7. Rollup di diary: riassunti settimanali/mensili autogenerati dai diari giornalieri. Il changelog di alto livello si mantiene da solo.
  8. Eval harness: set di "golden queries" per misurare la qualità del retrieval nel tempo (regressioni di rilevanza al cambiare modello/chunking).
  9. Citazioni/provenance: ogni risposta dell'agente referenzia lo slug + path della entry che ha usato. Tracciabilità.
  10. CLAUDE.md evergreen automatizzato: validatore che rifiuta dati volatili (LOC, numeri di riga) in CLAUDE.md, come già richiede la regola 7 di quack-brain.

9. Roadmap per fasi

  • Fase 0 — Scaffold + retrieval ibrido completo (workspace Cargo con 4 crate: pinky-core, pinky, pinky-mcp, pinky-hooks). Modello dei dati + store SQLite con FTS5 e sqlite-vec + fastembed (multilingual-e5-small) + parse di frontmatter + reindex incrementale + ricerca ibrida (BM25 + vector, RRF). Embeddings dal giorno 1, senza tappa di solo-testo.
  • Fase 1 — MCP + hooks: pinky-mcp con brain_search/brain_save/brain_stats. I 4 hook sottili (SessionStart/PreRead/PreWrite/Stop) appoggiati sul core/MCP.
  • Fase 2 — Diary + rollup: diary automatico nell'hook Stop, rollup settimanali/ mensili, breadcrumb // Brain: → backlink.
  • Fase 3 — Miglioramenti di qualità: dedup semantico, staleness decay, telemetria di utilizzo, rerank cross-encoder, eval harness di golden queries.

10. Scalabilità e performance (orizzonte 2 anni)

  • Volume reale: più progetti × diary giornaliero × gotcha ≈ decine di migliaia di entry in 2 anni. È poco dato: SQLite gestisce milioni di righe e la ricerca vettoriale su decine di migliaia di vettori è banale (<10ms). Il collo di bottiglia non è il volume, è l'organizzazione e il retrieval.
  • Re-index incrementale per content_hash: si ri-embedda solo ciò che è cambiato.
  • Cache di embeddings su disco per non ricalcolare.
  • Partizionamento per scope/progetto per delimitare le ricerche.
  • WAL mode in SQLite per letture concorrenti (CLI + MCP + hooks) senza bloccare.

11. Rischi e mitigazioni

  • Drift markdown ↔ indice → l'indice è usa e getta + reindex idempotente + hash.
  • Qualità degli embeddings multilingue → l'eval harness misura le regressioni (miglioramento 8).
  • Rumore di conoscenza (entra spazzatura) → le 4 condizioni di salvataggio + dedup + telemetria di potatura.
  • Lock-in del modello di embeddings → versionare il modello nel metadata del chunk; reindex globale al cambiarlo.