# Spec: CRUD de Fatos no MemoryClient

> Source PRD: `.vibeflow/prds/memory-crud.md`
> Generated: 2026-04-08

## Objective

Expor operações CRUD de fatos no `MemoryClient` para que consumidores do SDK (MCP servers, APIs REST) não precisem fazer SQL direto nos modelos.

## Context

O `MemoryClient` hoje só tem `write()` e `retrieve()`. Operações de gerenciamento (buscar por ID, listar, deletar) não existem no SDK. O MCP server do Arandu contorna isso com SQL direto nos modelos (`UPDATE memory_facts SET valid_to...`, `SELECT` direto). Todos os concorrentes (Mem0, Zep, Letta, LangMem) expõem CRUD completo. Hard delete é o padrão de mercado para a operação explícita do usuário.

## Definition of Done

1. `MemoryClient.get(agent_id, fact_id)` retorna `FactDetail | None` para um fato existente/inexistente.
2. `MemoryClient.get_all(agent_id)` retorna `list[FactDetail]` com paginação (`limit`, `offset`), filtrando apenas fatos ativos (`valid_to IS NULL`).
3. `MemoryClient.delete(agent_id, fact_id)` faz hard delete (row removida) e retorna `bool`.
4. `MemoryClient.delete_all(agent_id)` faz hard delete de todos os fatos do agente e retorna `int` (count).
5. `FactDetail` é um dataclass exportado no `__init__.py` e no `__all__`.
6. Testes unitários cobrem todos os 4 métodos (happy path + edge cases), seguindo o pattern mock-based do projeto (sem DB).
7. Docs atualizados em `docs/en/` e `docs/pt-BR/` — getting-started com seção CRUD + reference com `FactDetail`.

## Scope

### Código

- **`src/arandu/client.py`**:
  - Dataclass `FactDetail` com campos: `fact_id` (str), `entity_name` (str), `entity_key` (str), `entity_type` (str), `attribute_key` (str | None), `fact_text` (str), `category` (str | None), `confidence` (float), `importance` (float), `valid_from` (datetime), `created_at` (datetime), `source_context` (str | None).
  - `async def get(self, agent_id: str, fact_id: str) -> FactDetail | None` — query `MemoryFact` por `id` + `agent_id`. Retorna `None` se não encontrado. Não filtra por `valid_to` (permite buscar fatos invalidados pelo ID).
  - `async def get_all(self, agent_id: str, *, limit: int = 100, offset: int = 0) -> list[FactDetail]` — query `MemoryFact` filtrado por `agent_id` + `valid_to IS NULL`, ordenado por `created_at DESC`, com `LIMIT` e `OFFSET`.
  - `async def delete(self, agent_id: str, fact_id: str) -> bool` — hard delete da row. Retorna `True` se deletou, `False` se não encontrou. Entity links são removidos automaticamente via `CASCADE`.
  - `async def delete_all(self, agent_id: str) -> int` — hard delete de todos os `MemoryFact` do `agent_id`. Retorna count de rows deletadas.

- **`src/arandu/__init__.py`**: adicionar `FactDetail` ao import e ao `__all__`.

- **`pyproject.toml`**: bump de versão (minor — nova funcionalidade pública).

### Testes

- **`tests/test_client_crud.py`** (novo arquivo):
  - Mock do `AsyncSession` e `_session_factory` (contextmanager).
  - `class TestGet`: fato encontrado → retorna `FactDetail`; fato não encontrado → retorna `None`.
  - `class TestGetAll`: lista com fatos → retorna lista; sem fatos → retorna `[]`; verifica que query filtra `valid_to IS NULL` e aplica `limit`/`offset`.
  - `class TestDelete`: fato existe → deleta e retorna `True`; fato não existe → retorna `False`; verifica que commit é chamado.
  - `class TestDeleteAll`: fatos existem → deleta e retorna count; sem fatos → retorna `0`.

### Docs

- **`docs/en/getting-started.md`**: nova seção "Managing Facts" após a seção de retrieve, com exemplos de `get`, `get_all`, `delete`, `delete_all`.
- **`docs/pt-BR/getting-started.md`**: mesma seção, traduzida.
- **`docs/en/reference/index.md`**: adicionar `FactDetail` na seção Client (auto-generated via mkdocstrings).
- **`docs/pt-BR/reference/index.md`**: idem.

## Anti-scope

- **`update(id, data)`** — Updates são via `write()` → reconcile. Bypass manual não entra.
- **`history(agent_id, fact_id)`** — Versioning via `supersedes_fact_id` pode ser adicionado depois.
- **Filtros avançados** — Sem filtro por `entity_key`, `category`, `confidence`, data range no `get_all`. Lista tudo do agente.
- **Cursor-based pagination** — Offset é suficiente. Cursor pode vir depois.
- **Cascade em entidades/relações** — `delete` remove o fato + entity_links (FK CASCADE). Não remove `MemoryEntity`, `MemoryEntityRelationship`, ou `MemoryEvent`.
- **Batch delete filtrado** — `delete_all` é tudo-ou-nada por agent_id. Sem filtro por entity, date, etc.

