# Audit Report: entity-keys-alias-resolution

**Verdict: PASS**

**Spec:** `.vibeflow/specs/entity-keys-alias-resolution.md`
**Date:** 2026-04-09
**Tests:** 443 passed (full suite) · 18 entity-filter-specific · 11 new tests for this spec

---

## DoD Checklist

- [x] **1. `retrieve(agent_id, query, entity_keys=["person:pedro"])` retorna os mesmos fatos que `entity_keys=["person:pedro_menezes"]` quando "person:pedro" é alias registrado**
  Evidence: `src/arandu/read/retrieval.py:26-131` — helper `_resolve_entity_keys()` consulta `memory_entities` (canonical) e `memory_entity_aliases` (alias). `src/arandu/read/retrieval.py:513-520` — `retrieve_candidates()` chama o resolver antes de despachar `semantic_search`/`keyword_search`, e propaga **apenas** a lista resolvida (`resolved_entity_keys`) downstream. Validado pelos testes:
  - `TestResolveEntityKeys::test_alias_resolves_to_canonical`
  - `TestResolveEntityKeys::test_alias_with_full_prefix_form`
  - `TestResolveEntityKeys::test_canonical_key_passes_through`

- [x] **2. `get_all(agent_id, entity_keys=["person:pedro"])` também resolve alias**
  Evidence: `src/arandu/client.py:515-522` — o `get_all()` importa `_resolve_entity_keys` diretamente de `arandu.read.retrieval` e usa o mesmo helper antes de construir o filtro SQL. Validado por `TestGetAllAliasResolution::test_get_all_resolves_alias_before_query` que confirma que o resolver é chamado com os entity_keys originais.

- [x] **3. `RetrieveResult` tem novo campo `warnings: list[str]`, populado quando keys não resolvem**
  Evidence:
  - `src/arandu/client.py:110-126` — dataclass `RetrieveResult` com `warnings: list[str] = field(default_factory=list)`
  - `src/arandu/read/pipeline.py:55-62` — dataclass interna `ReadResult` também tem `warnings`
  - `src/arandu/read/pipeline.py:260-265` — invalid_entity_keys vira strings formatadas no padrão canônico: `f"entity_key '{key}' not found (not canonical, no matching alias)"`
  - `src/arandu/client.py:446` — mapeamento `warnings=list(result.warnings)` na conversão `ReadResult → RetrieveResult`
  - Validado por `TestRetrieveWarnings::test_warnings_propagated_from_pipeline`

- [x] **4. Comportamento parcial: `entity_keys=["person:pedro", "person:xyz"]` filtra pelos válidos e adiciona warning só pros inválidos**
  Evidence: `retrieval.py:99-122` — o loop de assembly acumula válidos em `resolved_keys` e inválidos em `invalid_keys` separadamente, independente uns dos outros. Nenhum abort, nenhum early-return. Validado por `TestResolveEntityKeys::test_partial_mix_valid_and_invalid` que confirma resolved=["person:pedro_menezes"] e invalid=["person:xyz"] a partir de input misto.

- [x] **5. Sem `entity_keys` (`None`), zero queries novas contra memory_entity_aliases/memory_entities**
  Evidence: `retrieval.py:509-515` guard `if entity_keys is not None:` antes da chamada a `_resolve_entity_keys`. `client.py:515-516` mesmo guard no `get_all`. Validado por `TestGetAllAliasResolution::test_get_all_without_entity_keys_skips_resolution` que verifica via mock que o resolver não é chamado. Também `TestResolveEntityKeys::test_empty_list_returns_empty` confirma que lista vazia não dispara query.

- [x] **6. Quality gate — helper único chamado uma vez upstream**
  Evidence: grep `_resolve_entity_keys(` no código do projeto (não testes):
  ```
  src/arandu/read/retrieval.py:28  # definição
  src/arandu/read/retrieval.py:515 # chamada em retrieve_candidates
  src/arandu/client.py:518        # chamada em get_all
  ```
  Zero outras chamadas. `semantic_search` e `keyword_search` continuam recebendo `entity_keys: list[str] | None` e não têm lookup de alias próprio — recebem a lista já resolvida vinda de upstream. Invariante arquitetural de "resolução única por request" satisfeito.

- [x] **7. Docs EN + PT-BR atualizados em getting-started + personal-assistant**
  Evidence:
  - `docs/en/getting-started.md:335-349` — admonition `tip "Aliases are resolved automatically"` + exemplo com warnings
  - `docs/pt-BR/getting-started.md:335-349` — tradução idêntica
  - `docs/en/personal-assistant.md:115-127` — nota sobre alias resolution + exemplo de warnings
  - `docs/pt-BR/personal-assistant.md:115-127` — tradução

---

## Pattern Compliance

- [x] **Data Access** (`.vibeflow/patterns/data-access.md`)
  Evidence: Ambas as queries do resolver usam `select(...).where(...)` async. Indexes existem: `uq_memory_entity_user_key` em `(agent_id, canonical_key)` e `ix_entity_alias_user_lookup` em `(agent_id, alias)`. Caller managed transaction mantido — o resolver não faz commit. Zero raw SQL, zero uso de `text()` — tudo via Core SQLAlchemy expression language.

- [x] **Error Handling** (`.vibeflow/patterns/error-handling.md`)
  Evidence: `retrieval.py:126-131` — bloco try/except genérico em volta da resolução. Em caso de falha, `logger.warning()` + fallback `return list(entity_keys), []` — degrada pro comportamento legacy (usa as chaves cruas, sem warnings). Padrão fail-safe do projeto mantido. Validado por `TestResolveEntityKeys::test_failsafe_on_db_error` que simula `RuntimeError` no `session.execute` e confirma o fallback.

