# P6 — Editorial: paridade de Notícias

| Campo | Valor |
|---|---|
| Programa | F7+ — operação, estrutura e conteúdo |
| Status | `planejado` |
| Depende de | P5, P5.5 |
| Habilita | P7 (busca local), P8/P9 (padrões editoriais) |
| Tamanho | **G** (4–5 sessões) |
| ADRs | ADR-012, ADR-017, ADR-026, ADR-027, ADR-028, ADR-033, ADR-035, ADR-049 |

## 1. Objetivo

Fechar as lacunas editoriais verificadas no módulo Notícias — categoria fixa no model, ausência de
tags, galeria ordenada, super-destaque único, preview de rascunho e prev/next por `OFFSET` — sem
reescrever o núcleo já entregue (portal, CMS, API, export, sitemap, `NewsArticle`, 4 locales, AST).

**Resultado observável:** o editor cria categorias e tags, ordena a galeria, define **um** super-
destaque, pré-visualiza o rascunho por URL assinada e navega prev/next sem pular itens quando o
conteúdo muda; quem não tem `noticias.aprovar` não publica direto.

## 2. Estado atual

- `src/modules/Publicacoes/Noticias/**` com portal, CMS, API, export, sitemap, `NewsArticle`,
  4 locales e conteúdo em AST/`jsonb`.
- `Noticia::CATEGORIES` é uma lista fixa no model; `category` é `text` default `geral`
  (`Database/Models/Noticia.php`; migração `..._create_noticias_table.php`).
- Status apenas `rascunho|publicado` (`ChangeNoticiaStatus`, `SaveNoticiaRequest`); o formulário
  aceita `status` do request.
- `published_at` já define agendamento implícito (scope `publicadas`).
- Sem tags, sem galeria ordenada, sem super-destaque, sem preview por token, prev/next por offset.

## 3. Escopo fechado

### S1 — Categorias como tabela

- Tabela `noticias.noticias_categorias`: `id`, `slug` único (case-insensitive), `nome` jsonb
  (LocaleCast), `parent_id` nullable (até 2 níveis), `position`, `active`, timestamps;
  `down()` → `backup_*`.
- Backfill: uma categoria por valor distinto de `noticias.category` (os 8 atuais), preservando o
  slug; criar `category_id` nullable → backfill → `NOT NULL` + FK `restrictOnDelete`; a coluna
  antiga deixa de ser escrita/lida pelo model e pelo formulário (uma única verdade). O
  `down()` preserva a tabela e a coluna antiga (nunca drop).
- CRUD no CMS: listar/criar/editar/desativar; não excluir categoria com notícias (bloqueio);
  desativar some do portal e do seletor, mas preserva vínculos.
- Portal: `/noticias/categoria/{slug}` (paginada, canonical, sitemap); breadcrumb; 404 se inativa.
- `Noticia` ganha `category()` (`belongsTo`); remove `CATEGORIES` do model; a lista vira dado.
- 4 locales para o rótulo padrão das categorias de referência (seed idempotente, ADR-041/017).

### S2 — Tags

- Tabelas `noticias_tags` (`id`, `slug` único case-insensitive, `nome` jsonb) e pivot
  `noticias_noticia_tag` (`noticia_id`, `tag_id`, PK composta, cascade).
- Autocomplete simples (server-side, sem JS obrigatório): select múltiplo com busca; criação inline
  de tag com slug gerado e validado (não sobrescreve slug existente; sufixo determinístico só em
  colisão real).
- Portal: `/noticias/tag/{slug}` paginada, canonical, sitemap; tag sem notícia publicada → 404.
- Limite de tags por notícia (ex.: 10) no `FormRequest`; normalização (trim, lowercase, sem
  duplicatas) antes de salvar.
- SEO: tags não geram `@graph` próprio; entram em `keywords`/`about` quando fizer sentido.

### S3 — Galeria ordenada

- Tabela `noticias_galeria`: `id`, `noticia_id` FK cascade, `media_id` (id do catálogo Media/
  Gallery), `alt` obrigatório, `position`, timestamps; unique `(noticia_id, media_id)`.
- Reordenar por botões (teclado) + drag opcional; `position` renumerada de 1..N na transação.
- Render no portal: `<x-service-media::picture>` (AVIF/WebP, `width`/`height`, `loading="lazy"`),
  `<figure>/<figcaption>`; `alt` sempre; sem lightbox que dependa de JS (link para a imagem é o
  fallback). Não usar HTML/CSS no banco.
- Validação: `media_id` existe no catálogo; `alt` `required|max:160`; no máximo N imagens
  (config); remover da galeria não apaga a mídia do catálogo.

### S4 — Destaque, super-destaque e workflow de aprovação

