# PRD: Deterministic Entity Resolution for Read Pipeline

> Generated via /vibeflow:discover on 2026-03-20

## Problem

O read pipeline depende 100% do LLM planner para extrair entities da query do usuário. Quando o LLM falha (variância natural), `plan.entities` fica vazio e o graph signal — BFS 2-hop que traz facts de entities relacionadas — simplesmente não roda.

Reteste da v0.6.5 pelo time de validação mostrou inconsistência: "Quem é irmão do Pedro?" → extrai `person:pedro` → graph roda (18 facts, 10 edges). "Onde o Carlos mora?" → entities vazio → graph = 0. Mesma estrutura de query, resultados diferentes. Quando o LLM chuta formato errado (`entity:Carlos` em vez de `person:carlos`), graph também não encontra.

O SDK já tem infraestrutura pra resolver isso que não está sendo usada no read:
- `MemoryEntityAlias` com aliases normalizados → canonical entity_key
- `MemoryEntity` com display_name
- `expand_query()` que resolve aliases e busca vizinhos KG — mas os `primed_entities` NÃO alimentam o graph signal
- Schema cache com todos os entity_keys do user

A dependência exclusiva no LLM é um defeito arquitetural. Entity extraction de queries deveria ser primariamente determinística, com o LLM como suplemento.

## Target Audience

Desenvolvedores integrando o Arandu SDK que dependem do graph signal para recuperar facts de entities relacionadas via `memory.retrieve()`.

## Proposed Solution

Criar entity resolution determinística no read pipeline que funciona como camada primária, usando a infraestrutura já existente. O LLM planner passa a ser suplementar. Três fontes de entities são unificadas antes do graph gate:

1. **Determinística (primária)** — Match de tokens da query contra entity names (display_name, alias, slug do entity_key). Case-insensitive, normalize accents. Usa tabelas `MemoryEntityAlias` e `MemoryEntity` que já existem.

2. **LLM planner (suplementar)** — Continua extraindo entities como hoje. Pega referências indiretas que o determinístico não resolve ("meu chefe", "a empresa dele").

3. **Query expansion (já existe)** — `expand_query().primed_entities` já resolve aliases. Hoje é usado só pra contexto. Passar a alimentar o graph também.

O pipeline faz union das 3 fontes antes do `if plan.entities:` gate.

## Success Criteria

- "Onde o Carlos mora?" retorna `graph_facts > 0` quando `person:carlos` existe no banco (entity extraída deterministicamente)
- Entity resolution determinística roda em < 10ms (single DB query, não LLM)
- Quando LLM falha em extrair entities, determinístico funciona como fallback
- Quando LLM extrai formato errado (`entity:Carlos`), pipeline ainda resolve pra `person:carlos`
- Zero regressão nos 338 testes existentes

## Scope v0

1. **Criar `_resolve_query_entities()` em `query_expansion.py`** — Função determinística que:
   - Extrai tokens da query (já existe `_extract_query_words()`)
   - Busca matches em `MemoryEntityAlias` (alias → canonical_entity_key)
   - Busca matches em `MemoryEntity.display_name` (case-insensitive)
   - Faz substring match de tokens contra slugs de entity_keys do schema
   - Retorna `list[str]` de entity_keys resolvidos

2. **Atualizar `pipeline.py`** — Antes do graph gate `if plan.entities:`:
   - Chamar `_resolve_query_entities()`
   - Merge com `plan.entities` (union, dedup)
   - Merge com `expanded.primed_entities` (se expand_query já rodou)
   - Usar entities unificados pro graph_task

3. **Normalização de entities do LLM** — No `_parse_plan()`, quando o LLM retorna entities com formato errado (ex: `entity:Carlos`), tentar normalizar contra o schema antes de descartar.

4. **Testes unitários** — Testar:
   - Determinístico encontra entity por display_name
   - Determinístico encontra entity por alias
   - Determinístico encontra entity por slug do entity_key
   - Fallback funciona quando LLM retorna entities vazio
   - Union de LLM + determinístico sem duplicatas
   - Formato errado do LLM é normalizado

5. **Doc updates EN + PT-BR** — Atualizar seção do Planner nos docs explicando a entity resolution híbrida (determinístico + LLM).

## Anti-scope

- Não mudar `graph_retrieval.py` — BFS funciona, o input que tava errado
- Não mudar o write pipeline ou entity resolution do write
- Não adicionar embedding-based fuzzy matching no read (overkill pra v0, difflib é suficiente se necessário)
- Não mudar a interface pública do `plan_retrieval()` ou `RetrievalPlan`
- Não fazer entity disambiguation (se "João" resolve pra 2 entities, inclui ambos)
- Não mudar config defaults
- Não adicionar cache novo — usar o schema_cache que já existe

## Technical Context

- `src/arandu/read/query_expansion.py` — `_resolve_aliases()` já faz match de palavras da query contra `MemoryEntityAlias`. `_entity_priming()` já retorna `primed_entities`. Infraestrutura pronta pra ser estendida.
- `src/arandu/read/retrieval_agent.py` — `_get_user_schema()` já retorna todos os `(entity_key, attribute_key)` pairs. Schema cache com TTL de 5 min.
- `src/arandu/read/pipeline.py` (linhas 226-238) — Graph gate `if plan.entities:`. Ponto de merge.
- `src/arandu/models.py` — `MemoryEntityAlias` (linhas 225-241), `MemoryEntity` (linhas 199-223).
- Pattern: fail-safe (se resolver retornar vazio, graph não roda — correto)
- Pattern: async everywhere (DB queries devem ser async)
- Pattern: no DB in tests (mock session)

## Open Questions

None.
