# PRD: Alias Resolution no entity_keys + Warnings

> Generated via /vibeflow:discover on 2026-04-09

## Problem

O filtro `entity_keys` no `retrieve()` e `get_all()` não resolve aliases e falha silenciosamente. Quando um consumidor passa `entity_keys=["person:pedro"]` mas a canonical key da entidade é `person:pedro_menezes`, o retrieve retorna **zero candidatos** sem nenhum sinal de erro — o cliente acha que simplesmente não tem dado sobre a entidade, quando na verdade o filtro errou.

Isso é grave por três motivos:

1. **Bug real em produção.** O Personal Genius (assistente do Pedro via MCP) reproduziu o bug num teste básico. O write pipeline já registrou o alias `"pedro" → person:pedro_menezes` na tabela `memory_entity_aliases` durante a extração, mas o read pipeline ignora essa tabela. Dessa forma, há uma lacuna de integração write↔read: o write é permissivo, o read é rígido.

2. **DX trap.** O parâmetro `agent_id` aceita qualquer string ("pedro"), o `speaker_name` no write resolve aliases via grafo. O dev naturalmente pensa que `entity_keys=["person:pedro"]` segue o mesmo padrão permissivo — mas não. E quando falha, falha em silêncio: zero resultado sem dizer por quê. É o pior comportamento possível pra uma API pública.

3. **Bloqueador prático.** Sem esse fix, pra usar `entity_keys` corretamente o dev precisa chamar `entities()` antes, manter cache de alias→canonical, ou construir a canonical por convenção (`person:{slug}`). Cada um desses caminhos adiciona fricção sem benefício.

Relacionado: quando o dev passa múltiplos `entity_keys` (ex: curadoria de contexto a partir de vários hints), se um deles for inválido, o comportamento atual é "filtra pelos válidos em silêncio" ou "retorna tudo errado". O dev precisa saber quais foram válidos e quais não foram.

## Target Audience

Devs do SDK — especialmente quem integra via MCP server ou qualquer camada que queira filtrar contexto por entidade. O caso de uso primário é um agente curando contexto pra um usuário específico ("quero fatos sobre o Pedro" → passa `pedro`). O caso secundário é uma curadoria com múltiplos entity_keys vindos de uma pipeline qualquer (algum deles pode estar desatualizado).

## Proposed Solution

Três mudanças coordenadas:

1. **Resolver alias automaticamente no `entity_keys` filter.** Quando o filter é aplicado em `semantic_search`, `keyword_search` e `get_all`, o SDK primeiro expande cada chave passada via lookup na tabela `memory_entity_aliases` (se a chave é alias) e/ou verifica se é canonical key existente. A lista expandida vai pro `WHERE entity_key IN (...)` do `EXISTS` subquery.

2. **Adicionar campo `warnings: list[str]` ao `RetrieveResult`.** Quando alguma chave não resolve (nem canonical nem alias), o SDK adiciona uma string de warning explicando qual chave foi ignorada. Isso elimina o "zero silencioso".

3. **Comportamento parcial (não abortar):** se o dev passa `entity_keys=["person:pedro", "person:xyz"]` e `person:xyz` não existe, o SDK filtra pelos válidos e adiciona warning só pros inválidos. Se **todos** forem inválidos, retorna resultado vazio com warnings — o dev pediu filtro explícito, ignorar o filtro seria pior. Mas sempre com warning explicando o que aconteceu.

## Success Criteria

1. `retrieve(agent_id="pedro", query="...", entity_keys=["person:pedro"])` retorna os mesmos fatos que `entity_keys=["person:pedro_menezes"]` quando "pedro" é alias registrado pra "pedro_menezes".
2. Se o dev passa uma chave que não existe nem como canonical nem como alias, `result.warnings` contém uma string indicando qual chave foi ignorada.
3. Com mistura de válidas e inválidas, o filtro aplica só as válidas e warnings listam só as inválidas.
4. Com todas inválidas, `result.facts == []` e `result.warnings` tem entradas pra cada chave.
5. Sem `entity_keys` (default `None`), comportamento é 100% idêntico ao atual — nenhuma query nova, zero overhead.
6. Docs (getting-started EN + PT-BR, personal-assistant EN + PT-BR) explicam que `entity_keys` aceita aliases, com exemplo concreto e menção ao campo `warnings`.

## Scope v0

