# POST_REVIEW — quadro geral da fase inicial (F0 → F6)

> **Origem:** `_to_revision/PROTOCOLO.md` v2.0 + as 54 fichas de revisão (`_to_revision/*.md`) +
> as 105 notas `risk_*` e as 31 tasks ativas do `kd`.
> **Data da análise:** 2026-10-20 · **Método:** cada item foi conferido contra o código atual
> (arquivo:linha). Nada foi alterado; nenhum commit foi feito (regra de ferro §0).
> **Legenda de veredito:**
> `CONFIRMADO` = o problema existe hoje · `PARCIAL` = existe em parte / cenário específico ·
> `RESOLVIDO` = já corrigido (a nota é histórica — fechar no `kd`) · `ACEITO` = risco/decisão
> consciente (não fazer) · `INFRA` = só fecha na conta/cPanel real (`humano`) · `OBS` = observação
> sem ação obrigatória.

## 0. Sumário executivo

| Classe | Qtde | Consequência |
|---|---:|---|
| **CONFIRMADO / PARCIAL (riscos)** | 66 | Entram no plano de correção (abaixo) |
| **CONFIRMADO (tasks ativas)** | 25 | Já estão no `kd`; executar/fechar |
| **RESOLVIDO** | 29 | Só higiene: fechar as notas/tasks no `kd` (26 riscos + 3 tasks) |
| **ACEITO / OBS** | 12 | Nenhuma ação de código |
| **INFRA / humano** | 3 | Bloqueiam o fechamento de F1/F6 |
| **Guarda-chuva** | 1 | `task_004xqkid` (F1.I7) — fecha quando o DOD2 fechar |
| **Total revisado** | 105 riscos + 31 tasks | 136 itens |

**Leitura curta:** o código está em estado melhor do que o `kd` sugere (29 itens já morreram),
mas **91 pontos seguem vivos** — sendo **8 de prioridade P0** (segurança/correção/go-live) e
**~30 de P1** (SEO/cache/mídia/a11y). O fechamento da fase inicial depende de **(a)** 3 itens de
infraestrutura humana, **(b)** o lote P0/P1, **(c)** saneamento do `kd` e **(d)** o backlog de
qualidade (paratest, ADR-039).

---

## 1. Prioridade P0 — bloqueia go-live / correção/segurança

| ID | Item | Veredito | Evidência atual | Ação |
|---|---|---|---|---|
| `task_00ja4pzi` | AutoSSL/`ProxyPass`/domínio aplicados no WHM + validar borda | **INFRA** | runbook e spec 16 prontos; falta a conta real | Humano/infra: executar `wiki/runbooks/deploy-e-recuperacao.md` e o `06-deploy-migracao.md` (DoD) |
| `task_00oifzp1` | DOD2 clone limpo → `up_prod.sh` → `/up` 200 | **INFRA** | só fecha a partir de clone (exige commit do humano — §0) | Humano: commit + clone limpo + rodar |
| `risk_01jcnlfd` | `.gitlab-ci.yml` nunca rodou em runner real | **INFRA** | pipeline existe, `DeployPipelineTest` cobre o texto | Humano: primeira execução no runner `deploy` |
| `task_00inehom` / `risk_01shfi4j` | **Não existia `src/core/Lang`**: `__('validation.required')` devolvia a chave crua em todo formulário | **CONFIRMADO** | `ls lang` → ausente; `config/app.php` sem fallback traduzido | Criar `src/core/Lang/pt_BR/{validation,auth,passwords}.php` (framework no Core, ADR-003/012) + gate que falha se a chave não resolver |
| `task_00lzwcrb` / `risk_01oagqo4` | Convidado em admin surface de service recebe **403** em vez de ir ao login | **CONFIRMADO** | `AreaRegistrar` só envolve rotas de **módulo** em `auth`; services declaram o próprio (linha 167) | Envolver também as rotas de service em `auth` (ou middleware equivalente) + teste 302 |
| `task_01htnzob` | Reentrega da fila de mídia sobrescreve `concluido` com `falha` | **CONFIRMADO** | `ProcessMediaUpload::failed()` e o `catch` gravam `FALHA` sem checar estado | Guardar estado terminal (`CONCLUIDO`/`CANCELADO`) e usar injeção no `failed()` (sem `app()`) |
| `task_01gpf5qd` / `task_00zo3k5i` | `Gallery` (service) importa **implementação** de `Media` (`Models`, `Enums`, `Jobs`) | **CONFIRMADO** | `src/services/Gallery/**` importa `Services\Media\Models\*`; `arch-check` aceita `services→services` integralmente | Mover para contrato/DTO do Media + endurecer `arch-check` (services só `Contracts/DTOs/Enums`) |
| `task_012jlp5n` / `risk_00amq05u` | Coluna `metadata` (jsonb) é gravada e **nunca lida**; `dcat:contactPoint` semeado se perde | **CONFIRMADO** | único uso é save/cast/teste; o gerador DCAT não lê `metadata` | Ler `metadata` no registro DCAT/INDA + teste |
| `task_00vimpys` / `risk_01npkvew` | Predicado de publicação copiado em **5 pontos**; `isPublished()` ignora `published_at <= now()` | **CONFIRMADO** | 4 controllers + export + API repetem `status='publicado'`; model só checa `!== null` | Um scope único (`publicados()`) + `isPublished()` coerente + testes |

---

## 2. Prioridade P1 — correção funcional / cache / SEO / mídia / a11y