- Colunas `destaque boolean default false`, `super_destaque boolean default false`; índice único
  parcial `ON noticias.noticias ((true)) WHERE super_destaque AND deleted_at IS NULL` (um único
  super-destaque) — a criação do segundo falha com 422 e mensagem, nunca 500.
- Workflow: `rascunho → pendente → publicado | rejeitado`; `rejeitado → rascunho`;
  `publicado → rascunho` (despublicar). `ChangeNoticiaStatus` valida a transição, grava
  `reviewer_id`, `reviewed_at`, `rejection_reason` (obrigatório ao rejeitar) e audita.
- `SaveNoticiaRequest`: não aceita `publicado` de quem não tem `noticias.aprovar`; salvar como
  `rascunho`/`pendente`; `publicado` só pela ação de aprovação/publicação. `published_at` futuro =
  agendado (scope já respeita `<= now()`).
- `NoticiaIndex` ganha filtros por status/categoria/tag/destaque; ações de aprovar/rejeitar/
  publicar/despublicar com `POST` + CSRF + confirmação; menu mostra só o que a permissão permite.
- Auditoria: `noticias.submitted`, `noticias.approved`, `noticias.rejected`, `noticias.published`,
  `noticias.unpublished` com before/after de status (sem conteúdo no payload).

### S5 — Preview e navegação determinística

- Preview por `temporarySignedRoute('noticias.preview', now()->addMinutes(30), ['noticia' => $id])`
  (ou `draft_token` por notícia), validado no controller; `X-Robots-Tag: noindex, nofollow`;
  nenhuma rota pública de rascunho; link “Pré-visualizar” no CMS.
- Prev/next por **keyset** `(COALESCE(published_at, created_at), id)`, não `OFFSET`; consulta
  separada para anterior/próximo, com índice parcial; empate resolvido por `id`.
- Cache do portal invalidado ao publicar/despublicar/editar; `ETag`/304 preservado.
- Testes: preview expirado 403; prev/next após publicar/despublicar; dois requests com cache.

### S6 — Integração com o motor de dados abertos (piloto)

- Implementar `NoticiasDatasetSource` no módulo dono (`Logic/Datasets/`), com:
  - `id()` `noticias.publicadas`; `module()` `Noticias`;
  - colunas `id`, `titulo`, `resumo`, `publicado_em`, `categoria`, `url` (valores escalares);
    tags, quando necessárias, viram string única (`tags` separadas por `|`) ou coluna fora do MVP;
  - apenas notícias publicadas (`published_at <= now()`), ordem determinística por
    `(published_at, id)`; **sem** e-mail de autor, IP, rascunho ou conteúdo de rascunho;
  - `metadata()` com título/descrição/licença `CC-BY-4.0`, periodicidade e `updated_at`;
  - `fingerprint()` por contagem + `max(published_at)` + `max(id)`.
- Registrar no `ModuleProvider::register()`:
  `app(DatasetRegistry::class)->register(NoticiasDatasetSource::class);` — sem importar
  `DadosAbertos` nem outro módulo.
- Criar via CMS um dataset **gerado** com `source = noticias.publicadas`; a página de catálogo,
  o DCAT/JSON-LD e o export CSV/JSON/XML passam a usar a fonte; nenhuma linha é copiada para
  `dados_abertos.datasets`.
- Testes: fonte registrada e visível no CMS; export com `ETag`/checksum; notícia despublicada sai
  do dataset; módulo `Noticias` inativo → dataset 404; nenhum campo PII.
- Atualizar `MODULOS.md` (linha de Notícias) e `PNTP.md` (o dataset de notícias é o exemplo do
  motor; demais módulos seguem a mesma receita).

### S7 — Fechamento

- E2E: criar categoria/tag, publicar com aprovação, galeria ordenada, super-destaque único,
  preview, prev/next; dataset `noticias.publicadas` exporta e despublicada sai.
- `make check` 2× + `E2E=1`; axe; export/sitemap atualizados; cobertura ≥90%.
- Atualizar `INVENTARIO.md` e o item `modulo-noticias.md` (mover o que fechou).

## 4. Fora de escopo

- Busca full-text local (P7); o filtro atual por `ILIKE` continua até lá.
- Editor de conteúdo novo (TipTap já existe); nenhum nó HTML livre.
- Múltiplos autores/coautoria, fluxo editorial com várias etapas além de aprovar/rejeitar.
- Comentários, reações, newsletter.
- Importação de notícias do legado (ADR-042).

## 5. Fatias de sessão

