# P5.5 — Motor de dados abertos: fonte pull, registry, serialização e catálogo

| Campo | Valor |
|---|---|
| **Fase** | P5.5 — motor de dados abertos (F7+), pré-requisito de P6 |
| **`kd` épico** | `epic_00m0ycb6` |
| **`kd` fatias** | `task_0020mukv` (S1) · `task_00501neb` (S2) · `task_01ibqp3i` (S3) · `task_01wla9bo` (S4) · `task_00v33ddo` (S5) · `task_01vhqj5l` (S6) |
| **Âncoras** | `wiki/implementation-phases/P5.5-motor-dados-abertos.md` §3–§5 · `wiki/implementation-phases/TECNICO.md` §2–§6, §9–§11, §14 · ADR-049 (aceita; implementação conferida), ADR-001/007/013/015/017/018/024/030/034/035/040/041 · `src/core/Contracts/Ports/DatasetSource.php` · `src/core/Contracts/Ports/Data/**` · `src/core/Contracts/DatasetRegistry.php` · `src/core/Contracts/ModuleStatus.php` · `src/core/Internal/Datasets/**` · `src/core/Internal/ModuleDiscovery/ManifestModuleStatus.php` · `src/services/OpenData/**` · `src/modules/Portal/DadosAbertos/**` · `src/tests-e2e/{histories,tests}/33-*` |
| **Status da revisão** | ✅ **revisado** — S1–S6.2 concluídos; revisão paralela (6 agentes, PROTOCOLO v2.0) em 2026-10-26: 3 altos corrigidos com teste, 10 médios corrigidos, 1 médio parcial (throttle aplicado, custo do ETag adiado) e 2 médios adiados com risk no `kd`; `make check` 2× + `E2E=1` verdes |
| **Unidade de revisão** | fase (o motor, o service e o catálogo só fazem sentido juntos; a fonte de P6 é o primeiro consumidor) |

## 1. O que foi implementado

### S1.1/S1.2 — Core: porta, DTOs, registry e status de módulo (`task_0020mukv`)

- **Porta `Core\Contracts\Ports\DatasetSource`**: `id()`, `module()`, `metadata()`, `columns()`,
  `rows()` lazy (`iterable<array<string, scalar|null>>`), `fingerprint()`.
- **DTOs** em `Core\Contracts\Ports\Data`: `DatasetMetadata` (título/descrição/publisher/licença
  `CC-BY-4.0` por default/personal data/basis/allowlist/classificação/tema/tags/schemaVersion),
  `DatasetColumn` (`key`/`label`/tipo/nulabilidade/unidade), enums de tipo, periodicidade,
  classificação e base de publicação; validação de shape na borda (`InvalidDatasetData`).
- **`Core\Contracts\DatasetRegistry`** + `Core\Internal\Datasets\InMemoryDatasetRegistry`:
  resolução preguiçosa pelo container, `ksort` por id, id duplicado lança
  (`DuplicateDatasetId`), `find()`; construtor da fonte leve.
- **`Core\Contracts\ModuleStatus`** + `ManifestModuleStatus`: ativação na mesma fonte do
  ADR-034 (cache curto + DB, fallback ao manifesto, fail-open), sem query no boot.
- Binds singleton no `CoreServiceProvider`; testes de ordem/duplicidade/binding/ativação.

### S2.1/S2.2 — Service `OpenData` (`task_00501neb`)

- `src/services/OpenData/ServiceProvider.php` descobre-se por convenção e liga
  `Contracts\DatasetExporter` à Action concreta.
- `DatasetExporter`: `byId(string,string)`, `of(DatasetSource,string)`, `sources()` (só fontes
  ativas e publicáveis).
- `Support\DatasetSerializer`: **um** serializador CSV/JSON/XML extraído do exportador do módulo;
  ordem declarada de colunas/linhas, UTF-8, `\n`, sem BOM, CSV com neutralização de
  `= + - @ TAB CR` (números negativos preservados), JSON `{meta,columns,data}`, XML por
  `XMLWriter`, datas ISO 8601, decimais como string, SHA-256 do corpo e `ETag` forte.
- `Support\ArrayDatasetSource` + `Contracts\DatasetSourceFactory`: fonte curada em memória
  (o módulo constrói pela interface, não pela implementação — boundary ADR-013/015).
- `Actions\OpenDataExportDataset`: resolve → gate de módulo ativo → gate de PII
  (`containsPersonalData` exige base legal válida e allowlist não vazia; classificação sensível
  sempre recusada) → serializa; span `open-data.export` + `log_event` de mesmo nome.
- Exceções de domínio para fonte inexistente/módulo inativo/PII bloqueada/formato inválido.

### S3.1/S3.2 — `DadosAbertos` como catálogo de dataset gerado (`task_01ibqp3i`)

