# Cache / Memoizacion persistente

Gap identificado en el registry. Reimplementar desde cero.

memoize_go_core es in-memory y se pierde entre ejecuciones. Para pipelines que
corren repetidamente (especialmente con llamadas LLM costosas), un cache
persistente con TTL evita recalculos.

## Funciones a implementar

### cache_to_sqlite

- **Dominio:** infra
- **Lang:** Python
- **Purity:** impure (I/O disco)
- **Signature:** `cache_to_sqlite(db_path: str, namespace: str = "default") -> CacheStore`
- **Retorno:** Objeto CacheStore con metodos get/set/delete/clear/stats
- **Descripcion:** Cache key-value persistido en SQLite. Cada namespace es una tabla logica (filtro en la query, no tabla separada). Keys son strings, values se serializan con json. TTL en segundos, 0 = sin expiracion. Limpieza de expirados en cada get (lazy eviction).
- **Schema SQLite:**
  ```sql
  CREATE TABLE IF NOT EXISTS cache (
      namespace TEXT NOT NULL,
      key TEXT NOT NULL,
      value TEXT NOT NULL,        -- JSON serializado
      created_at REAL NOT NULL,   -- time.time()
      expires_at REAL,            -- NULL = no expira
      PRIMARY KEY (namespace, key)
  );
  ```
- **API del CacheStore:**
  ```python
  class CacheStore:
      def get(self, key: str) -> any | None: ...          # None si miss o expirado
      def set(self, key: str, value: any, ttl: float = 0) -> None: ...
      def delete(self, key: str) -> bool: ...
      def clear(self) -> int: ...                          # retorna filas eliminadas
      def stats(self) -> dict: ...                         # {"hits": N, "misses": N, "size": N}
      def get_or_set(self, key: str, factory: callable, ttl: float = 0) -> any: ...
  ```
- **Deps:** `sqlite3`, `json`, `time` (solo stdlib)
- **Tests:**
  - Set y get basico
  - TTL expirado → None
  - TTL 0 → nunca expira
  - get_or_set con factory que solo se llama en miss
  - Namespaces independientes
  - Clear elimina solo el namespace
  - Stats contadores correctos
  - Concurrencia (threading basico)

### cache_to_file

- **Dominio:** infra
- **Lang:** Python
- **Purity:** impure
- **Signature:** `cache_to_file(cache_dir: str, namespace: str = "default") -> FileCache`
- **Retorno:** Objeto FileCache con misma API que CacheStore
- **Descripcion:** Cache key-value donde cada entry es un archivo JSON en disco. Mas simple que SQLite, mejor para valores grandes (PDFs procesados, embeddings). Key se hashea con SHA-256 para nombre de archivo. Metadata (ttl, created_at) en un sidecar `.meta` file.
- **Estructura en disco:**
  ```
  cache_dir/
    namespace/
      {sha256_key}.json    # valor
      {sha256_key}.meta    # {"created_at": ..., "expires_at": ..., "original_key": ...}
  ```
- **API:** Misma interfaz que CacheStore (get, set, delete, clear, stats, get_or_set)
- **Deps:** `os`, `json`, `hashlib`, `time` (solo stdlib)
- **Tests:**
  - Set y get basico
  - TTL expirado → None
  - Archivo .meta con metadata correcta
  - Clear elimina el directorio del namespace
  - Key con caracteres especiales → hash seguro

### cache_decorator

- **Dominio:** core
- **Lang:** Python
- **Purity:** impure (usa cache store)
- **Signature:** `cache_decorator(store: CacheStore | FileCache, ttl: float = 0, key_fn: callable | None = None)`
- **Retorno:** Decorator que cachea resultados de funciones
- **Descripcion:** Decorator que cachea el resultado de una funcion en un store persistente. Por defecto, la key se genera hasheando `(func.__name__, args, sorted(kwargs))`. `key_fn` permite custom key generation.
- **Uso:**
  ```python
  store = cache_to_sqlite("cache.db")
  
  @cache_decorator(store, ttl=3600)
  def call_llm(prompt: str) -> str:
      ...  # llamada costosa
  
  result = call_llm("explain X")  # primera vez: llama LLM
  result = call_llm("explain X")  # segunda vez: desde cache
  ```
- **Deps:** `functools`, `json`, `hashlib`
- **Tests:**
  - Funcion llamada una vez, segunda vez desde cache
  - TTL expirado → llama de nuevo
  - key_fn custom
  - Argumentos distintos → keys distintas
  - Funciona con async (detect asyncio.iscoroutinefunction)

### cache_to_sqlite (Go)

- **Dominio:** infra
- **Lang:** Go
- **Purity:** impure
- **Signature:** `CacheToSQLite(dbPath, namespace string) (*SQLiteCache, error)`
- **Retorno:** *SQLiteCache con metodos Get/Set/Delete/Clear
- **Descripcion:** Misma semantica que Python. Valores almacenados como JSON string. Get retorna `([]byte, bool)` — el caller deserializa.
- **API:**
  ```go
  type SQLiteCache struct { ... }
  func (c *SQLiteCache) Get(key string) ([]byte, bool) { ... }
  func (c *SQLiteCache) Set(key string, value []byte, ttl time.Duration) error { ... }
  func (c *SQLiteCache) Delete(key string) error { ... }
  func (c *SQLiteCache) Clear() (int64, error) { ... }
  func (c *SQLiteCache) GetOrSet(key string, factory func() ([]byte, error), ttl time.Duration) ([]byte, error) { ... }
  ```
- **Deps:** `database/sql`, `encoding/json`, `time`, `crypto/sha256`
- **Tests:**
  - Set/Get basico
  - TTL expirado
  - GetOrSet con factory
  - Concurrencia (goroutines)