| ID | Item | Veredito | Evidência atual | Ação |
|---|---|---|---|---|
| `task_01btnk4j` / `risk_01111rdx` | `hreflang`/`canonical` apontam para 404 quando o caminho não existe em todos os locales | **CONFIRMADO** | `Locales::alternatesForPath()` (165–182) gera os 4 locales sem checar rota | Filtrar por rota existente (ou `Route::has`) + `x-default` |
| `risk_00jfeg7s` | `canonical`/`hreflang` não carregam paginação (`?page=2` canoniza para a 1) | **CONFIRMADO** | `BuildSeoGraph` usa `$data->canonical ?? baseUrl.'/'` sem query | Incluir página + `rel=prev/next` |
| `risk_015naqub` | `theme.css` compara `If-None-Match` por igualdade estrita; ignora `W/` e múltiplos ETags | **CONFIRMADO** | `ThemeStylesheetController:30` | Parser de `If-None-Match` (coerente com o resto) |
| `risk_00qafbph` | `/theme.css` com `max-age=3600` e URL fixa: mudança no painel pode levar 1 h | **CONFIRMADO** | `ThemeStylesheetController:36` | Cache-busting por ETag/URL versionada ou `no-cache`+revalidação |
| `risk_00rmgqoq` | `ThemeResolver::defaults()` devolve `''` e pode emitir `--tenant-x: ;` | **CONFIRMADO** | `defaults()` linhas 52–61 | Descartar vazio antes de emitir + teste |
| `risk_01l9ajqq` | Shell liga `/theme.css` mesmo com todos os overrides inválidos | **CONFIRMADO** | `portal.blade.php:61` testa `filled(config(...))`, não validade | Só emitir quando houver ≥1 override aceito pelo schema |
| `risk_01chvd8p` | Orçamento e2e "≤1 stylesheet" quebra quando o painel configura tema | **CONFIRMADO** | `11-performance.spec.ts:57` | Contar por cenário (sem/com tema) ou excluir `theme.css` |
| `task_01v75xex` | `gallery_media` sem índice único `(module, path)` — idempotência não provada por constraint | **CONFIRMADO** | migration só tem índice comum `module, is_shared` | Migração aditiva com `UNIQUE` + `ON CONFLICT` no commit |
| `task_01kvi19b` / `risk_00znkqy1` | Manifest PWA inventa 192/512 PNG para o mesmo logo | **CONFIRMADO** | `SeoRootFilesController:78-79` | Ícones reais do Media (ou remover entradas falsas) |
| `risk_00cibbhu` | Salvar como rascunho **apaga** `published_at`; a action de status preserva | **CONFIRMADO** | `DadosAbertosSave::resolvePublishedAt()` retorna `null` p/ rascunho | Regra única de transição de status |
| `risk_00swoasy` | Colunas duplicadas no dataset não são recusadas (JSON colide, CSV perde) | **CONFIRMADO** | `DadosAbertosSaveRequest` só valida slug único | `Rule` de unicidade de colunas + teste |
| `risk_01v8pm0b` | `{state}` da rota de status não restringido — qualquer valor cai em rascunho | **CONFIRMADO** | `Noticias/Routes/cms.php:28` sem `whereIn` | `->whereIn('state', ['publicar','despublicar'])` |
| `task_00wmwv84` | e2e não idempotente: notícias `e2e-*` com `Date.now()` sem limpeza | **CONFIRMADO** | `08-cms-noticias.spec.ts:20,90` | Cleanup determinístico (seed/factory + teardown) |
| `risk_00j25aw6` | CMS importa `a11y.css` mas não emite `data-a11y-*` — camada inerte no painel | **CONFIRMADO** | `src/layout/Components/cms.blade.php` sem `data-a11y-*` | Emitir as preferências no shell CMS (server-side, sem FOUC) |
| `risk_00lbc67g` | Preferência de movimento reduzido é inalcançável (ninguém grava o cookie) | **CONFIRMADO** | `A11yPreferences` lê `a11y_motion`; nenhum controle o escreve | Controle na barra a11y + persistência server-side |
| `risk_01qtnjqe` | Modo dislexia remove `'Atkinson Fallback'` (fallback calibrado não é usado) | **CONFIRMADO** | `a11y.css:28-32` | Manter a cadeia calibrada no modo dislexia |
| `risk_006noxdx` | `Ctrl+K` global dispara dentro do editor rico | **CONFIRMADO** | `Cms/Assets/js/app.js:110` | Ignorar quando o alvo é `contenteditable`/TipTap |
| `risk_00hi43ht` | `hasPlacements` sem span e TTL de perfis fixo em 300 s | **CONFIRMADO** | `ResolveLayoutPlacements` 55–59 e `Cache::remember(..., 300)` | Span + TTL em `config('layout.*')` |
| `risk_003a5995` | `orbital` mapeia `--i` só para os 6 primeiros itens; do 7º usa default | **CONFIRMADO** | `Cms/Assets/css/shell.css:364-379` | Gerar `--i` por contagem (loop limitado) ou fallback |
| `risk_01dxalee` | `z-index` cru (30/35/40/45) fora da allowlist `--z-*` | **CONFIRMADO** | `Cms/Assets/css/shell.css:17,179,200,313` | Criar tokens `--z-*` (ou justificar no DESIGN) |
| `risk_00jse6xz` | `PresetConfig::label` é pt_BR fixo e nunca lido | **CONFIRMADO** | `PresetConfig:13`; nenhum consumidor | Remover campo morto ou virar chave i18n se o CMS exibir presets |
| `risk_006skhy0` | Card do CMS tem `<header>` sem heading (`h2`) | **CONFIRMADO** | `Cms/components/card.blade.php:5` | `<h2>`/`<h3>` conforme nível + teste |
| `risk_00urwtfp` | Só o dashboard recebe `aria-current`; a tela do módulo não marca o menu | **CONFIRMADO** | `Cms/partials/nav-items.blade.php:18` | `request()->routeIs($item['route'].'*')` |
| `risk_009tjwbb` | Admin surface de service não aparece na navegação do painel | **CONFIRMADO** | `PanelMenu` só lê módulos do manifesto | Registrar entrada no manifesto/nav (ou documentar URL) |
| `risk_0061h2mb` | `cover.blade.php` usa `<img>` cru sem `width/height/decoding` (ADR-026) | **CONFIRMADO** | `Noticias/Layout/Views/partials/cover.blade.php` | Trocar por `<x-service-media::picture>` ou atributos completos |
| `task_01doc455` / `risk_0061h2mb` | (duplicado) | **CONFIRMADO** | — | Fechar duplicidade no `kd` |
| `risk_01cl9z5i` | Coluna `status` do redirect nunca é lida (middleware sempre devolve 301) | **CONFIRMADO** | `ResolveSeoRedirect::map()` só guarda `to_path` | Ler `status` no mapa e devolver 301/302 |
| `risk_01qke8zj` | Gate de borda valida **texto** do arquivo; container com Caddyfile antigo passa e só o e2e pega | **CONFIRMADO** | `tools/check.sh` não reinicia/valida o container | Comparar config efetiva do container (ou `caddy validate` + restart no prep) |
| `risk_011cjiyl` | Breadcrumb do painel repete o título da página sem hierarquia | **CONFIRMADO** | `src/layout/Components/cms.blade.php` | Breadcrumb com níveis reais (ou omitir a repetição) |

---

## 3. Prioridade P2 — determinismo, dados, arquitetura, gates