| Sessão | Entrega | Prova | Depende |
|---|---|---|---|
| **P6.S1** | categorias + backfill + CRUD + rota portal | 8 categorias migradas; inativa 404; sem dupla verdade | P5 |
| **P6.S2** | tags + pivot + rotas + limites | slug único; tag vazia 404; duplicata normalizada | S1 |
| **P6.S3** | galeria ordenada + render `<picture>` | ordem; alt obrigatório; mídia não apagada | S1 |
| **P6.S4** | destaque/super-destaque + workflow + permissões | único super; sem `aprovar` não publica; transições; auditoria | S1 |
| **P6.S5** | preview assinado + prev/next keyset + cache | expirado 403; keyset correto; dois requests | S4 |
| **P6.S6** | `NoticiasDatasetSource` + dataset gerado + export | registrada; despublicada sai; inativa 404; sem PII | S2–S5, P5.5 |
| **P6.S7** | E2E, docs, 2× check | evidência no `kd` | S1–S6 |

## 6. Contratos e dados

- **Tabelas:** `noticias_categorias`, `noticias_tags`, `noticias_noticia_tag`, `noticias_galeria`;
  colunas `destaque`, `super_destaque`, `reviewer_id`, `reviewed_at`, `rejection_reason`.
- **Rotas:** `noticias.cms.categorias.*`, `noticias.cms.tags.*`, `noticias.cms.aprovar`,
  `noticias.cms.rejeitar`, `noticias.preview`, `noticias.portal.categoria`, `noticias.portal.tag`.
- **Permissões:** `noticias.ver/editar/aprovar/arquivar` (já declaradas; `aprovar` passa a valer).
- **Eventos:** `noticias.submitted/approved/rejected/published/unpublished`.
- **Dados abertos:** `NoticiasDatasetSource` (`noticias.publicadas`) registrado em
  `ModuleProvider::register()`; dataset gerado no catálogo; export via `Services\OpenData`.
- **Cache:** tags de portal/listagens por rota; invalidação em publicação/edição.

## 7. DoD

- [ ] Categoria é tabela (sem lista fixa no model) e o backfill cobre os 8 valores atuais.
- [ ] Tags e galeria ordenada funcionam no CMS e no portal, com `alt` obrigatório.
- [ ] Um único super-destaque garantido por índice parcial; segundo → 422.
- [ ] Quem não tem `noticias.aprovar` não publica; transições inválidas → 422; tudo auditado.
- [ ] Preview assinado expira e é `noindex`; prev/next por keyset, sem `OFFSET`.
- [ ] `noticias.publicadas` registrado e exportando via `Services\OpenData`; despublicada sai;
      módulo inativo → 404; sem PII; `DatasetIntegrationTest` verde.
- [ ] Sitemap/canonical/`NewsArticle` continuam corretos; 4 locales.
- [ ] `make check` 2× + `E2E=1`; cobertura ≥90%; PHPStan max 0.

## Fechamento — ficha de revisão

> **Obrigatório antes de fechar:** criar `_to_revision/P6-editorial-noticias.md` e seguir
> `_to_revision/README.md` + `_to_revision/PROTOCOLO.md` (v2.0). A ficha não é gate de build; é a
> entrada da fila de revisão profunda.

- Cabeçalho: fase, `kd` épico/tasks/slices, âncoras (este arquivo, ADRs e código) e status
  `⬜ pendente` → `🟡 em revisão` → `✅ revisado` / `🔧 com correções`.
- Conteúdo: o que foi implementado, arquivos/superfícies a revisar, checklist D1–D14 aplicável,
  riscos e observações.
- Fechamento: preencher “Notas da revisão” com data/revisor/resultado e achados (`kd write
  --anchor`), atualizar a fila em `_to_revision/README.md` e `_to_revision/_inventario.md`.
- Achado com correção exige teste vermelho→verde; a fase só é `closed` no `kd` com a ficha
  registrada.

## 8. Riscos

| Risco | Mitigação |
|---|---|
| Backfill de categoria perder vínculo | backfill em transação + relatório de contagem; coluna antiga preservada |
| Duas verdades categoria texto × FK | model/formulário só usam `category_id`; teste que rejeita escrita na coluna antiga |
| Super-destaque concorrente | índice único parcial + tratamento de violação (422) |
| Preview vazar rascunho | URL assinada com TTL curto + `noindex` + teste de expiração |
| Prev/next pular itens | keyset `(published_at, id)` + teste com publicação/despublicação |
| Galeria apagar mídia do catálogo | remover só a linha da galeria; teste de mídia preservada |

## 9. Retomada

```bash
kd prime
kd ask "notícias categorias tags galeria super-destaque preview"
kd task list --ready
```

Ler: `src/modules/Publicacoes/Noticias/**` → migração/modelo atuais →
`lotep-v2/publicacoes-noticias/09-APROVEITAMENTO-V7.md` e `publicacoes-noticias/09` (contratos) →
`wiki/backlog/modulo-noticias.md` (paridade).
