# F5.I1.T3 — Core Web Vitals budget + precompressed

| Campo | Valor |
|---|---|
| **Fase** | F5 — Borda |
| **Issue** | F5.I1 — SEO e performance (plano 14) |
| **kd task** | `task_00gm927p` (`closed`) |
| **Âncora** | `wiki/implementation/05-borda.md` |
| **Status da revisão** | 🔧 com correções |

## O que foi implementado

- Plugin `concordia:precompress` (`vite.config.js`, `node:zlib`) gera `.br`/`.gz` **determinísticos**; Caddy serve via `precompressed`.
- `PerformanceBudgetTest` (variantes + tamanho brotli + anti-N+1) e E2E `11-performance` (**CLS ≤ 0,1** + recursos bloqueantes). LCP/INP ficam para RUM.

## Arquivos para revisar

- `vite.config.js`
- `tests/Feature/PerformanceBudgetTest.php`
- `src/tests-e2e/tests/11-performance.spec.ts`
- `src/tests-e2e/histories/11-performance.md`
- `tools/e2e.sh` (fix: repassar args no runner host)
- `Makefile` (alvo `perf`)
- `wiki/specs/16-edge-apache-whm.md`
- `wiki/plan/14-seo-performance.md`

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

- [ ] `.br`/`.gz` determinísticos (brotli q11 / gzip l9, mtime 0); acima do threshold.
- [ ] Nada de medição de parede no CI (LCP/INP = RUM).

## 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 `precompressed` listava **`zstd`**, que o build **nunca gera** — e os docs (spec 16, plano F5) prometiam `.br/.zst/.gz` ✗ → medido: `.br` (28 604 B, `Vary: Accept-Encoding`) e `.gz` saem do **disco**, mas `Accept-Encoding: zstd` respondia `Content-Encoding: zstd` vindo do **`encode` dinâmico** (CPU no caminho quente, contra o texto "o app nunca comprime em runtime") → `precompressed br gzip` no Caddyfile dev **e** na spec 16, com o texto explicitando que zstd fica no `encode` (só para o HTML dinâmico); `caddy validate` = *Valid configuration* e o `.br` segue idêntico após o reload (`fact_01y1ketd`); **B** (médio → `task_005ts67s`) o teste de **CLS** usa **`waitForTimeout(500)`** ✗ → em CI lento o shift do `font-display: swap` pode chegar **depois** da leitura e o CLS sai **0** (falso verde — gate que não morde); o determinístico é `document.fonts.ready` + 2 `rAF` (`risk_0109sibj`); **C** (médio) o **determinismo** era **afirmado sem gate** ✗ → medido: **dois builds** (o da sessão anterior + `make assets` agora, no `node:26-alpine`) dão o **mesmo md5 agregado** de todas as variantes (`182fa4bd…`, 6 `.br`) e o gzip sai com `MTIME = 0`/`OS = 3` → gate novo `the_precompressed_variants_carry_no_build_timestamp` (lê os bytes 4..7 do header), **vermelho** ao injetar um MTIME no arquivo (`fact_00ic0lae`); **D** (baixo) o orçamento "no máximo **1** folha de estilo" **quebra** quando o painel de suporte configura tema (o `/theme.css` é legítimo e o `head` passa a ter 2) (`risk_01chvd8p`); **E** (baixo) o plano F5 citava **`Alvo \`make perf\``** — alvo que **não existe** (o caminho é a etapa `perf` do `make check E2E=1`: `phpunit --filter PerformanceBudget` + o e2e `11-performance`) → doc corrigido junto com a linha do `precompressed` (`fact_00m0u0t4`); **F/G** (obs) o plugin fixa `outDir='public/build'` (duplicando `build.outDir` e o `buildDirectory` do laravel-vite-plugin) e **retorna em silêncio** se o diretório não existir — se um mudar, nada é comprimido e o build passa (só o gate denuncia); a ordem do `readdirSync` varia (inofensiva, cada arquivo é independente); e gerar `.zst` **é possível** (Node ≥ 22.15) mas não compensa — brotli é menor para texto e quem aceita zstd aceita br. **Verificado OK:** o `perf` do `make check` roda PHPUnit + o e2e; orçamento **brotli** por entrada (5 entradas, margem ~20%) + existência das duas variantes; home ≤ 20 queries e listagem com contagem **constante** de 1→10 itens (`assertSame` + guarda de que o log está ligado); e2e cobre **CLS ≤ 0,1** na home e em `/noticias`, **zero** script bloqueante (`module`/`defer`/`async`) e a **paridade métrica** do fallback de fonte (anti-CLS, ADR-011) com CSSOM em vez de `style="…"` (CSP/ADR-022); LCP/INP ficam **fora** do CI (RUM) — nenhuma medição de parede. `make check E2E=1`: 20 OK · 0 FAIL · 1 SKIP, e2e **91 passed**, cobertura **91,37%**. |

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