| ID | Item | Veredito | Evidência atual | Ação |
|---|---|---|---|---|
| `task_005ts67s` / `risk_0109sibj` | Teste de CLS usa `waitForTimeout(500)` → falso verde | **CONFIRMADO** | `11-performance.spec.ts:40,63` | `document.fonts.ready` + 2 `rAF` |
| `risk_0115bt4r` | `Noticia` usa `now()` e `Str::random(6)` nos fallbacks de `booted()` | **CONFIRMADO** | `Noticia.php:83,88,103` | Injetar `Clock`/`IdGen` (ou justificar por ser model boot) |
| `risk_003v3cyz` | Rodapé usa `now()->year` direto (ADR-001) | **CONFIRMADO** | `Portal/partials/footer.blade.php:13` | Passageiro do `Clock` (view composer/passado pelo shell) |
| `risk_00jg0337` | `LgpdDsarRequest::isOverdue()` cai em `Carbon::now()` | **CONFIRMADO** | `LgpdDsarRequest.php:83-85` | Fallback explícito via `Clock` (ou parâmetro obrigatório) |
| `risk_00catso0` | Plugin `precompress` fixa `outDir='public/build'` e retorna em silêncio | **CONFIRMADO** | `vite.config.js:46-52,90` | Derivar do `build.outDir` + falhar se ausente |
| `risk_0107zknc` | `sniff/current` acumula snapshots; nome com `date -u` | **CONFIRMADO** | `tools/sniff-gate.sh:43-45` | Poda/retenção + nome determinístico |
| `risk_00ub9we8` | `a11y-diff` usa `ls -t` (mtime) — não determinístico | **CONFIRMADO** | `Makefile:129` | Escolher por nome/último índice determinístico |
| `task_00hzia04` / `risk_00kwwdvz` | `sniff` mede só a home; `/noticias` e `/dados-abertos` ficam fora | **CONFIRMADO** | `sniff-gate.sh` sem `KEY_PAGES`; um único `URL` | Lista de páginas (desktop+mobile) |
| `risk_00qfi5yx` | `03-no-js` fixa cookie em `localhost` | **CONFIRMADO** | `03-no-js.spec.ts:29` | Derivar do `BASE_URL` |
| `task_01b6e7df` | `LocaleUrlInvariantTest` só rastreia `/en` e `/en/noticias` | **CONFIRMADO** | `LocaleUrlInvariantTest.php:107` | Iterar `es`/`fr` e as demais rotas públicas |
| `task_01kyxn03` | `LayoutContractTest` cobre só `src/modules/*`; views de service sem gate | **CONFIRMADO** | `LayoutContractTest.php:26-27` | Incluir `src/services/*/View/**` |
| `task_01o3tbg3` / `risk_01803fxp` | Blade usa FQN de classes do módulo/core | **CONFIRMADO** | `Noticias/Layout/Views/cms/form.blade.php:6,57,93` | Importar e usar o nome curto (gate `BladeStaticCalls`) |
| `task_0085czqs` | Home usa `\Illuminate\Support\Facades\Route::has` na view | **CONFIRMADO** | `Home/Layout/Views/index.blade.php:24` | Importar a facade |
| `task_01o6ify3` | Home lê `config('layout.portal.nav')` (módulo conhece o layout) | **CONFIRMADO** | `Home/index.blade.php:23` | Mover a navegação para slot/dado do layout |
| `risk_01qasi11` | Navegação rápida (lista plana) montada na view; `PanelMenu` só expõe árvore | **CONFIRMADO** | `Components/cms.blade.php:19-25` | Expor lista plana no `PanelMenu` |
| `risk_01angu5b` | `makeProviderFor` instancia com `new $class`; URI do `MODULE.md` embute domínio | **CONFIRMADO** | `ModuleDiscoveryService.php:239` | Container/make + URI relativa |
| `risk_003dknza` | `SlotRegistry` instancia provedores a cada `resolve()`/`has()` (dupla) | **CONFIRMADO** | `SlotRegistry.php:54-92` | Cache por slot ou `has()` sem resolver |
| `risk_004981jq` | `ThemeResolver` só recebe `global` (preset/URL/instância sempre vazios) | **CONFIRMADO** | `ThemeStylesheetController:26` | Ligar as camadas reais (ADR-016) |
| `risk_00ieqlef` | `make:module` em grupo novo não cria `layout::shell.group_*` | **CONFIRMADO** | `MakeModuleCommand` gera `Lang/index|menu|cms`, não o rótulo de grupo | Gerar/garantir a chave + teste |
| `risk_00ap1w5z` | Broker `passwords` aponta para tabela inexistente | **CONFIRMADO** | `config/auth.php:97-100` | Remover broker (reset fora de escopo) |
| `risk_00aakt4d` | Login lê `$request->string()` em vez de `validated()` | **CONFIRMADO** | `AuthenticateController:28-29` | Usar `$request->validated()` |
| `risk_00frr34f` / `risk_00h2gtga` | Marca do painel usa `url('/')` e título duplicado com ` · CMS` | **CONFIRMADO** | `Cms/partials/header.blade.php:12,17` | `route('cms.dashboard')` + fonte única do nome |
| `risk_00yrew63` | Busca aponta para `/busca`, rota inexistente (Search no backlog) | **CONFIRMADO** | `config/layout.php:27`; `@debt [SEARCH]` na partial | Esconder/desabilitar o campo até o service existir |
| `risk_01cavrsa` / `task_00w29li2` | Listeners do `openDialog` acumulando | **RESOLVIDO** | `dialogs.js:24-36` usa `{ once: true }` | Fechar a task como stale |
| `risk_00fao013` | `data-indent` sem CSS (recuo só no portal) | **CONFIRMADO** | nenhum CSS `[data-indent]` encontrado | CSS no editor (ou remover atributo) |
| `risk_01ve5fta` | `format('woff2-variations')` diverge do exemplo da ADR-011 | **CONFIRMADO** | `fonts.css:13,26,39,52` | Alinhar doc/implementação (`tech(variations)` é o moderno) |
| `risk_01yrqp1i` | `SeoService::resolve()` usa `url()->current()` num método dito puro | **CONFIRMADO** | `SeoService.php:34` | Injetar/passar a URL |
| `risk_01j5sg9f` | Conversão BCP-47 duplicada em 4 pontos | **CONFIRMADO** | `BuildSeoGraph:143`, `BuildSeoSitemap:105`, `SeoSettings:93`, `tags.blade:45` | Helper único no `Core\Localization` |
| `risk_01x6p56m` | Limite duro de mídia é estático; mudar `config/media.php` exige Caddy/Apache/PHP juntos | **ACEITO** | `config/media.php` documenta a dependência | Manter documentado (não é bug) |
| `risk_000ctztx` | `legal_hold` sem superfície de gestão; `type`/`status` sem CHECK; retenção sem chunking | **CONFIRMADO** | `LgpdDsarRetention`; só DB/seed altera `legal_hold` | Comando/tela de legal hold + CHECKs + chunking |
| `risk_01vx21pf` | Auditoria grava `lgpd.updated` enquanto span/log usam `lgpd.update_dsar` (vocabulário não único) | **CONFIRMADO** | `LgpdUpdateDsar:28,46` × evento do model | Alinhar `{module}.{action}` nos três canais (ADR-024/035) |
| `risk_017spml8` | Nenhum gate pega `id` duplicado comum nas páginas | **OBS** | axe deprecou a regra | Opcional: gate próprio |
| `risk_00flr3qt` | `IconSet` injeta SVG com `{!! !!}` | **ACEITO** | SVG vendorizado verificado sem `script`/`on*` | Sem ação enquanto for vendorizado |

---

## 4. Prioridade P3 — documentação, higiene e dívida registrada

| ID | Item | Veredito | Evidência atual | Ação |
|---|---|---|---|---|
| `task_00tbqx0x` / `risk_00o5fn3y` | 21 docs normativos ainda citam alvos `make` aposentados | **CONFIRMADO** | `grep` em `wiki/plan`, `wiki/adr`, `wiki/implementation` | Substituir por `make check`/`make fix`; gate em `DocsReferencesTest` |
| `risk_013jd913` | Tabela "Estado" do README divergente do `kd` | **CONFIRMADO** | `wiki/implementation/README.md:89-90` (F4/F5 antigos) | Atualizar ao fechar cada épico |
| `risk_01irkxc8` | 13 ADRs ainda "Proposto" com implementação fechada | **CONFIRMADO** | `wiki/adr/*.md` | Promover status (ADR-009/010/011/012/028…) |
| `risk_00vh5oe7` | `DESIGN.md` diz só `--tenant-*`, mas o schema aceita `font_content` | **CONFIRMADO** | `DESIGN.md:144` × `config/design.php:47` | Corrigir o texto |
| `risk_01115fpq` | ADR-016 §47 lista borda/densidade/espaçamento; schema só cobre cor/raio/tipografia | **CONFIRMADO** | `config/design.php:37-55` | Emenda ADR-016 (escopo do schema) |
| `risk_003nlw9n` | Tabela "Prontas" de `wiki/specs/README.md` fora de ordem | **CONFIRMADO** | `04`/`05` no fim | Reordenar |
| `task_00v7tsxs` | `Demo`, `Blueprint` e `Home` sem `MODULE.md` (ADR-014) | **CONFIRMADO** | 3 pastas sem o arquivo | Criar os três |
| `task_01wyciib` | `.phpunit.cache/test-run-history` (3,9 MB) rastreado | **CONFIRMADO** | `git ls-files` | `git rm --cached` (o humano remove no commit) |
| `risk_00eg38fl` | Dead-code detector com falsos-positivos (Blade estático/app/listeners) | **ACEITO** | `phpstan.neon` usa `shipmonk` | Manter (documentado) |
| `risk_00avmp3u` | `Locales`/`LocalizedDate` usam `app()->getLocale()` | **ACEITO** | justificado por chamadas de Blade | Sem ação |
| `risk_00lalpoz` | ETag só no GET (HEAD remove corpo antes do middleware) | **ACEITO** | documentado em `PublicResponseCache` | Sem ação |
| `risk_01dpmc5y` | sniff não mede contraste sobre transparente/imagem (axe arbitra) | **ACEITO** | documentado no `sniff-gate.sh` | Sem ação |
| `risk_01i3xldb` | Freio é e-mail+IP (credential stuffing distribuído) | **ACEITO** | decisão ADR-037 | Backlog (2FA/SSO) |
| `risk_0181ecxw` | URL antiga não mapeada cai em 404 silencioso | **ACEITO** | `http.not_found` monitora | Operação (monitorar 1ª semana) |
| `risk_01gkj3e9` | ADR-034 previa UI de ativação de módulo no CMS; só comando existe | **CONFIRMADO** | manifest/nav filtram; sem tela | Backlog (F4 futuro) — registrar |
| `risk_01utjqyo` | Painel sem projeto mobile no e2e/sniff | **CONFIRMADO** | `playwright.config.ts` só `cms-desktop` | Adicionar projeto mobile do CMS |
| `task_01ubnz8t` | Modo `orbital` sem e2e/axe (07 só exercita sidebar left) | **CONFIRMADO** | nenhum spec de orbital | Spec e2e/axe do orbital |
| `risk_01072eae` | `ActionTest.stub` nasce com `markTestIncomplete` | **ACEITO** | por design (vermelho→verde) | Sem ação |
| `risk_00i5tf40` | POST único 20–50 imagens estouraria borda | **RESOLVIDO** | lote em N requisições + limites (110M/114M) | Sem ação |
| `risk_01s1kl4y` | e2e de mídia exige rebuild do Vite/restart do queue | **OBS** | ambiente de teste | Documentar no README de e2e |
| `risk_003dqbkw` | Só o Demo usa `metadata['seo']`; Notícias passa schemas direto na view | **OBS** | `Noticias/index.blade.php:25`, `show.blade.php:15` | Unificar no service quando a Notícias migrar (sem urgência) |
| `risk_01urn5zn` | Observações de F6: barra final, `BACKUP_KEEP` cru, `PGDATA` fixo, `--password` no histórico | **PARCIAL** | `up_prod.sh:203` já sanitiza `BACKUP_KEEP`; `pitr` fixa `PGDATA`; `--password` continua | Corrigir os 3 restantes (barra final, `PGDATA`, senha por stdin) |
| `task_018ycvqv` | Coverage ~45 s: avaliar paratest | **CONFIRMADO** | maior passo do `make check` | Spike de paratest + merge de clover (ADR-038) |
| `risk_01x4nb59` / `risk_00laibl4` | Cobertura <90% | **RESOLVIDO** | gate ≥90 ativo e F5 verde | Fechar a nota |

