# F4.I2.T2 — Notícias ponta a ponta (unit + feature + e2e)

| Campo | Valor |
|---|---|
| **Fase** | F4 — Produto & CMS |
| **Issue** | F4.I2 — Módulo piloto + blueprint (plano 13) |
| **kd task** | `task_01ms8ypu` (`closed`) |
| **Âncora** | `wiki/implementation/04-produto-cms.md` |
| **Status da revisão** | 🔧 com correções |

## O que foi implementado

- `content` migrou de `text` para `jsonb` (AST) com backfill; portal renderiza pelo `ContentASTParser`.
- CRUD no CMS protegido por `Permission::authorize` (`noticias.{ver,editar,aprovar,arquivar}`); capa via `EditorMediaStore`.
- Seeder em AST; `NoticiaContentTest` (5), `NoticiaCmsTest` (8), E2E `08-cms-noticias` (com e sem JS).

## Arquivos para revisar

- `src/modules/Publicacoes/Noticias/**`
- `src/modules/Publicacoes/Noticias/Database/Migrations/2026_10_18_000000_convert_noticias_content_to_jsonb.php`
- `src/modules/Publicacoes/Noticias/Logic/Actions/{SaveNoticia,ChangeNoticiaStatus}.php`
- `src/modules/Publicacoes/Noticias/Logic/Controllers/**`
- `src/modules/Publicacoes/Noticias/Layout/Views/**`
- `src/modules/Publicacoes/Noticias/Test/{NoticiaContentTest,NoticiaCmsTest}.php`
- `src/tests-e2e/tests/08-cms-noticias.spec.ts`

## Pontos de revisão

### PHP 8.4
- [ ] `declare(strict_types=1)`; tipos nativos (union/enum/`readonly`); sem `mixed` desnecessário.
- [ ] Dependências **injetadas** (nenhum `new`/`app()`/`resolve()` no corpo); sem traits/abstract/generics.
- [ ] `match` em vez de cadeias de `if`; `?->`/`??`; sem código morto; PHPStan `max` limpo.

### Laravel 12
- [ ] Nativo primeiro (Eloquent casts/scopes, `FormRequest`, `Gate`, scheduler, events) — sem reimplementar.
- [ ] Sem closure em rota; resources/rotas nomeadas; `config()` só lê, `env()` só em `config/`.
- [ ] Migração **idempotente**; `down()` → `backup_*` (nunca `DROP`); sem `orderBy` cru quando `latest/oldest` serve.

### HTML semântico + a11y (eMAG/WCAG 2.2 AA)
- [ ] Um `<main>`, headings em nível correto, landmarks; só ARIA quando necessário.
- [ ] Alvos ≥ 24 px (≥ 44 px em ponteiro grosso); foco visível; rótulos em todos os campos; contraste (axe).
- [ ] Funciona **sem JS**; `sniffCSS` sem `fail`/`warn` fora do ledger.

### CSS moderno
- [ ] `oklch`, `clamp`, `rem`/`fr`/`dvh`/`%`; `@layer` (a11y por último); `@property`; subgrid com fallback.
- [ ] Sem `!important`, sem `style="…"` inline, sem hex/rgb (exceto justificado); sem CDN.

### Decisões do projeto (ADRs)
- [ ] Respeita a ADR do escopo (ver cabeçalho) e não escapa das demais (001, 002, 004, 013, 015…).
- [ ] Determinismo: relógio/id injetados, ordem explícita, dados idempotentes (ADR-001).

### i18n (ADR-012)
- [ ] Nenhuma string visível fixa; 4 locales com paridade de chaves; `route()`/`localized_url()` (nunca path literal).

### Observabilidade/segurança/testes
- [ ] Span + `log_event('{module}.{action}')` com atributos `scalar|null`, **sem PII** (ADR-024).
- [ ] Teste que falha sem a mudança (vermelho→verde) + evidência no `kd`; `make quality` 2×.

### Específicos desta task

- [ ] Migração idempotente + backfill determinístico; `down()` → `backup_*`.
- [ ] Nenhum HTML no banco; render semântico; capa com `width/height` (CLS 0).
- [ ] `Rule::unique` com schema → closure `DB::table` (o ponto não é conexão).