- Migração aditiva `source` (text, index) + `source_options` (jsonb default `{}`);
  `down()` não dropa dado (no-op registrado) — ADR-017.
- Model/DTO/FormRequest: `source`/`sourceOptions`, `isGenerated()`, validação da fonte contra
  `DatasetExporter::sources()`, `columns`/`rows` proibidos no dataset gerado; alternar
  curado↔gerado limpa o lado perdedor.
- `DadosAbertosExport` virou adaptador fino (curado → `of(ArrayDatasetSource)`; gerado →
  `byId(source)`), com **paridade de bytes/checksum/ETag** do curado provada por teste.
- `DadosAbertosPresentation` + `DadosAbertosPresent`: uma única leitura pública (metadados da
  fonte quando gerado); catálogo/show/API/DCAT/JSON-LD/sitemap usam a apresentação; fonte
  inexistente/inativa/PII-inválida → 404 público e erro de validação no CMS.
- CMS: select “Fonte” (só fontes ativas), aviso de linhas geradas, sem edição de
  `columns`/`rows`; estado vazio explicado; 4 locales.

### S4.1/S4.2 — Gate estrutural e receita (`task_01wla9bo`)

- `src/services/OpenData/Test/DatasetIntegrationTest.php`: varre `src/modules/*/*`
  (excluindo `Test/`, `Fixtures/`, `src/demonstration/`), falha fonte implementada e não
  registrada, valida id namespaced/único, `module()` no manifesto, licença, shape mínimo,
  PII/basis/allowlist, classificação sensível e a regra de import (só Core + o próprio módulo);
  **não executa `rows()`**.
- Receita de registro do módulo documentada em `MODULOS.md`/README:
  `app(DatasetRegistry::class)->register(XSource::class);` no `ModuleProvider::register()`.

### S5.1/S5.2 — ADR-049 e propagação (`task_00v33ddo`)

- ADR-049 confirmada com seção de aderência (artefatos entregues; snapshot/refresh adiado).
- P6/P9/P12/PNTP/MODULOS/README atualizados: P5.5 implementado; P9 não recria
  `services/Export`; dívida de snapshot/refresh registrada (`risk_01tammuc`).

### S6.1 — E2E do CMS e correções (`task_01vhqj5l`)

- `src/tests-e2e/tests/33-cms-dados-abertos-fonte.spec.ts` + história: formulário/lista com axe,
  branch condicional (com fonte → dataset gerado; sem fonte → estado vazio explicado), fonte
  obsoleta recusada com erro focado, e fluxo sem JavaScript para o dataset curado.
- Correções no CMS: `<label for>` do select, `aria-describedby`/`aria-invalid`, resumo de erros,
  mensagens específicas para fonte/colunas/linhas; 4 locales.
- S6.2 (rodar `make check E2E=1`, registrar contagens/tempos e fechar evidência) **pendente**.

## 2. Arquivos a revisar

**Core**
- `src/core/Contracts/Ports/DatasetSource.php`, `src/core/Contracts/Ports/Data/*.php`
- `src/core/Contracts/{DatasetRegistry,ModuleStatus}.php`
- `src/core/Internal/Datasets/{InMemoryDatasetRegistry.php,Exceptions/*}`
- `src/core/Internal/ModuleDiscovery/ManifestModuleStatus.php`
- `src/core/CoreServiceProvider.php`, `src/core/Contracts/Test/ContractsBindingTest.php`,
  `src/core/Contracts/Ports/README.md`

**Service OpenData**
- `src/services/OpenData/{ServiceProvider.php,Contracts/*,Support/*,Actions/*,Exceptions/*}`
- `src/services/OpenData/Test/*` (inclui `DatasetIntegrationTest.php` e fixtures)

**Módulo DadosAbertos**
- migração `2026_10_26_000000_add_source_to_dados_abertos_datasets_table.php`
- `Database/Models/DadosAbertosDataset.php`
- `Logic/{Actions/DadosAbertosExport.php,Actions/DadosAbertosSave.php,DTOs/*,Requests/DadosAbertosSaveRequest.php,Support/DadosAbertosPresent.php}`
- controllers `Index/Show/Export/ApiIndex` + `Cms/{Create,Edit}`
- `Seo/{DadosAbertosSchema.php,DadosAbertosSitemapSource.php}`
- `Layout/Views/{index,show,cms/form}.blade.php`, `Lang/{pt_BR,en,es,fr}/cms.php`
- testes `DadosAbertos{Test,CmsTest,ExportParityTest,GeneratedDatasetTest}.php` + fixtures/support

**E2E/docs**
- `src/tests-e2e/tests/33-cms-dados-abertos-fonte.spec.ts`,
  `src/tests-e2e/histories/33-dados-abertos-fonte.md`, `src/tests-e2e/README.md`,
  `wiki/specs/04-acessibilidade-ux.md`, `AGENTS.md` (33 histórias)