---

## 5. Itens já resolvidos — só fechar no `kd` (nenhum código)

| ID | Por que está resolvido |
|---|---|
| `risk_000gxvgo` | Nenhum `/** @test */` restou em `src/` |
| `risk_001dbwjw` / `risk_007kcri0` | `ModuleI18nTest` já varre `src/layout/Portal` e `Components` (linhas 353–354) |
| `risk_0024uunm` / `risk_006mvu34` | Resolvidos em F1.I4.T1 (declarado na própria nota) |
| `risk_006dqxz7` | Dockerfile instala `libvips-tools` (linha 26) |
| `risk_006siwh4` | `core` não importa mais `Services\SEO` |
| `risk_00auaesf` | `ModuleDiscoveryTest` roda (sem skip) |
| `risk_00b2hslm` | `a11y.css` remapeia `--color-primary-content`/`--civic-*` (linhas 57–98) |
| `risk_00b7wcy7` | `14-security-headers.spec.ts` é o gate |
| `risk_00ey1rv2` | Migração `convert_noticias_content_to_jsonb` existe |
| `risk_00hmalfj` | Vendorização/allowlist fechadas |
| `risk_00i5tf40` | Lote + orçamento/cota implementados |
| `risk_00laibl4` | Cobertura ≥90 no gate (evidência F5) |
| `risk_00sjrphk` | Seeder cria o fundador (`CmsUserSeeder:47-58`) |
| `risk_00v9atym` | `e2e.sh` deriva de `WEB_PORT` (linha 35) |
| `risk_00yl39im` | Base é `php:8.4-fpm-trixie` oficial pinada (sem DHI/credencial) |
| `risk_00ynkz5p` | Fonte vendorizada; CDN removida |
| `risk_018l5yn9` | Login/logout do CMS existem (ADR-037) |
| `risk_01aws2yx` | `ThemeSchema` rejeita cor por nome — comportamento intencional |
| `risk_01cavrsa` / `task_00w29li2` | Listeners usam `{ once: true }` |
| `risk_01dw3m18` | ADR-033 usa `core_roles.permissoes` jsonb (sem tabela) |
| `risk_01fm623b` | `PlatformArea` não carrega mais a CSP do v5 |
| `risk_01iquhwe` | `SlotConfig::order` é aplicado no `resolve()` |
| `risk_01okkydv` | `StripPublicCookies` remove `Set-Cookie` público e adiciona `Vary: Cookie` |
| `risk_01x4nb59` | Cobertura ≥90 no gate |
| `task_00f8f18w` | `DEPS_IF_STALE` (lockfile × `node_modules`) |
| `task_00nffbim` | `PublicMilestoneTest` existe |
| `task_00w29li2` | (idem `risk_01cavrsa`) |
| `task_00h25ff0` | **PARCIAL** — verificar token kebab (ADR-004) antes de fechar |

---

## 6. Duplicidades e saneamento do `kd`

Estes pares são o mesmo achado; **fechar o redundante** para não inflar o quadro:

| Mantém | Fecha (duplicado) |
|---|---|
| `task_01btnk4j` | `risk_01111rdx` |
| `task_012jlp5n` | `risk_00amq05u` |
| `task_00vimpys` | `risk_01npkvew` |
| `task_01kvi19b` | `risk_00znkqy1` |
| `task_005ts67s` | `risk_0109sibj` |
| `task_01o3tbg3` | `risk_01803fxp`, `task_0085czqs` |
| `task_00lzwcrb` | `risk_01oagqo4` |
| `task_00inehom` | `risk_01shfi4j` |
| `task_00hzia04` | `risk_00kwwdvz` |
| `task_00tbqx0x` | `risk_00o5fn3y` |
| `task_01hpxxr3` | `risk_01o4u1fp` |
| `task_00v7tsxs` | `risk_0061h2mb`/`task_01doc455` (mesma família: `MODULE.md`/ADR-026) |

Também: `F3` (`epic_01swzu00`), `F6.I2` (`task_01wedy29`) e `F6.I3` (`task_01op1idn`) têm
**todos os filhos fechados** — podem ser encerrados no `kd` assim que a borda real for validada.

---

## 7. Itens sem task no `kd` (criar quando entrar em execução)

Sugestão de agrupamento (uma issue por tema, tasks curtas):

1. **`PostReview/Seo`** — canonical/paginação, hreflang 404, BCP-47 único, `url()` puro, manifest PWA.
2. **`PostReview/Cache`** — `If-None-Match` (W/), `theme.css` (cache-busting + emissão só válida), `defaults()` vazio.
3. **`PostReview/A11y-CMS`** — `data-a11y-*` no shell CMS, movimento reduzido alcançável, dislexia/fallback, `aria-current`, card heading, `Ctrl+K` no editor, orbital (e2e+CSS `--i`), projeto mobile.
4. **`PostReview/Dados`** — predicado único de publicação, `metadata` DCAT, colunas duplicadas, rascunho × `published_at`, `{state}` restrito, legal hold + CHECKs + chunking.
5. **`PostReview/Arquitetura`** — `services→services` no `arch-check`, contrato de leitura do Media, `new $class`, `ThemeResolver` camadas, `SlotRegistry` dupla instanciação, `make:module` grupo.
6. **`PostReview/Higiene`** — `MODULE.md` faltantes, docs com alvos aposentados, tabela Estado, ADRs "Proposto", `DESIGN.md`/ADR-016, `.phpunit.cache` rastreado, `src/core/Lang` (nada de `lang/` na raiz).

> Regra do protocolo: achado recorrente vira **teste estrutural**; cada correção entra com teste
> vermelho→verde e `make check E2E=1` verde **2×**.

---

## 8. Sequência encadeada de execução