## Comandos úteis

```bash
make quality            # lint + PHPStan max + gates + testes
make e2e                # Playwright + axe (se a task toca UI)
make check-boundaries   # fronteiras core/services/layout/modules
```

---

> Revisar contra `AGENTS.md`, `wiki/adr/` e o plano citado. Registrar achados no `kd` (`kd write --anchor`) e, se houver correção, teste de regressão.

## Notas da revisão

| Data | Revisor | Resultado | Observações |
|---|---|---|---|
| 2026-10-18 | agente | 🔧 | **A** (**alto**) o link do **título** no card de `/noticias` tinha **22px** quando o título cabe em **uma linha** — abaixo do mínimo de 24px (SC 2.5.8) e muito abaixo dos 44px da casa em ponteiro grosso — e **nenhum gate via**, porque todos os títulos semeados quebram em duas linhas; apareceu quando uma notícia de título curto entrou na base (o e2e `06-alvos-minimos` falhou em desktop **e** mobile com `a.transition-colors… "Probe de capa" width:209 height:22`) → `inline-block min-h-6 pointer-coarse:min-h-11` + **notícia de título curto na semente** (o gate fica honesto em banco recém-populado) — **vermelho→verde** — `fact_00fve5ex`; **B** (médio) as rotas CMS **de módulo** não tinham `auth`: medido ao vivo, `cms.localhost/noticias` → **403** enquanto a dashboard → 302 para o login (ADR-037) — o e2e nunca vê porque sempre faz login antes → `AreaRegistrar` passou a envolver **rotas de módulo** em `auth` (serviços declaram o próprio; a primeira versão, mais ampla, tornou `cms/robots.txt` privado e o `SeoRootFilesCoverageTest` pegou) + teste novo em `CmsAuthTest` (**vermelho** antes) — `fact_019nrozg`; **C** (médio) a **capa** não tinha teste nenhum — `SaveNoticia` estava em **28,57% de métodos / 71,42% de linhas** (o pior do módulo) e a semente nunca define capa → exatamente a classe do bug de T2 (caminho de mídia sem gate) → teste novo com o serviço real + `Storage::fake('s3')` (payload, dimensões e `<picture>` no portal; **vermelho** ao desligar a ramificação da capa) e o e2e `08` passou a **subir capa real** e exigir `naturalWidth > 0` — `fact_01wl0vwn`; **D** (baixo) `NoticiasWebController` recebia `Request $request` e nunca usava (injeção morta) → removido; **E** (baixo) `SeedersCoverageTest` fixava as contagens (4/3/2) e quebrava a cada mudança de semente → reescrito para comparar a **primeira** passada com a segunda (+ não-vazio), provando idempotência sem acoplar ao tamanho — `fact_017v9jwm`; **F–N** (obs) fallbacks do model com `now()`/`Str::random(6)` (sinal de ADR-001; cobertos por `ModelsCoverageTest`) — `risk_0115bt4r`; `scopePublicadas` em vez de `#[Scope]` (padrão da casa); `{state}` de status sem `->where()`; `backfill()` sem chunking; `lock_timeout` sem `RESET` (padrão da casa); sem `throttle:` nas escritas do CMS; FQNs em Blade (`task_01o3tbg3`); arquivar sem confirmação (soft delete). **Verificado OK:** migração `text→jsonb` idempotente (marcador `content_legacy`), backfill determinístico, `down()` → `content_backup_<ts>` + restauração do texto, `CHECK … NOT VALID` + `VALIDATE`; **nenhum HTML no banco** (AST + `LocaleCast`) nem na saída (parser escapa — `NoticiaContentTest`); `<picture>` com `width`/`height` só quando > 0 (CLS 0); o `Rule::unique` com schema resolvido por closure `DB::table`; permissão **negação por padrão** por ação (`ver/editar/aprovar/arquivar`); span + `log_event('noticias.{ação}')` nas duas Actions; semente em `src/demonstration/` (ADR-041) com conteúdo em AST. |

> Resultado: `✅ revisado` · `🔧 com correções` · `⏭️ delegado` (com justificativa).
