# ADR-049 — Motor de dados abertos: fonte pull-based, não evento carregando dados

| Campo | Valor |
|---|---|
| **Status** | Aceito (2026-10-23) |
| **Data** | 2026-10-23 |
| **Plano de origem** | `wiki/implementation-phases/P5.5-motor-dados-abertos.md` |
| **Relacionadas** | ADR-001, ADR-013, ADR-015, ADR-018, ADR-030, ADR-034, ADR-042 |

## Contexto

O módulo `Portal/DadosAbertos` já publica datasets curados com CSV/JSON/XML, DCAT/INDA, checksum e
`ETag`. O PNTP 2026 e a LAI exigem que os demais módulos também publiquem dados — receita, despesa,
licitações, legislação, saúde, educação, Legislativo — sem que cada um invente um CSV, uma tabela
paralela ou um evento próprio.

A pergunta de desenho: **um service de dados abertos que recebe os dados dos módulos via event,
padroniza e unifica no catálogo**. A intenção é correta (um ponto de entrada e padronização); o
transporte por evento é o problema.

## Decisão

1. **Fonte é pull, não push.** O módulo dono implementa
   `Core\Contracts\Ports\DatasetSource` e registra a classe no `ModuleProvider::register()`; o
   service `OpenData` resolve a fonte, normaliza e serializa.
2. **Porta e registry no Core.** `DatasetSource`/`DatasetRegistry` ficam no Core pelo mesmo motivo
   de `SitemapRegistry`/`RetentionRegistry`: o `ModuleProvider::register()` roda antes de os
   services existirem (`bootstrap/providers.php`).
3. **O service é motor, o módulo é catálogo.** `Services\OpenData` não tem UI nem rota pública;
   `Portal/DadosAbertos` continua dono do catálogo, CMS, DCAT, sitemap e permissões. O dataset
   gerado referencia a fonte (`source`), **sem copiar as linhas** para `dados_abertos.datasets`.
4. **Evento não carrega dado.** Se houver invalidação imediata, o evento leva **somente** o
   `sourceId` e o service invalida cache/snapshot; um `open-data:refresh` por `fingerprint()` é o
   fallback. O dado é sempre relido da fonte.
5. **Um service, não dois.** O `services/Export` antes previsto no P9 é absorvido pelo `OpenData`;
   CSV/JSON/XML têm um só dono. O P9 fica com importação, notificações, feed e conversores.
6. **PDF é apresentação.** O motor entrega tabela normalizada + CSV/JSON/XML; HTML/print cobre PDF.
   PDF server-side não entra no núcleo.
7. **PII é gate, não bloqueio cego.** `DatasetMetadata::containsPersonalData` exige
   `publicationBasis` (`legal_obligation|public_interest`) e `personalDataFields` (allowlist) para
   publicação; sem base/allowlist, o service bloqueia. Dados sensíveis (saúde, criança, etc.) não
   são publicados; agregação/pseudonimização é da fonte, não do serializador.
8. **Ativação é runtime.** Fonte de módulo inativo não aparece nem exporta (ADR-034); o filtro é na
   leitura, via `ModuleStatus`.

## Justificativa — alternativas descartadas

| Alternativa | Por que não |
|---|---|
| Evento carregando linhas | Sem outbox/replay, perde/duplica/desordena; payload vira contrato e pode vazar PII. Exigiria event sourcing, outro produto. |
| Cópia central de todas as linhas | Segunda verdade e staleness; backfill e reconciliação permanentes. |
| Cada módulo com seu CSV | Duplicação de escaping, `ETag`, checksum, PII e testes; 20+ implementações divergentes. |
| `services/Export` + `services/OpenData` | Dois donos da mesma serialização; divisão artificial. |
| Service com registry | O registry nasce vazio: módulos registram antes do service. |
| CKAN/portal externo | Dependência e infra extra; o domínio cabe no V7 (ADR-018). |
| PDF como saída principal | PDF não é dado legível por máquina; perde filtro/CSV e adiciona dependência pesada. |

## Consequências

**Positivas**
- Um contrato para todos os módulos; o catálogo não copia dados.
- Determinismo: ordem explícita, checksum, `ETag`/304, testes de bytes.
- PII, licença e ativação validados em um só lugar.
- Backfill/reprocessamento triviais: a fonte é a verdade.
- O módulo dono continua dono do dado; sem import cruzado.

**Custos / riscos**
- Cada módulo que publica dado precisa implementar uma fonte e registrá-la.
- Consultas de fonte podem ser caras; snapshot/refresh entra quando houver volume (P12/P22).
- O contrato precisa de disciplina de schema/versionamento.
- Evento de invalidação, se usado, é otimização; nunca a única via.

## Exemplo mínimo

```php
// Módulo dono — Logic/Datasets/NoticiasDatasetSource.php
final readonly class NoticiasDatasetSource implements DatasetSource
{
    public function id(): string { return 'noticias.publicadas'; }
    public function module(): string { return 'Noticias'; }
    public function metadata(): DatasetMetadata { /* licença, updated_at, sem PII */ }
    public function columns(): array { /* id, titulo, publicado_em, categoria, tags, url */ }
    public function rows(): iterable { /* publicadas, ordem (published_at, id) */ }
    public function fingerprint(): string { /* count + max(published_at) + max(id) */ }
}

// ModuleProvider::register()
app(DatasetRegistry::class)->register(NoticiasDatasetSource::class);
```

```php
// Catálogo — nenhuma cópia de linha
$dataset->source = 'noticias.publicadas';
$export = $exporter->byId($dataset->source, 'csv'); // CSV/JSON/XML + ETag
```

## Verificação

- `DadosAbertos` curado continua com os mesmos bytes/`ETag` após a extração.
- Dataset gerado exporta CSV/JSON/XML; fonte inativa/inexistente/PII sem base → 404/validação.
- PII legalmente pública (ex.: remuneração) publica com `publicationBasis` e allowlist; sensível não.
- `DatasetIntegrationTest` falha se uma fonte não for registrada ou se o `id` for inválido.
- Evento, quando existir, não contém linhas; `open-data:refresh` reconstrói por `fingerprint()`.
- Nenhum módulo importa `Modules\Portal\DadosAbertos`; `tools/arch-check` verde.

## Emenda 2026-10-23 — `DadosAbertos` é o único owner da camada pública

- **Catálogo e serialização têm um dono:** `Portal/DadosAbertos` é dono do catálogo,
  `dados_abertos.datasets`, DCAT/INDA, `ETag` e CSV/JSON/XML. Nenhum outro módulo cria dataset,
  tabela paralela ou serializador público.
- **Módulo de domínio é dono do dado operacional:** P22–P24 (e demais) mantêm suas tabelas e
  expõem uma `DatasetSource`; o dado público é lido pelo motor e apresentado pelo catálogo.
- **Página de transparência é view/curadoria:** `/transparencia` não tem tabela de dados; usa
  páginas/blocos e links para datasets. Um módulo que só apresenta dado não ganha tabela própria.
- **Consequência:** qualquer novo dataset público passa por `DatasetSource` + `DadosAbertos`; a
  duplicação de export/catálogo falha no teste de contrato e no `tools/arch-check`.