> **Princípio da ordem:** primeiro o que **desbloqueia** (contratos, traduções, invariantes), depois os
> **consumidores** (SEO, cache, CMS), depois os **gates** que travam regressão, e só no fim **docs e
> otimização**. Cada bloco é uma mudança coesa (um “PR lógico”) com **um único gate de bloco** — isso
> reduz rebuild, troca de contexto e o custo de `make check`.
>
> **Regras:** (1) passo só começa com o anterior verde; (2) cada passo entra com teste vermelho→verde;
> (3) `make check` ao fim de **todo** bloco; `make check E2E=1` ao fim dos blocos que tocam
> UI/HTML/CSS/JS/rota (1, 3, 4, 5, 6, 7 e 10); (4) nada disso fecha o épico até o gate final.

### Cadeia (linear)

```
B0 saneamento/higiene ─▶ B1 contratos+textos ─▶ B2 invariantes de domínio ─▶ B3 SEO
   ─▶ B4 cache/tema ─▶ B5 CMS+a11y ─▶ B6 arquitetura/determinismo ─▶ B7 qualidade do ciclo
   ─▶ B8 docs/higiene ─▶ B9 otimização ─▶ B10 infra+go-live
```

> **Lane paralela (humano):** B10.1–B10.3 podem começar a qualquer momento; não bloqueiam B1–B9,
> mas **bloqueiam o fechamento** de F1/F6.
>
> **Duplicados:** os riscos que são o mesmo achado de uma task (`risk_00amq05u`, `risk_00kwwdvz`,
> `risk_00o5fn3y`, `risk_00znkqy1`, `risk_0109sibj`, `risk_01111rdx`, `risk_01803fxp`,
> `risk_01npkvew`, `risk_01oagqo4`, `risk_01shfi4j`) não aparecem duas vezes: a sequência executa a
> **task canônica** e fecha a nota no `kd` (§6).

---

### B0 — Saneamento do `kd` + higiene de teste (sem feature)

Objetivo: o `kd` passa a refletir o código e os runs param de acumular lixo.

| # | Item | Depende | Teste |
|---|---|---|---|
| 0.1 | Fechar **26 riscos resolvidos**, **12 aceitos/obs**, **3 tasks resolvidas** e as duplicidades do §6 | — | `kd doctor` |
| 0.2 | Auditar fechamento: `F3` só fecha em B7 (`task_01b6e7df`); `F6.I2`/`F6.I3` só em B10 — **não fechar agora** | 0.1 | `kd task show` |
| 0.3 | Criar as issues `PostReview/*` do §7 para o que não tem task | 0.1 | `kd task list` |
| 0.4 | `task_00wmwv84` → `task_00nidhhg` — e2e idempotente (cleanup de `e2e-*`) | — | rodar `08-cms-noticias` 2× sem crescer |
| 0.5 | `task_01wyciib` → `task_00benx6j` — destrackear `.phpunit.cache` (**humano**: `git rm --cached`) | — | `git ls-files` limpo |
| 0.6 | `risk_00qfi5yx` → `task_01sg50t7` — cookie do `03-no-js` derivado do `BASE_URL` | — | e2e no-JS |
| 0.7 | `risk_00ub9we8` + `risk_0107zknc` → `task_01aozb7z` — snapshot determinístico e sem acúmulo | — | 2 rodadas do `a11y-diff` |

**Gate B0:** `make check` (sem E2E) + specs afetados (`08-cms-noticias`, `03-no-js`) + `kd doctor`.

> **Status 2026-10-20 — concluído (parcial humano):** 0.1 (39 riscos fechados + 3 tasks resolvidas),
> 0.2 (auditoria: `F3` fecha em B7, `F6` em B10), 0.3 (11 issues criadas sob `epic_01e7jml7`),
> 0.4–0.7 implementados e provados (`08-cms-noticias` e `03-no-js` verdes 2×; `make check E2E=1`
> **19 OK · 0 FAIL · 4 SKIP**). **Pendente humano:** 0.5 `git rm --cached .phpunit.cache/test-run-history`
> (`task_00benx6j`). Ver `kd task show --id task_00rr90ip`.

---

### B1 — Contratos, traduções e autenticação (desbloqueia o resto)

Objetivo: mensagens traduzidas, convidado indo ao login e mídia com contrato estável — o que
qualquer teste posterior assume.

| # | Item | Depende | Teste |
|---|---|---|---|
| 1.1 | `task_00inehom` — `src/core/Lang/pt_BR/{validation,auth,passwords}.php` + gate de chave traduzida | B0 | `ValidationMessagesTest` (chave nunca crua) |
| 1.2 | `task_00lzwcrb` — `auth` também nas rotas de service (convidado → login, não 403) | 1.1 | `CmsAuthTest` (302 × 403) |
| 1.3 | `task_01htnzob` — estado terminal da fila de mídia (não sobrescrever `concluido`) | B0 | `ProcessMediaUploadTest` (reentrega) |
| 1.4 | `task_00zo3k5i` — contrato de leitura por dono no Media (sem model importado) | 1.3 | teste do contrato + IDOR |
| 1.5 | `task_01gpf5qd` — Gallery consome o contrato; `arch-check` recusa `services→services` de implementação | 1.4 | `arch-check` com fixture vermelha |

**Gate B1:** `make check E2E=1` + `tools/arch-check`.

> **Status 2026-10-20 — concluído.** `src/core/Lang` nos **4 locales** (validation/auth/passwords/pagination; paridade de chaves provada; `useLangPath`
> no `bootstrap/app.php`, raiz sem `lang/`), `auth` nas rotas admin do Layout (convidado → login),
> `ProcessMediaUpload` terminal/idempotente sem `app()` (limpeza no listener `JobFailed`), contrato
> `MediaRepository` + DTOs e Gallery sem implementação do vizinho; `arch-check` endurecido
> (`services→services` só `Contracts|DTOs|Enums`) com fixtures. Gate: `make check E2E=1`
> **21 OK · 0 FAIL · 2 SKIP**; PHPStan max limpo; `gates-selftest` OK. IDs fechados:
> `task_00inehom`, `task_00lzwcrb`, `task_01htnzob`, `task_00zo3k5i`, `task_01gpf5qd`.

---

### B2 — Invariantes de domínio e dados (LGPD/medições)

Objetivo: uma única regra de publicação/status e dados íntegros — antes de qualquer tela nova.

| # | Item | Depende | Teste |
|---|---|---|---|
| 2.1 | `task_00vimpys` — scope único `publicados()` + `isPublished()` coerente | B1 | `DadosAbertosTest` (5 superfícies) |
| 2.2 | `task_012jlp5n` — ler `metadata` no registro DCAT/INDA | 2.1 | DCAT com `dcat:contactPoint` |
| 2.3 | `risk_00cibbhu` — regra única de `published_at` (rascunho × status) | 2.1 | `DadosAbertosCmsTest` |
| 2.4 | `risk_00swoasy` — recusar colunas duplicadas (JSON × CSV) | 2.1 | request test |
| 2.5 | `risk_01v8pm0b` — `{state}` restrito (`publicar`/`despublicar`) | B1 | rota 404/422 fora do enum |
| 2.6 | `task_01v75xex` — `UNIQUE (module, path)` em `gallery_media` + `ON CONFLICT` | B1 | `migration-check` + commit idempotente |
| 2.7 | `risk_000ctztx` — superfície de legal hold + `CHECK`s + chunking | 2.3 | `LgpdRetentionTest` |
| 2.8 | `risk_01vx21pf` — vocabulário `{module}.{action}` único (audit = span = log) | B1 | `RecordingTelemetry` + audit |

**Gate B2:** `make check` + `tools/migration-check` + testes de auditoria.