- `wiki/adr/ADR-049-motor-dados-abertos-fonte-pull.md`,
  `wiki/implementation-phases/{P5.5,P6,P9,P12,PNTP,MODULOS,README}.md`

## 3. Checklist D1–D14 (preenchido na revisão)

| Dimensão | Situação | Evidência / achados |
|---|---|---|
| D1 Simplicidade & PHP 8.4 | ✅ | `strict_types` em todos os arquivos, `final readonly`, DI por construtor, sem trait/abstract/generic/`Result`; gate e serviço concretos. |
| D2 Laravel 12 | ✅ | Provider por convenção, binds por contrato, sem `env()` fora de `config/`, sem closure de rota; migrações idempotentes. |
| D3 Determinismo | ✅ | `ksort`/ordenação explícita, rows lazy, bytes/checksum/`ETag` derivados do corpo, fixtures sem relógio/aleatório; `make check` 2×. |
| D4 Fronteiras/arquitetura | ✅ | Core não importa services/modules; service não importa modules; módulo só consome `Contracts`/`Contracts\Ports`; `DatasetSourceFactory` evita importar implementação. |
| D5 Dependências/licenças/CDN | ✅ | Nenhuma dependência nova (XMLWriter/`fputcsv`/`hash` nativos), zero CDN. |
| D6 CSS/Tailwind/daisyUI/Lucide | ✅ | Só classes Tailwind/daisyUI; sem `px`/inline/`!important`/hex nas views novas; ícones via `<x-ui::icon>`. |
| D7 HTML/a11y | ✅ | Um shell, `<main>` único, `label for`, `aria-describedby`/`aria-invalid`, resumo `role="alert"`, tabela com `caption`/`scope`, alvos ≥ 2.75rem; axe no e2e 33. |
| D8 i18n | ✅ | 4 locales com paridade e diacríticos; nenhuma string fixa; `route()`/`localized_url()`; metadados de fonte não são traduzíveis por natureza. |
| D9 Grep-friendly | ✅ | Token em classes/rotas/views/tabelas/testes; `open-data`/`dados-abertos`; exceção nominal documentada no gate. |
| D10 Dados/migrações/PII | ✅ (corrigido) | Migrações aditivas/idempotentes, `down()` não destrói; PII ganhou classificação/base/allowlist no dataset curado e CHECKs. |
| D11 Segurança/LGPD | ✅ (corrigido) | Gate de PII único (fonte e curado), base legal + allowlist, recusa de `sensivel`, CSV injection neutralizado, throttle no export, 404 em vez de 500. |
| D12 Observabilidade/auditoria | ✅ | `open-data.export` span+log com o mesmo nome; atributos escalares e sem PII; mutações do CMS cobertas pelo `GlobalAuditSubscriber`. |
| D13 Testes/evidência | ✅ | Red→green em todas as correções; suíte 1757 testes/30392 asserções; cobertura `src/` 94,19%; e2e 154 passed + 1 skip; axe/perf verdes. |
| D14 Documentação/rastreabilidade | ✅ | ADR-049 conferida, ficha/README/inventário/kd atualizados, `docs-check` verde; itens adiados registrados como risco no `kd`. |

## 4. Achados e disposição

> Sessão paralela de 2026-10-26 (6 revisores: Core, service, dados/export, UI/a11y/spec, gate/docs, segurança/LGPD).
> Todos os altos foram corrigidos com teste vermelho→verde; 10 médios corrigidos, 1 parcial e 2 adiados viraram risk/task no `kd`.