- Função interna `_resolve_entity_keys(session, agent_id, entity_keys) -> tuple[list[str], list[str]]` que retorna `(resolved_keys, invalid_keys)`. Vive em `src/arandu/read/retrieval.py` ou em novo helper.
- Consulta única à `memory_entity_aliases` + `memory_entities` pra resolver o batch todo numa query só (evitar N queries).
- `semantic_search` e `keyword_search` recebem `entity_keys` já resolvida (a resolução acontece upstream em `retrieve_candidates` ou `run_read_pipeline`).
- `get_all` em `client.py` também resolve.
- Campo `warnings: list[str] = field(default_factory=list)` em `RetrieveResult` (`client.py`) e em `ReadResult` interno (`read/pipeline.py`).
- Propagação do `warnings` do `ReadResult` pro `RetrieveResult` na conversão final.
- Atualizar docs EN + PT-BR em `docs/en/getting-started.md`, `docs/pt-BR/getting-started.md`, `docs/en/personal-assistant.md`, `docs/pt-BR/personal-assistant.md`.
- Testes unitários:
  - Alias resolution → canonical
  - Canonical key direta continua funcionando
  - Mistura de válidos + inválidos → filtra pelos válidos + warnings
  - Todos inválidos → vazio + warnings
  - Sem entity_keys → comportamento idêntico (sem queries extras à alias table)
- Bump de versão (patch — bugfix + DX).

## Anti-scope

- **`warnings` no `WriteResult`** — `facts_added/updated/deleted/unchanged` já dão sinais suficientes pro write. Adiciona só se virar caso real.
- **Warnings estruturados (dict/enum)** — string livre no v0. Se o consumidor precisar parsear, evolui depois com backward compat.
- **Fuzzy matching de entity_key** — se o dev escreveu "persn:pedro" (typo), não tenta adivinhar. Warning e segue em frente.
- **Resolver entity_key por display_name** (ex: `entity_keys=["Pedro"]`) — não tá no escopo. Escopo é expandir alias→canonical existente, não fazer lookup por nome humano. Se virar dor, spec separada de `entity_names`.
- **Cache de alias resolution** — sem cache explícito. Uma query adicional por `retrieve()` quando `entity_keys` é passada é aceitável (< 1ms em tabela indexada).
- **Warnings no filter de `entities()`** — `entities()` não tem `entity_keys` filter.
- **Quebra de backward compat** — passar uma lista vazia `[]` em `entity_keys` continua significando "filtrar por nada" (ou seja, filtro ativo mas sem matches — resultado vazio). Passar `None` continua significando "sem filtro".

## Technical Context

### O que já existe

- **`MemoryEntityAlias` model** (`src/arandu/models.py:254-269`): tabela com `agent_id`, `alias`, `canonical_entity_key`, `canonical_entity_type`. Index em `(agent_id, alias)`. Já populada pelo write pipeline via `register_entity_alias` em `upsert.py`.
- **`MemoryEntity` model** (`src/arandu/models.py:226-252`): tabela com `canonical_key`, `agent_id`. Usada pra validar se uma key passada é canonical existente.
- **Filter atual no `entity_keys`:** `semantic_search` (`retrieval.py:105-113`) e `keyword_search` (`retrieval.py:189-197`) fazem `EXISTS` subquery contra `MemoryFactEntityLink.entity_key.in_(entity_keys)`. Zero lookup em `memory_entity_aliases`.
- **`get_all` em `client.py:476-530`:** mesmo padrão `EXISTS`, mesmo problema.
- **`RetrieveResult`** em `client.py:52-76`: dataclass já tem vários campos; adicionar `warnings` é trivial.
- **`ReadResult` interno** em `read/pipeline.py:53-61`: dataclass interna que precisa também ter `warnings` pra propagar do pipeline pro client.

### Patterns a seguir

- **Data Access** (`.vibeflow/patterns/data-access.md`): uma única query `SELECT canonical_entity_key FROM memory_entity_aliases WHERE agent_id = ? AND alias IN (...)` + uma única query `SELECT canonical_key FROM memory_entities WHERE agent_id = ? AND canonical_key IN (...)`. Combinar resultados em Python.
- **Error Handling** (`.vibeflow/patterns/error-handling.md`): se a query de resolução falhar (connection error), degradar pra comportamento antigo (usar entity_keys direto como fallback) + log warning. Nunca deixa o retrieve crashar.
- **Testing** (`.vibeflow/patterns/testing.md`): mock-based, sem DB real. Mockar a resposta da query de aliases.
- **Conventions**: Google docstrings, type hints strict, snake_case.

### Constraints

- A resolução acontece antes de chamar `semantic_search`/`keyword_search`, não dentro de cada uma. Evita duplicar query.
- Preservar ordem da resolução: se o dev passa `["a", "b"]`, a lista expandida deve ser determinística (facilita debug).
- Warnings são strings curtas e acionáveis: `"entity_key 'person:xyz' not found (not canonical, no matching alias)"`.

## Open Questions

None.