> **Status 2026-10-20 — concluído.** Publicação com `scopePublicados()` + `isPublished()` coerentes
> (5 superfícies; agendado invisível); DCAT lê `metadata['dcat:contactPoint']` (`vcard:Kind`);
> `DadosAbertosPublicationDate` é a regra única de `published_at` (rascunho preserva `dct:issued`;
> seeder com datas fixas); request recusa colunas duplicadas e linhas desalinhadas; rota de status
> de Noticias restrita (`publicar|despublicar` → 404 fora); `gallery_media` com `UNIQUE (module,path)`
> + `createOrFirst`; legal hold no CMS (`lgpd.aprovar`, auditado `lgpd.legal_hold`), `CHECK` de
> `type`/`status` e retenção com `chunkById`; `LgpdUpdateDsar` grava evento explícito
> `lgpd.update_dsar` (ADR-035 emendada). Gate: `make check E2E=1` **21 OK · 0 FAIL · 2 SKIP**;
> `make check` **2× 17 OK**; `migration-check` OK. IDs fechados: `task_00vimpys`,
> `task_012jlp5n`, `task_012yvmne`, `task_01v75xex`, `risk_01npkvew`, `risk_00amq05u`,
> `risk_00cibbhu`, `risk_00swoasy`, `risk_01v8pm0b`, `risk_000ctztx`, `risk_01vx21pf`.

---

### B3 — SEO e arquivos de raiz

Objetivo: uma passada só no `services/Seo` (helper → URLs → redirect → manifest).

| # | Item | Depende | Teste |
|---|---|---|---|
| 3.1 | `risk_01j5sg9f` — helper BCP-47 único no `Core\Localization` | B2 | 4 locais iguais nos 4 pontos |
| 3.2 | `task_01btnk4j` — `hreflang`/`canonical` só para rota existente | 3.1 | caminho só-default sem 404 |
| 3.3 | `risk_00jfeg7s` — paginação no canonical + `rel=prev/next` | 3.2 | `/noticias?page=2` |
| 3.4 | `risk_01yrqp1i` — `SeoService::resolve()` sem `url()->current()` | 3.2 | teste puro (sem request) |
| 3.5 | `risk_01cl9z5i` — ler `status` do redirect (301/302) | B2 | redirect 302 |
| 3.6 | `task_01kvi19b` — ícones reais no manifest PWA | B1 | manifest × Media |

**Gate B3:** `make check E2E=1` (SEO/headers/root files).

> **Status 2026-10-20 — concluído.** `Locales::bcp47()` é o ponto único (5+ pontos migrados);
> `LocaleRoutes` só emite `hreflang` para path que existe no roteador (caminho só-default não gera
> 404); paginação é self-canonical (`?page=N`) com `rel=prev/next` no `<head>`; `SeoService::resolve()`
> ficou puro (canonical default `null`, a view fornece a URL); redirect lê o `status` persistido
> (301/302/307/308); manifest PWA usa `pwa_icons` do catálogo Gallery com `mime`/dimensões reais
> (sem 192/512 inventados). ADR-027 emendada; spec 13 e README do Seo atualizados.
> Gate: `make check E2E=1` **21 OK · 0 FAIL · 2 SKIP**; `make check` **2× 18–19 OK**.
> IDs fechados: `task_01btnk4j`, `task_01kvi19b`, `task_01gg610g`, `risk_01j5sg9f`,
> `risk_00jfeg7s`, `risk_01yrqp1i`, `risk_01cl9z5i`.

---

### B4 — Cache e tema (cadeia resolver → emissão → headers)

Objetivo: seguir a dependência natural do fluxo, sem retrabalho de contrato.

| # | Item | Depende | Teste |
|---|---|---|---|
| 4.1 | `risk_004981jq` — `ThemeResolver` recebe as 5 camadas | B3 | precedência default→…→instância |
| 4.2 | `risk_00rmgqoq` — descartar valor vazio (nunca `--x: ;`) | 4.1 | `ThemeTokens` sem vazio |
| 4.3 | `risk_01l9ajqq` — só emitir `/theme.css` com ≥1 override válido | 4.2 | shell com overrides inválidos |
| 4.4 | `risk_015naqub` — `If-None-Match` com `W/` e lista | 4.3 | 304 com ETag fraco |
| 4.5 | `risk_00qafbph` — cache-busting/`no-cache` do `theme.css` | 4.4 | mudança reflete sem 1 h |
| 4.6 | `risk_01chvd8p` — orçamento de stylesheet por cenário | 4.3 | `11-performance` com tema |

**Gate B4:** `make check E2E=1` (cache/theme/perf).

> **Status 2026-10-20 — concluído.** `ThemeResolver::configuredLayers()` lê as 5 camadas
> (`design.overrides` + `preset/url_profile/instance_overrides`) e `GET /theme.css` resolve a cadeia
> completa; defaults também passam pelo schema e `ThemeTokens` não emite valor vazio; o shell só
> referencia `/theme.css` quando há **algum** override válido; `If-None-Match` usa
> `Response::isNotModified` (aceita `W/` e lista); `theme.css` virou
> `public, no-cache, must-revalidate` + `ETag` (mudança reflete na hora); o orçamento do e2e passou a
> contar **1 folha Vite + no máximo 1 theme.css**. ADR-016 emendada.
> Gate: `make check E2E=1` **20 OK · 0 FAIL · 3 SKIP**; `make check` **2× 18 OK**.
> IDs fechados: `task_01kzmchu`, `risk_004981jq`, `risk_00rmgqoq`, `risk_01l9ajqq`,
> `risk_015naqub`, `risk_00qafbph`, `risk_01chvd8p`.

---

### B5 — CMS e acessibilidade (uma passada no shell + axe/e2e)

Objetivo: tocar o layout **uma vez** e rodar a bateria de a11y no fim do bloco.

| # | Item | Depende | Teste |
|---|---|---|---|
| 5.1 | `risk_00j25aw6` — `data-a11y-*` no shell CMS (server-side) | B4 | `/cms` com preferência aplicada |
| 5.2 | `risk_00lbc67g` — movimento reduzido alcançável | 5.1 | cookie → `data-a11y-motion` |
| 5.3 | `risk_01qtnjqe` — dislexia usa **OpenDyslexic** + fallback métrico | 5.1 | `ContentFontTest` |
| 5.4 | `risk_00urwtfp` — `aria-current` no módulo aberto | B1 | e2e de navegação |
| 5.5 | `risk_006skhy0` — heading no card do CMS | B1 | axe/heading order |
| 5.6 | `risk_011cjiyl` — breadcrumb com hierarquia (sem repetir título) | 5.5 | snapshot/e2e |
| 5.7 | `risk_006noxdx` — `Ctrl+K` ignorado no editor | B1 | e2e editor |
| 5.8 | `risk_003a5995` — `--i` do orbital para N itens | B1 | CSS/estrutural |
| 5.9 | `task_01ubnz8t` — e2e/axe do orbital | 5.8 | spec orbital |
| 5.10 | `risk_01utjqyo` — projeto mobile do CMS no Playwright/sniff | 5.1 | e2e mobile |
| 5.11 | `risk_009tjwbb` — entrada de navegação da admin surface | B1 | menu × rota |
| 5.12 | `risk_0061h2mb`/`task_01doc455` — capa via `<picture>` (ADR-026) | B1 | `MediaNoRewrite`/picture |
| 5.13 | `risk_00frr34f`/`risk_00h2gtga` — marca `route('cms.dashboard')` + nome único | B1 | link do painel |
| 5.14 | `risk_00hi43ht` — span em `hasPlacements` + TTL dos perfis em config | B4 | `RecordingTelemetry` + TTL |
| 5.15 | `risk_01dxalee` — `z-index` do CMS via tokens `--z-*` | B5 | `DesignTokensTest` |
| 5.16 | `task_01ojpax3` — CMS só em `cms.[domínio]` + **única** ponte `[domínio]/cms[/*]` (301, noindex) | B4 | `CmsDomainRedirectTest` |
| 5.17 | `task_00nsikf4` — áreas restritas invisíveis para indexação (robots/sitemap) | 5.16 | `SeoSitemapTest` + ADR-043 |

