# F4.I3.T1 — WidgetRegistry no Core; embed permitido, import proibido

| Campo | Valor |
|---|---|
| **Fase** | F4 — Produto & CMS |
| **Issue** | F4.I3 — Widgets, banners e builder (plano 16) |
| **kd task** | `task_01k245tc` (`closed`) |
| **Âncora** | `wiki/implementation/04-produto-cms.md` |
| **Status da revisão** | 🔧 com correções |

## O que foi implementado

- Porta `Core\Contracts\Ports\WidgetSource` (`id`/`title`/`icon`/`slots`/`embeddable`/`render`); `WidgetRegistry` redesenhado (`forSlot`, `all` determinístico).
- Widget de módulo **inativo** não aparece (ADR-034); `<x-layout::widget id="…">`; widget de exemplo `demo.status`; testes `WidgetRegistryTest`, `ContractsBindingTest`.

## Arquivos para revisar

- `src/core/Contracts/Ports/WidgetSource.php`
- `src/core/Contracts/WidgetRegistry.php`
- `src/core/Internal/Widgets/InMemoryWidgetRegistry.php`
- `src/core/Internal/Widgets/Test/*`
- `src/layout/Components/widget.blade.php`
- `src/modules/Desenvolvimento/Demo/Widgets/DemoStatusWidget.php`
- `src/modules/Desenvolvimento/Demo/Layout/Views/widgets/demo-status.blade.php`
- `tests/Feature/WidgetRegistryTest.php`
- `src/core/Contracts/Test/ContractsBindingTest.php`

## 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

- [ ] Destino referencia só o `id` (import proibido — `make check-boundaries`).
- [ ] Ordem determinística; widget inativo oculto.

## 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** (**médio**) o fragmento do widget de exemplo fixava `id="demo-widget-status-title"` + `aria-labelledby` — e o widget pode ocupar **dois** slots (`main.right`/`home.aside`) e a validação do CMS **não** proíbe repetir o mesmo widget em colocações diferentes → medido ao vivo: renderizar duas vezes produz **dois ids iguais** e `aria-labelledby` ambíguo (WCAG 4.1.1; regra `duplicate-id-aria` do axe) e **nenhum gate via** (nenhuma página repete o widget, e a regra de id duplicado *puro* está deprecada no axe) → `id`/`aria-labelledby` removidos do fragmento (o `<h2>` já nomeia a seção no fluxo) + **gate novo** que renderiza **todo** widget registrado **duas vezes** e exige ids únicos — **vermelho** antes (`O widget 'demo.status' repete id ao aparecer duas vezes`) — `fact_00w3iihq`; **B** (baixo) o fragmento tinha `@props(['widget' => null])` **sem uso** (a fonte renderiza a view sem dado) → removido; **C** (baixo) o ponto específico “ordem determinística” estava **implementado** (`ksort`/`sort`) mas **não testado** com mais de um widget → teste novo registra ids fora de ordem (`zzz.ultimo`/`aaa.primeiro`) e exige `all()` e `forSlot()` ordenados — **vermelho** ao remover `ksort`/`sort` — `fact_018n0bu8`; **D** (obs) nenhum gate pega id duplicado *comum* nas páginas (o axe deprecou a regra pura) — um e2e nas telas-chave contando `[id]` fecharia a classe para qualquer view — `risk_017spml8`. **Verificado OK:** contrato da porta/registro (`register` por class-string resolvido pelo contêiner — sem `new`; `get`/`has`/`all`/`forSlot`); resiliência a módulo **inativo** (fail-open via `ModuleActivation`, teste com rollback de transação) e id desconhecido → nada renderizado; o módulo registra o próprio widget no `register()` com `app(WidgetRegistry::class)` (idioma de provider do framework); fronteira ADR-013 respeitada (nenhum import entre módulos — `arch-check` verde); ícone `lucide-trending-up` **vendorizado**; `FakeWidgetSource` usado por dois testes (não é código morto); a view do widget é fragmento sem shell. |

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