- [x] **Pipeline Orchestration** (`.vibeflow/patterns/pipeline-orchestration.md`)
  Evidence: A resolução acontece no **ponto de entrada do retrieve** (`retrieve_candidates`), antes do despacho paralelo pra `semantic_search`/`keyword_search`. Não há novo estágio de pipeline — a resolução é um step interno de `retrieve_candidates`. Mantém o fluxo linear do read pipeline (`plan → expand → retrieve → rerank → compress`).

- [x] **Testing** (`.vibeflow/patterns/testing.md`)
  Evidence: `tests/test_retrieve_entity_filter.py` adiciona 3 novas classes (`TestResolveEntityKeys`, `TestRetrieveWarnings`, `TestGetAllAliasResolution`) com 11 testes novos. Todos mock-based via `AsyncMock`/`MagicMock`. Zero uso de DB real. Class-based grouping como nos outros testes do projeto. Imports relativos via `from arandu.read.retrieval import _resolve_entity_keys` dentro dos testes (pattern normal).

---

## Convention Violations

Nenhuma. Verificações:
- ✅ Google-style docstrings com Args/Returns em `_resolve_entity_keys` e atualização na `retrieve_candidates` (nova tupla de retorno documentada)
- ✅ Type hints strict — mypy passa sem erros nos 43 source files
- ✅ Line length 120 — ruff format passa
- ✅ `snake_case` para funções, `PascalCase` para classes de teste
- ✅ Import sorting (isort via ruff)
- ✅ Helper privado com `_` prefix
- ✅ Lazy import de `sqlalchemy.select` dentro da função mantido (consistente com padrão do projeto)

---

## Anti-scope Verification

Todos os itens anti-scope foram respeitados:
- ✅ `WriteResult` não recebeu `warnings` — só `RetrieveResult`
- ✅ `get_all` retorna `list[FactDetail]` sem warnings — silently discards invalid keys (documentado na docstring em `client.py:502-509`)
- ✅ Formato de warning é **string livre** fixa, não estruturado
- ✅ Zero fuzzy matching, typo correction ou heurística de similaridade
- ✅ Zero lookup por `display_name` — só alias/canonical exato
- ✅ Sem cache — uma query extra por retrieve filtrado
- ✅ `entities()` não recebeu novo filtro
- ✅ Backward compat preservada: `None` = sem filtro (zero overhead), lista vazia = filtro ativo vazio

---

## Tests

- **Runner**: `python -m pytest tests/ -q`
- **Resultado**: `443 passed, 16 warnings in 0.28s`
- **Status**: PASS
- **CI completa**: ruff ✅ + ruff format ✅ + mypy ✅ + pytest ✅

### Testes específicos desta spec (11 novos)

```
TestResolveEntityKeys::test_canonical_key_passes_through       PASSED
TestResolveEntityKeys::test_alias_resolves_to_canonical        PASSED
TestResolveEntityKeys::test_alias_with_full_prefix_form        PASSED
TestResolveEntityKeys::test_unknown_key_goes_to_invalid        PASSED
TestResolveEntityKeys::test_partial_mix_valid_and_invalid      PASSED
TestResolveEntityKeys::test_empty_list_returns_empty           PASSED
TestResolveEntityKeys::test_failsafe_on_db_error               PASSED
TestResolveEntityKeys::test_duplicates_are_deduplicated        PASSED
TestRetrieveWarnings::test_warnings_propagated_from_pipeline   PASSED
TestRetrieveWarnings::test_warnings_empty_when_all_resolved    PASSED
TestGetAllAliasResolution::test_get_all_resolves_alias_before_query    PASSED
TestGetAllAliasResolution::test_get_all_without_entity_keys_skips_resolution PASSED
```

### Testes pré-existentes ajustados (3)

```
TestGetAllWithEntityKeys::test_entity_keys_filter_calls_execute  PASSED  # ajustado: patch no resolver
TestGetAllWithEntityKeys::test_entity_keys_empty_result          PASSED  # ajustado: patch no resolver
TestGetAllWithEntityKeys::test_multiple_entity_keys              PASSED  # ajustado: patch no resolver
```

---

## Architectural Notes

A implementação estabelece um padrão reusável para "normalização de inputs antes de filtrar queries":

> **Guideline:** quando uma API pública aceita um input que pode ser apresentado em múltiplas formas (canonical, alias, slug), o SDK deve resolver upstream — antes do filtro SQL — e sinalizar o que não resolveu via um campo `warnings` no dataclass de retorno.

Isso elimina o "zero silencioso" que era a raiz do trap de DX. Se no futuro outras features adotarem inputs polimórficos (ex: filtro por `categories` com sinônimos), o padrão a seguir é:

1. Helper privado `_resolve_X()` retornando `(resolved, invalid)`
2. Chamada única upstream no entry point do pipeline
3. Downstream recebe lista canônica
4. Warnings no dataclass de retorno público

Fail-safe: resolução nunca derruba a operação; degrada pro comportamento antigo em caso de erro.

---

## Overall

**Verdict: PASS**

Ready to ship. Todos os 7 DoD checks verificados com evidência concreta, CI verde, anti-scope respeitado integralmente, patterns seguidos. A refatoração elimina um bug reproduzido em produção (zero silencioso) e estabelece um invariante arquitetural ("resolução única upstream") que pode guiar features futuras com inputs polimórficos.