**Gate B5:** `make check E2E=1` (desktop + mobile CMS) + axe.

> **Status 2026-10-20 — concluído.** O shell do CMS agora aplica `data-a11y-*` no SSR e tem a
> **mesma barra** do portal (movimento reduzido incluído); o modo dislexia usa **OpenDyslexic**
> vendorizada (OFL) com fallback métrico. `aria-current` marca a tela aberta, o card usa `<h2>`, o
> breadcrumb começa no painel, `Ctrl+K` não sequestra o editor, a órbita usa `sibling-index()` (N
> itens), o mobile do CMS tem projeto Playwright próprio e o sniff mede o login do painel; serviços
> com admin surface (Layout) entram na navegação; `z-index` só por `--z-*`; `hasPlacements` tem span
> e o TTL dos perfis é config; a capa legada passa por `<picture>`. **Fato de revisão (dono):** o CMS
> não pode contaminar o domínio público — toda rota CMS vive em `cms.[domínio]` e o domínio base tem
> **uma única** ponte `/[domínio]/cms[/*]` (301, query preservada, `X-Robots-Tag`). **ADR-043:** só o
> portal é indexável; CMS/API/futuras são invisíveis em robots/sitemap e nascem com segurança mais
> elevada. Gate: `make check E2E=1` **21 OK · 0 FAIL · 2 SKIP**; `make check` 2× **18 OK**. IDs fechados: `task_01ojpax3`,
> `task_00nsikf4` + os 15 itens de B5.

---

### B6 — Arquitetura, fronteiras e determinismo

Objetivo: fechar os gates estruturais **depois** que o comportamento estabilizou.

| # | Item | Depende | Teste |
|---|---|---|---|
| 6.1 | `risk_003dknza` — `SlotRegistry` sem dupla instanciação | B5 | contador de `make()` |
| 6.2 | `risk_01angu5b` — provider via container (`new $class`) | B1 | `ModuleDiscoveryTest` |
| 6.3 | `risk_00ieqlef` — `make:module` cria `layout::shell.group_*` | 6.2 | módulo novo navegável |
| 6.4 | `task_01o3tbg3` + `task_0085czqs` — sem FQN em Blade | B5 | `BladeStaticCallsTest` |
| 6.5 | `task_01o6ify3` — Home não lê `config('layout.*')` | B5 | `HomeModuleTest` |
| 6.6 | `risk_01qasi11` — lista plana no `PanelMenu` | B5 | `PanelMenuTest` |
| 6.7 | `task_01kyxn03` — `LayoutContractTest` cobre views de service | 6.4 | gate vermelho→verde |
| 6.8 | `risk_0115bt4r` — `Clock`/`IdGen` no `Noticia` | B2 | determinismo (slug/data) |
| 6.9 | `risk_003v3cyz` — `Clock` no rodapé | 6.8 | shell com data fixa |
| 6.10 | `risk_00jg0337` — `Clock` no `isOverdue()` | B2 | teste com data fixa |
| 6.11 | `risk_00ap1w5z` — remover broker `passwords` | B1 | `config/auth` |
| 6.12 | `risk_00aakt4d` — login via `validated()` | B1 | login sem `string()` |
| 6.13 | `risk_00yrew63` — esconder busca até o service existir | B3 | sem link 404 |
| 6.14 | `risk_00fao013` — `data-indent` com CSS no editor | B5 | CSS do editor |
| 6.15 | `risk_01ve5fta` — `format()` do WOFF2 alinhado | B5 | `DesignTokensTest` |
| 6.16 | `risk_00jse6xz` — `PresetConfig::label` morto/pt_BR | B5 | remoção + PHPStan dead-code |
| 6.17 | `task_00h25ff0` — token grep-friendly (kebab × `strtolower`/snake), decidir e alinhar ADR-004 | B6.4 | `name-check` + 4 locales |
| 6.18 | `task_01hpxxr3` — Demo sem `app(Hasher)` na view (serviço injetado/passado) | B5 | `BladeStaticCallsTest` |

**Gate B6:** `make check E2E=1` + PHPStan max + `arch-check` + `name-check`.

> **Status 2026-10-20 — concluído.** Fechados os 18 itens: `SlotRegistry` memoiza o provedor por
> requisição (`has()`+`resolve()` = 1 instância); `ModuleProvider` é resolvido pelo **container**
> (teste com construtor que exige `Clock`); `make:module` grava `layout::shell.group_*` nos 4 locales;
> Blade sem FQN (gate em `BladeStaticCallsTest`); Home usa `<x-layout::quick-links>` (não lê
> `config('layout')`); `PanelMenu::flat()` é dono da navegação rápida; `LayoutContractTest` cobre
> views de página de service; Notícia e LGPD usam `Clock`/`IdGen`/`Carbon` injetados (observer para
> criação direta); rodapé usa `Clock`; broker `passwords` removido e `defaults.passwords` nulo;
> login consome `validated()`; busca morta (`/busca`) foi retirada do chrome até o service existir;
> recuo `data-indent` tem CSS no editor; WOFF2 usa `format('woff2') tech(variations)`; `PresetConfig`
> perdeu o `label` morto; ADR-004/spec 09 explicitam identidade Pascal + formas derivadas (kebab no
> log/span e snake no namespace/permissão, logs de DadosAbertos alinhados); Demo injeta `Hasher` no
> controller. Evidência: `make check E2E=1` **21 OK · 0 FAIL · 2 SKIP**; `make check` 2× **19 OK**;
> PHPStan max **0 erros**; `arch-check`/`name-check`/`docs-check` verdes.

---

### B7 — Qualidade do ciclo e gates transversais

Objetivo: agora que o código estabilizou, tornar os gates **confiáveis e rápidos**.

| # | Item | Depende | Teste |
|---|---|---|---|
| 7.1 | `task_005ts67s` — CLS por `document.fonts.ready` + rAF | B5 | vermelho com shift tardio |
| 7.2 | `task_00hzia04` — sniff com `KEY_PAGES` (inclui `/dados-abertos`) | B3 | 2 páginas medidas |
| 7.3 | `risk_01qke8zj` — gate de borda valida a config **efetiva** do container | B5 | Caddyfile antigo reprova |
| 7.4 | `task_01b6e7df` — `LocaleUrlInvariantTest` em `es`/`fr` | B3 | crawl 4 locais |
| 7.5 | `risk_00catso0` — precompress deriva `outDir` e falha se ausente | B4 | build sem `public/build` |
| 7.6 | `risk_01s1kl4y` — documentar rebuild/queue no e2e de mídia | B5 | README e2e |

**Gate B7:** `make check E2E=1` **2×** (determinismo) + tempos ≤100 s/≤3 min.

> **Status 2026-10-20 — concluído.** Fechados os 6 itens: o CLS deixou o `waitForTimeout` e passou a
> esperar `document.fonts.ready` + dois `rAF` (`PerformanceBudgetTest` prova a regressão); o sniff
> mede `KEY_PAGES` (default `/ /dados-abertos`) nos dois viewports e no login do CMS, com duas páginas
> efetivamente capturadas; o `security-check` lê os **headers do `web` em execução** (CSP do CMS com
> Trusted Types) além do arquivo, com fixture `old-edge` no `gates-selftest` provando que Caddyfile
> antigo reprova; `LocaleUrlInvariantTest` rastreia `es`/`fr` em 6 páginas públicas (4 locales,
> 199 asserções); `precompress(BUILD_OUT_DIR)` deriva a constante única de `build.outDir` e **falha**
> se o diretório não existe (gate em `PerformanceBudgetTest`); o README do e2e documenta rebuild +
> `queue:restart` para mídia (`risk_01s1kl4y`). No ciclo, o autoload trocou o mtime do classmap por
> stamp próprio e ignora `*/Lang/*` (arrays, não classes) — o passo caro deixou de rodar em todo
> check. Evidência: `make check E2E=1` **2× 20 OK · 0 FAIL · 3 SKIP** no código final (145 s; antes do
> stamp do autoload havia rodado 2× 21 OK · 2 SKIP); `make check` **89,06 s** (18 OK · 5 SKIP), abaixo
> do alvo de 100 s; `security-check` efetivo e `gates-selftest` verdes.