## Technical Decisions

### 1. Hard delete (não soft delete)

**Decisão:** `delete()` e `delete_all()` removem a row do banco.

**Trade-off:** Soft delete (`valid_to = now`) preservaria auditoria, mas todos os 4 concorrentes fazem hard delete. O soft delete via `valid_to`/`invalidated_at` já existe no pipeline de reconcile (quando o LLM decide que um fato mudou). A operação explícita do usuário ("quero apagar isso") é definitiva.

**Justificativa:** Quando o usuário diz "delete", ele espera que sumiu. Manter fantasma no banco pra uma operação que o usuário entende como "apagar" gera confusão. Se precisar de auditoria no futuro, o `history()` é o caminho (fora do v0).

### 2. `get()` não filtra por `valid_to`

**Decisão:** `get(agent_id, fact_id)` busca qualquer fato pelo ID, inclusive invalidados (soft-deleted pelo reconcile).

**Trade-off:** Poderia filtrar só ativos, mas o propósito de `get()` é referência direta — "me dá esse fato específico". Se o usuário tem o ID, ele quer ver o fato independente do status.

**Justificativa:** O `get_all()` já filtra ativos. O `get()` por ID é lookup direto — não faz sentido esconder um fato que o usuário está referenciando explicitamente.

### 3. `FactDetail` como tipo separado de `ScoredFact`

**Decisão:** Novo dataclass `FactDetail` em vez de reusar `ScoredFact`.

**Trade-off:** Reusar `ScoredFact` evitaria um tipo novo, mas `ScoredFact` tem `score` e `scores` (orientado a retrieval) e não tem `entity_type`, `category`, `confidence`, `importance`, `valid_from`, `created_at`, `source_context`.

**Justificativa:** Domínios diferentes pedem tipos diferentes. CRUD quer detalhes do fato; retrieval quer scores de relevância.

### 4. Paginação limit+offset (não cursor)

**Decisão:** `get_all()` usa `limit` + `offset`.

**Trade-off:** Cursor é mais robusto para datasets grandes com inserções frequentes, mas adiciona complexidade (precisa de cursor opaco, ordenação estável). O uso via MCP é interativo, não stream de dados.

**Justificativa:** Simplicidade. O Mem0 faz a mesma coisa. Se performance com offset alto for problema no futuro, cursor pode ser adicionado sem breaking change (novo parâmetro).

### 5. Método `delete_all` sem confirmação

**Decisão:** `delete_all(agent_id)` deleta tudo sem flag de confirmação.

**Trade-off:** Poderia exigir `confirm=True` como safety. Mas o SDK é uma biblioteca — a safety layer fica no consumidor (MCP server, API). O Mem0 faz igual: `delete_all(user_id=...)` sem confirmação.

**Justificativa:** SDKs não devem ter UX opinions. Quem chama `delete_all` já tomou a decisão.

## Applicable Patterns

- **Data Access** (`.vibeflow/patterns/data-access.md`):
  - Session via `self._session_factory()` como context manager
  - `select(MemoryFact).where(...)` para queries
  - `session.commit()` no final da operação (caller manages transaction)
  - `session.delete()` para hard delete individual
  - `delete(MemoryFact).where(...)` para bulk delete

- **Error Handling** (`.vibeflow/patterns/error-handling.md`):
  - CRUD é operação direta, não pipeline. Não precisa de fail-safe com defaults vazios.
  - Exceções de DB (connection error, constraint violation) devem propagar — o caller decide como tratar.
  - `delete()` retorna `False` em vez de raise quando fato não existe (not-found não é erro).

- **Testing** (`.vibeflow/patterns/testing.md`):
  - Mock do `AsyncSession` via `MagicMock(spec=AsyncSession)`
  - Sem DB real nos testes
  - Class-based grouping por método

- **Conventions** (`.vibeflow/conventions.md`):
  - Google-style docstrings com `Args:` e `Returns:`
  - Type hints strict (mypy `disallow_untyped_defs`)
  - `snake_case` para métodos, `PascalCase` para dataclasses

## Risks

1. **Entity links órfãos após delete** — O FK em `MemoryFactEntityLink` tem `ondelete="CASCADE"`, então links são removidos automaticamente. Risco baixo, mas validar no teste.

2. **`MemoryEntity.fact_count` fica inconsistente** — Após `delete`, o counter `fact_count` na entidade não é decrementado. Isso é tech debt aceitável — o `fact_count` já não é mantido em tempo real pelo pipeline (é atualizado pelo background job `compute_entity_importance`). Mitigação: documentar que `fact_count` é eventual.

3. **`delete_all` pode ser lento para agentes com muitos fatos** — Um `DELETE FROM memory_facts WHERE agent_id = X` pode lockar a tabela. Mitigação: pro v0 é aceitável. Se virar problema, adicionar batch delete com `LIMIT` em versão futura.
