# ADR-050 — Busca local-first e serviço compartilhado no segundo consumidor

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-23) |
| **Data** | 2026-10-23 |
| **Plano de origem** | `wiki/implementation-phases/P7-descoberta-busca.md`, `P9-servicos-plataforma.md` |
| **Relacionadas** | ADR-001, ADR-013, ADR-015, ADR-024, ADR-034, ADR-043 |

## Contexto

Notícias precisa de busca em português no P7, mas o produto terá outros módulos com conteúdo
(legislação, publicações, páginas, licitações). Criar um service de busca antes do segundo
consumidor adiciona contrato, índice e reindexação sem demanda comprovada; manter cada módulo com
sua própria busca cria duas verdades e resultados inconsistentes.

O CMS também precisa buscar rascunhos e conteúdo restrito, enquanto o portal só pode ver conteúdo
publicado. Um único índice misturando os dois escopos vaza rascunho ou exige filtro frágil em toda
consulta.

## Decisão

1. **Local-first:** no P7, Notícias usa `search_text` + `search_tsv` gerada + índice GIN e busca
   própria do módulo. Nenhum service compartilhado nasce nessa fase.
2. **Service no segundo consumidor:** `src/services/Search` nasce no P9, quando houver o segundo
   módulo consumidor. A integração é **opt-in**: módulo sem `SearchSource` não entra no índice e
   não quebra o build.
3. **Fonte pull-based:** o módulo registra uma `SearchSource` no `ModuleProvider::register()`; o
   service lê os documentos na reindexação. O módulo **chama o service** e entrega um
   `SearchDocument` padrão; o service não importa o módulo.
4. **Dois escopos, dois objetos:** `SearchDocument` tem `scope` (`portal|cms`), e o módulo devolve
   até dois documentos separados:
   - `portal`: URL pública, título/resumo/corpo, locale, `published_at`, visibilidade pública;
   - `cms`: URL do painel, título/resumo, locale, permissão `{module}.ver`, status editorial.
5. **Filtro no service:** o índice guarda `scope`, `module`, `entity_type`, `entity_id`, `locale`;
   a consulta filtra por escopo e, no CMS, por permissão. Resultado público nunca inclui rascunho.
6. **Uma verdade:** no P9, Notícias migra para o índice compartilhado; a busca local não continua
   como segunda implementação. Reindexação é idempotente e conteúdo despublicado sai do índice.

## Justificativa — alternativas descartadas

| Alternativa | Por que não |
|---|---|
| Service de busca no P7 | Contrato e reindexação sem segundo consumidor; atrasa a entrega local. |
| Busca separada por módulo para sempre | Duas verdades, ranking e escaping divergentes, resultados incompletos. |
| Índice único misturando portal e CMS | Exige filtro de permissão em toda query e arrisca vazar rascunho. |
| Módulo importando o service e empurrando linhas | Acopla módulo ao transporte; o service deve ler a fonte. |
| PostgreSQL FTS em todos os módulos sem serviço | Repetição de DDL, stemming, ranking e testes; aceitável só no piloto. |

## Consequências

**Positivas**
- Notícias entrega valor no P7 sem antecipar arquitetura.
- O contrato do P9 nasce com dois consumidores reais e um caso CMS/portal comprovado.
- Um único índice e uma única reindexação; despublicado deixa de aparecer.

**Custos / riscos**
- Duas fases de busca: local e depois cutover.
- Reindexação e ranking do service exigem testes de contrato e query count.
- O módulo precisa declarar a fonte; fonte ausente é silêncio intencional, não erro.
- O índice do CMS deve ser tratado como área restrita (ADR-043).

## Exemplo mínimo

```php
final readonly class NoticiasSearchSource implements SearchSource
{
    public function entityType(): string { return 'noticia'; }

    /** @return array{0?: SearchDocument, 1?: SearchDocument} */
    public function documents(string $id, string $locale): array
    {
        // portal: publicada; cms: qualquer status, com permissão e URL do painel
    }
}
```

```php
// consulta pública
$page = $search->query($term, SearchScope::Portal, ['locale' => $locale]);

// consulta do painel: o service aplica a permissão do ator
$page = $search->query($term, SearchScope::Cms, ['actor' => $user]);
```

## Verificação

- P7: busca de Notícias funciona com `tsvector` gerado e GIN; nenhum service `Search` existe.
- P9: dois módulos aparecem no mesmo resultado; portal não retorna rascunho; CMS filtra por
  permissão.
- Módulo sem `SearchSource` não quebra build nem aparece no índice.
- Despublicar remove do índice; reindexar duas vezes produz o mesmo resultado.
- `tools/arch-check` confirma que nenhum módulo importa `Modules\*` nem o service importa módulo.