---

### B8 — Documentação e higiene

| # | Item | Depende | Teste |
|---|---|---|---|
| 8.1 | `task_00tbqx0x` — remover alvos `make` aposentados (21 docs) | B7 | `DocsReferencesTest` |
| 8.2 | `risk_013jd913` — tabela “Estado” = `kd` | 8.1 | contagem derivada |
| 8.3 | `risk_01irkxc8` — promover ADRs “Proposto” | 8.1 | `docs-check` |
| 8.4 | `risk_00vh5oe7` + `risk_01115fpq` — `DESIGN.md`/ADR-016 | 8.3 | `docs-check` |
| 8.5 | `risk_003nlw9n` — ordem da tabela de specs | 8.1 | `docs-check` |
| 8.6 | `task_00v7tsxs` — `MODULE.md` em Demo/Blueprint/Home | 8.1 | `DocsReferencesTest` |
| 8.7 | `risk_01urn5zn` — barra final/`PGDATA`/senha por stdin no F6 | B1 | drills de restore/PITR |
| 8.8 | `risk_01gkj3e9` — registrar UI de ativação de módulo no backlog | 8.1 | — |

**Gate B8:** `make check` + `bash tools/docs-check.sh`.

> **Status 2026-10-20 — concluído.** Fechados os 8 itens. **8.1:** 32 docs de `wiki/` (planos, ADRs,
> relatos de fase e índices) perderam os alvos `make` aposentados — `make quality/quality-2x`,
> `lint`, `static`, `test`, `coverage`, `e2e`, `a11y`, `perf`, `check-*`, `optimize-*`, `audit`,
> `demo`, `psql`, `ratchet` etc. — e o `DocsReferencesTest` ganhou o gate que varre **todo** `wiki/`
> (0 restantes). **8.2:** a tabela `Estado` virou snapshot explícito de `kd` (F0–F6, incluindo
> 16/18 e 25/25) e o épico F3 foi fechado (20/20) após B7. **8.3:** as 12 ADRs que ainda diziam
> `Proposto` passaram a `Aceito (2026-10-20)` e o índice `wiki/adr/README.md` reflete as 14 linhas.
> **8.4:** `DESIGN.md` e ADR-016 separam o schema atual do tenant (cor, raio, `font_content`) de
> borda/densidade/espaçamento (`PresetSchema`/presets). **8.5:** tabela de specs em ordem numérica.
> **8.6:** `MODULE.md` criado para Demo, Blueprint e Home (rotas, eventos, permissões, widget e
> verificação). **8.7:** `seo:redirect` canonicaliza a barra final; `BACKUP_KEEP` é sanitizado uma
> vez; `pitr-drill` deriva o `PGDATA`; `core:user:create --password-stdin` + runbook sem senha no
> histórico; JSON do go-live escapado; e-mail existente é achado sem diferenciar caixa; `now_ms`
> tem fallback portátil. **8.8:** UI de ativação de módulos registrada no backlog
> (`wiki/backlog/ui-ativacao-modulos.md`). Evidência: `make check` **2× verde** (18 OK · 0 FAIL ·
> 5 SKIP e 17 OK · 6 SKIP; 99,93 s no segundo), `docs-check` **13 specs**, PITR **4 s** e restore
> do dump **4 s**.

---

### B9 — Otimização e observabilidade pós-revisão

| # | Item | Depende | Teste |
|---|---|---|---|
| 9.1 | `task_018ycvqv` — paratest + merge de clover (ADR-038) | B8 | cobertura ≥90% e ciclo ≤100 s |
| 9.2 | ADR-039 — Infection em 2 etapas (observabilidade → gate) | 9.1 | `src/core` + `src/services` |

**Gate B9:** `make check` ≤100 s · `make check E2E=1` ≤3 min.

> **Status 2026-10-20 — concluído.** **9.1:** a suíte foi para **ParaTest 4** (`tools/coverage-parallel.sh`,
> `COVERAGE_WORKERS`) com **banco por worker** (`concordia_test_<token>`) e clover/cobertura
> mesclados pelo próprio ParaTest; todos os testes usam `RefreshDatabase` (`Tests\TestCase`), o
> banco base só atende PHPUnit serial/Infection. Determinismo: lock compartilhado para a árvore
> `src/` (testes que geram/apagam módulo usam `LOCK_EX`) e `Cache::flush()` por teste; três falhas
> de corrida foram reproduzidas e eliminadas. **9.2:** etapa 1 do ADR-039 — `infection.json5`
> (`src/core` + `src/services`, `minMsi=0`), `tools/mutation.sh` e job **manual/não bloqueante** no
> CI com MSI em `build/mutation/`; `LOG_JSON_STREAM=/dev/null` evita o falso erro do
> `InitialTestsRunner` (ele para no primeiro stderr). Amostra de 190 mutantes em `src/core/Localization`
> fechou MSI 100% em 73 s; o gate de piso continua para a etapa 2. Evidência: `make check`
> **90,30 s / 92,44 s / 81,93 s** (3× verde, 17 OK · 0 FAIL · 6 SKIP), `make check E2E=1` **158,83 s** (19 OK,
> e2e 67 s + perf 12 s), cobertura mesclada **92,83%**.

---

### B10 — Infra real e fechamento (humano)

| # | Item | Depende | Evidência |
|---|---|---|---|
| 10.1 | `task_00ja4pzi` — domínio/AutoSSL/`ProxyPass` no WHM + headers | B9 | `go-live-check` 9/9 no cliente |
| 10.2 | `task_00oifzp1` — clone limpo → `up_prod.sh` → `/up` 200 | commit do humano | log do deploy |
| 10.3 | `risk_01jcnlfd` — primeira execução do CI no runner `deploy` | B9 | pipeline verde |
| 10.4 | Deploy 2× + falha injetada **no servidor** + páginas críticas do cliente | 10.1–10.3 | DoD de `06-deploy-migracao.md` |

**Gate B10 (DoD):** `make check E2E=1` verde **2×** + `go-live-check` + `kd sync` + `kd rewind`.

---

### Regra de bolso por bloco

```
para cada passo:
  escrever teste que falha  →  implementar  →  verde
fim do bloco:
  make check [E2E=1 conforme o bloco]  →  corrigir  →  registrar evidência no kd
fim de B7/B10:
  repetir 2× (sem flaky)  →  kd sync  →  entrega ao humano (sem commit)
```

> **Por que esta ordem simplifica:** contratos e traduções primeiro evitam retrabalho em todos os
> testes; dados antes de SEO/CMS evitam semear dados errados; SEO antes de cache porque o cache
> depende das URLs finais; CMS antes dos gates estruturais porque os gates precisam do markup
> estável; docs por último porque só descrevem o que já foi decidido; infra no fim porque valida o
> que o código já prova em dev.


## 9. Rastreabilidade

- Notas `risk_*`: `.knudge/notas/risk/` · Tasks: `kd task list --status active`
- Fichas: `_to_revision/F*-T*.md` · Protocolo: `_to_revision/PROTOCOLO.md` (v2.0)
- Planos: `wiki/implementation/{00..06}.md` · ADRs: `wiki/adr/` · Specs: `wiki/specs/`
- Este documento é **análise**, não decisão: cada item confirmado deve virar/atualizar uma task no
  `kd` antes de ser corrigido (ADR-001/regra do projeto).