| # | Sev. | Achado | Disposição |
|---|---|---|---|
| 1 | alto | Fonte que saiu do catálogo era desvinculada em silêncio ao salvar a edição (`source=''`) | corrigido: opção obsoleta selecionada + validação `source.in`, registro intacto; teste de edição |
| 2 | alto | Export curado respondia 500 para slug iniciado por dígito (e cabeçalho numérico) | corrigido: id aceita dígito inicial, chaves normalizadas para string, `InvalidDatasetData` → 404; testes de paridade |
| 3 | alto | Dataset curado ignorava o gate de PII (sem classificação/base/allowlist) | corrigido: colunas `classification`/`contains_personal_data`/`publication_basis`/`personal_data_fields` + CHECKs, regra única no exporter, CMS e apresentação; testes |
| 4 | médio | Gate estrutural aceitava PII com `non_personal` (divergia do runtime) | corrigido: exige `legal_obligation\|public_interest`; testes |
| 5 | médio | Regras do gate sem teste que falhe; asserts tautológicos de licença | corrigido: fixtures para todas as regras, licença verificada semanticamente |
| 6 | médio | `source_options`/`metadata` gravados sem validação de forma | corrigido: rules de shape/escalares/limite + CHECK `jsonb_typeof='object'` |
| 7 | médio | Erro de `columns/rows` proibidos ficava escondido pelo próprio bloco | corrigido: bloco re-renderiza com erros, `aria-invalid`/`aria-describedby` |
| 8 | médio | Metadados editáveis do dataset gerado divergiam do público | corrigido: copiados da fonte no save, campos readonly no CMS |
| 9 | médio | Toggle de status publicava dataset gerado com fonte morta | corrigido: recusa com erro de sessão; teste |
| 10 | médio | `integer` truncava valor fracionário em silêncio | corrigido: só inteiro exato, senão `InvalidDatasetData` |
| 11 | médio | Export público sem throttle; `ETag` só depois de serializar | throttle `open-data` aplicado; custo do `ETag` adiado (`risk_00k3phqq`) |
| 12 | médio | Contagens do catálogo/API incluíam dataset oculto | adiado como risco (`risk_01s5867q`) |
| 13 | médio | Widget `dadosabertos.ultimos` ignorava a apresentação | corrigido: feed filtra pela apresentação; teste de ausência |
| 14 | médio | E2E criava dataset publicado (corrida com spec 12) e `created` tarde | corrigido: rascunho + flag imediata |
| 15 | médio | Publicar no save exige só `editar` (transversal com Notícias) | adiado como risco (`risk_00883m9b`) |
| 16 | baixo | `ManifestModuleStatus` com catch inalcançável e default divergente | corrigido: catch morto removido, fail-open é do `ModuleActivation` |
| 17 | baixo | Inventário `Contracts/README.md` sem os contratos novos | corrigido: três linhas |
| 18 | baixo | Docblocks invertidos nas fixtures e regra de import superdeclarada | corrigidos |
| 19 | baixo | Identificador sensível em camelCase não detectado | corrigido: normalização de chave |
| 20 | obs | `sources()` re-resolve o registry a cada chamada; `ExportedDataset::filename/etag` duplicados no adaptador | registrado como observação (sem impacto de correção hoje) |

## 5. Riscos residuais (aceitos)

| # | Risco | Impacto | Mitigação |
|---|---|---|---|
| 1 | Snapshot/`open-data:refresh` adiado (P12/P22) | export sempre relê a fonte; volume alto pode pesar | `fingerprint()` no contrato; dívida `risk_01tammuc` |
| 2 | Dataset curado × gerado com metadados duplicados | duas fontes de título/licença se a apresentação divergir | `DadosAbertosPresent` é a única leitura; paridade de bytes testada |
| 3 | Fonte de módulo inativo | dataset some/404 | `ModuleStatus` na resolução e no gate; teste dedicado |
| 4 | PII em fonte de dados abertos | exposição pública | gate no service + `DatasetIntegrationTest`; base legal/allowlist explícitas |
| 5 | E2E 33 hoje exercita o branch “sem fonte” | o branch “dataset gerado” só roda quando P6.S6 registrar `noticias.publicadas` | spec já cobre os dois; P6.S6 troca sem editar o teste |

## Notas da revisão

| Data | Revisor | Resultado | Observações |
|---|---|---|---|
| 2026-10-26 | revisão paralela (6 agentes: Core, OpenData, DadosAbertos, UI/a11y/e2e, gate/docs, segurança/LGPD) + correções | ✅ revisado | 20 achados consolidados: **3 altos** (fonte desvinculada em silêncio; 500 no export curado com slug numérico; PII do curado fora do gate), **10 médios corrigidos** (gate de PII, validação de opções, erro oculto, metadados divergentes, status, inteiro, throttle, widget, e2e, docs), **1 parcial** (throttle do export aplicado; ETag antes da serialização adiado) e **3 adiados** com risk no `kd` (contagens, custo de ETag/cache, aprovação no save). Evidência: suíte **1757 testes/30392 asserções**, cobertura **94,19%**, `make check` 2× (23 OK/0 FAIL) e `E2E=1` (25 OK/0 FAIL, e2e **154 passed + 1 skip**, axe/perf OK). Nada commitado pelo agente. |

## Evidência (2026-10-26)

- `vendor/bin/phpunit` (Core Datasets + ModuleStatus + OpenData + DadosAbertos): **134 testes/1404 asserções OK**; suíte completa **1757/30392**.
- Cobertura `src/`: **94,19%** (22 047/23 407; piso 90%).
- `make check` #1 **23 OK · 0 FAIL · 11 SKIP** (114,69 s) · #2 **23 OK · 0 FAIL · 11 SKIP** (111,57 s).
- `make check E2E=1` **25 OK · 0 FAIL · 9 SKIP**: e2e **154 passed + 1 skip** (3,2 min), `a11y` OK, `perf` OK.
- PHPStan max 0, Pint limpo, `arch-check`/`name-check`/`migration-check`/`vendor-check`/`docs-check` OK.
