# QA de revisão — Onda 3 (gate G3)

**Data/snapshot:** 2026-10-21. Documentos e `sucesu-cms/V2/` lidos nesse estado; nenhum
arquivo de unidade ou de `sucesu-cms/` foi alterado; `git -C sucesu-cms status
--porcelain V2` está vazio, confirmando a leitura read-only.

**Escopo:** leitura integral dos 11 documentos da Onda 3:

- `05_modulos/Support/SupportConfigs/{spec,dados,rotas,cms}.md`
- `05_modulos/Support/StructuralLandingEditor/{spec,editores-temas-e-assets,ui}.md`
- `05_modulos/Support/NoticiasImport/{spec,contrato-api}.md`
- `05_modulos/Layouts/BasicPage/{spec,runtime-e-seo}.md`

**Método:**

1. Leitura integral dos 11 arquivos à luz de `_contrato/TAXONOMIA.md`,
   `_contrato/CHECKLIST_EXTRACAO.md` e `PLANO.md` §7.3/§10 (G3).
2. Validação de citações `caminho:linha` por resolvedor próprio e por
   `python3 sucesu_analise/tools/lint_docs.py` (0 erros; 9 avisos de faixa fora do arquivo).
3. Conferência dos trechos pivô contra o intervalo citado no código e leitura dos pontos de
   despacho fora do diff (middlewares, composers, views de portal, indexadores).
4. Verificação semântica por unidade: tema/flags/manutenção/SMTP/reset; render por `eval` e
   `BlockRenderer`; payloads `v2_layout_base_configs`; assets/File Library; API interna,
   `consumer`, chave estática, path traversal e idempotência; catch-all, 404, `metadata`
   duplo-encode, `{!! content !!}`, busca, cache e páginas sombra.
5. Conferência dos índices parciais (`_indices/parcial-*`) contra o texto e o código, incluindo
   contagem de rotas, blades e cobertura do índice canônico de componentes.
6. Consistência cruzada com W1/W2 (auth/middleware, cache/CSP, Storage/Media/Gallery/SEO/Search,
   SLE × LSE × BasicPage) e com `_qa/security-g1.md` (SEC-G1-04/05).

Ferramentas executadas: `lint_docs.py` (11 docs, 0 erros, 9 avisos de citação fora do arquivo),
`merge_indices.py --check` (Índices OK) e conferência direta dos CSVs canônicos de componentes e
rotas.

## 1. Verificações do gate

| # | Critério | Resultado |
|---|---|---|
| 1 | Contratos de tema, editor e fallback descritos com efeito real | **OK.** SupportConfigs documenta `CmsThemeComposer`/tema real; SLE documenta `BlockRenderer`/`eval`, assets e temas; BasicPage documenta catch-all, fallback Blade e SEO. |
| 2 | Citações `caminho:linha` existentes e dentro do arquivo | **Ressalva.** 0 caminhos inexistentes; 9 faixas passam do fim do arquivo (G3-12), além de âncoras deslocadas em trechos de UI. |
| 3 | Trechos pivô (3–8 por documento, 5–20 linhas) | **OK.** Os trechos são literais nos pontos centrais e explicam o efeito; nenhum bloco cola arquivo inteiro. |
| 4 | Receitas e anti-usos | **OK.** Todos os documentos têm 2–5 receitas e 1–4 anti-usos com consequência explícita. |
| 5 | Critérios de paridade testáveis | **Ressalva.** `PC-BASICPAGE-13` (G3-01), `PC-SLE-12` (G3-07) e `PC-SLE-08` (G3-08) descrevem resultados que o código não produz. |
| 6 | Consistência cruzada com W1/W2 | **Ressalva.** O contrato de `active_pattern` (G3-03), a contagem do LSE (G3-10) e o mapa de escritores de `v2_portal_configs` (G3-04) divergem de W1/W2/código. |
| 7 | Índices parciais coerentes com texto/código | **Ressalva.** Rotas, tabelas e configs conferem; faltam `componentes.csv` em duas unidades e linhas de grupo em SupportConfigs (G3-13). |

## 2. Achados

### G3-01 — `updateSeo` sem `BasicPage: write` não responde o 403 documentado

**Severidade: P2.** **Confiança: 0.95.**

`05_modulos/Layouts/BasicPage/spec.md:72` e `:838` (`PC-BASICPAGE-13`) afirmam que
`POST /cms/paginas-fixas/seo/update` sem `BasicPage: write` responde
`403 Acesso negado. Ação restrita a operadores com permissão de escrita.` O `403` do controller
existe (`BasicPageCmsController.php:287-289`), mas a rota é POST sob
`active_pattern = 'cms/paginas-fixas*'`; `V2PermissionMiddleware` é executado antes do controller
e, para `read`, responde `302` com `Operação negada. Seu perfil possui apenas permissão de leitura...`
(`V2PermissionMiddleware.php:102-121`), e, para `none`, `302` para o dashboard. Em AJAX a resposta
é `403 Forbidden...`, mas não a mensagem do controller. O `403` do controller só seria alcançado
por quem já tem `write` — caso em que `v2_can` também retorna `true`. O critério de paridade é,
portanto, falso para o fluxo HTML normal.

**Impacto:** um teste de paridade escrito a partir de `PC-BASICPAGE-13` espera status e corpo que
não ocorrem; a distinção entre negação do middleware e negação do controller fica perdida.

**Correção recomendada:** documentar o comportamento real por tipo de requisição (302/redirect no
HTML; 403 genérico em AJAX) e reescrever `PC-BASICPAGE-13` para citar `V2PermissionMiddleware.php:102-121`
e `BasicPageCmsController.php:287-289` como duas camadas, deixando claro que a segunda é
inalcançável para perfis sem `write`.

### G3-02 — Erro de favicon do tema CMS não termina em 500 no caminho HTTP

**Severidade: P2.** **Confiança: 0.93.**

`05_modulos/Support/SupportConfigs/spec.md:321` e `:587` dizem que o `InvalidArgumentException` do
favicon “o controller não captura → 500”. O serviço realmente lança (`SystemMaintenanceService.php:491-536`),
mas o único formulário HTTP que envia `cms_favicon` é o `theme.save` do SLE; `SleCmsController::saveTheme`
envolve `UserConfigsCmsController@update` em `try/catch (\Exception)` e, no erro, grava
`Erro ao salvar tema: ...` e redireciona (`SleCmsController.php:155-159`). `UserConfigsCmsController@update`
apenas re-lança (`UserConfigsCmsController.php:30-35`), mas o chamador captura. O resultado observável
é `302` + flash, não `500`.

**Impacto:** o contrato de erro do painel fica errado; um teste de upload de favicon inválido espera
500 e recebe redirect com flash.

**Correção recomendada:** trocar `500` por `302` + flash `Erro ao salvar tema: ...`; registrar que o
`InvalidArgumentException` é do serviço e que o `500` só ocorreria se `saveCmsThemeConfigs` fosse
chamado sem o `catch` do SLE. Ajustar também a linha de “Códigos” em §11.

### G3-03 — “Todos os outros providers usam array” é falso

**Severidade: P3.** **Confiança: 0.99.**

`SupportConfigs/spec.md:162` e `rotas.md:262` justificam o defeito dizendo que “todos os outros
providers usam array de padrões”. Vários providers usam string única e funcionam:
`Infraestrutura/CertificadoDigital/ModuleProvider.php:25`, `Infraestrutura/MailAudit/ModuleProvider.php:20`,
`Layouts/BasicPage/ModuleProvider.php:28`, `Layouts/Homepage/ModuleProvider.php:44`,
`Support/StructuralLandingEditor/ModuleProvider.php:35` e `_Exemplos/ExemploAPI/ModuleProvider.php:27`.
O defeito é o `|` literal dentro da string, não o tipo string.

**Impacto:** a invariante documentada induz a converter patterns válidos em arrays e esconde que
a string única é suportada; a causa raiz (`|` escapado por `Str::is`) fica diluída.

**Correção recomendada:** dizer “strings sem `|` funcionam; o problema é a string com `|`, que
`Str::is()` trata como literal”, citando um provider de string única além do array de Noticias.

### G3-04 — Mapa de escritores de `v2_portal_configs` inclui leitores e omite escritores

**Severidade: P3.** **Confiança: 0.97.**

`SupportConfigs/dados.md:274` afirma que `v2_portal_configs` é escrita por “SEO, Associados,
Fale Conosco, História”. `FaleConoscoPortalController.php` apenas lê `config_group='CONTATO'` e as
chaves `contato_*` (`:56-58,116-142`); não há `insert`/`updateOrInsert` no módulo. A linha 485
lista `StructuralPortalSeeder.php:60`, que também só lê `titulo_site`. Escritores reais incluem
`UnidadeRegional/Controllers/UnidadeRegionalCmsController.php:501` (`theme_regionais_bg`),
`Conexoes/Associados/Controllers/InstituicaoCmsController.php:519` (`theme_associados_bg`),
`AssocieSeConfigController.php:83`, `MilestoneCmsController.php:319` e os seeders/migração de SEO.
A linha 628 repete “Fale Conosco” entre os que acrescentam chaves.

**Impacto:** o mapa de propriedade/escrita do dado fica incorreto; uma reimplementação pode criar
um escritor inexistente em Fale Conosco ou omitir a Unidade Regional.

**Correção recomendada:** separar “quem escreve” de “quem lê”; retirar Fale Conosco e
`StructuralPortalSeeder` da lista de escritores e incluir `UnidadeRegionalCmsController` e
`InstituicaoCmsController`.

### G3-05 — Referência cruzada `LG-SLE-03` aponta para a lacuna errada

**Severidade: P3.** **Confiança: 0.99.**

`StructuralLandingEditor/spec.md:92` diz que a ausência de seed da permissão `LayoutBase` é
`[L] LG-SLE-03`. Na tabela de lacunas, `LG-SLE-01` é exatamente “não há seed de permissão
`LayoutBase`” (`:993`); `LG-SLE-03` é “não há validação/sanitização de HTML/JS” (`:995`).

**Impacto:** a rastreabilidade da lacuna de permissão aponta para um risco de segurança diferente.

**Correção recomendada:** trocar `LG-SLE-03` por `LG-SLE-01` na linha 92.

### G3-06 — RN-SLE-15 não vale para `comunicado-global`

**Severidade: P3.** **Confiança: 0.95.**

`StructuralLandingEditor/editores-temas-e-assets.md:372` afirma que `logos` e `comunicado-global`
“preservam o payload antigo e só sobrescrevem as chaves enviadas; campos não enviados não somem”.
`handleLogosUpdate` preserva o payload e só altera as chaves recebidas (`LayoutBaseCmsController.php:63-105`),
mas `handleComunicadoGlobalUpdate` sempre atribui `url_destino`, `start_date`, `end_date` e
`banner_image_alt` (`:217-225`), inclusive com `null` quando ausentes; só `banner_image` é
preservado. Um POST de banner sem esses campos zera-os.

**Impacto:** a semântica de merge do banner é documentada como replace parcial, mas é replace
total de quatro campos.

**Correção recomendada:** separar a regra em duas: `logos` = merge parcial; `comunicado-global` =
preserva apenas `banner_image`, sobrescrevendo os demais campos com o valor enviado (ou `null`).

### G3-07 — `PC-SLE-12` superestima a invalidação de cache

**Severidade: P3.** **Confiança: 0.97.**

`StructuralLandingEditor/spec.md:1013` (`PC-SLE-12`) exige que “qualquer submit de escrita” limpe
`cache.prefix:page:*`. Apenas `saveMeta`, `saveTheme`, `saveStructuralData`, `saveFeatures` e
`saveEditor` chamam `clearPageCache` (`SleCmsController.php:104,172,222,298,403`);
`assetsUpload`, `assetsFromGallery` e `assetsDelete` (`:423-531`) não limpam. Os assets não alteram
o HTML por si sós, mas o critério literal falha.

**Impacto:** um teste que faça upload/exclusão de asset e espere a limpeza de `page:*` falha;
a extensão da invalidação fica ambígua.

**Correção recomendada:** restringir o critério às cinco ações de conteúdo/configuração e
registrar explicitamente que assets não invalidam o cache.

### G3-08 — `PC-SLE-08` espera `compile error` onde o runtime produz `eval error`

**Severidade: P3.** **Confiança: 0.9.**

`StructuralLandingEditor/spec.md:1009` (`PC-SLE-08`) espera `<!-- SLE compile error -->` para
“erro de sintaxe em bloco”. Em `BlockRenderer::renderBlock`, o `catch (\Exception)` externo só
captura falha de `compileString`; um erro de sintaxe PHP gerado por `@if`/`{{ }}` só aparece durante
o `eval`, capturado pelo `catch (\Throwable)` interno, que devolve `<!-- SLE eval error -->`
(`BlockRenderer.php:154-165`). O texto `compile/eval error` da RN-SLE-11 está correto; o critério
específico não.

**Impacto:** um teste de paridade com Blade sintaticamente inválido espera o comentário errado.

**Correção recomendada:** reescrever `PC-SLE-08` para distinguir `compile error` (falha em
`compileString`) de `eval error` (falha em PHP gerado), ou testar um caso que realmente falhe na
compilação.

### G3-09 — Fallback do `<title>` de página runtime está errado

**Severidade: P3.** **Confiança: 0.97.**

`Layouts/BasicPage/spec.md:768` diz que, sem `metadata.seo`, o `<title>` “cai em `pageTitle`
ausente → `siteTitle` do `v2_portal_configs`”. `getSeoData()` retorna
`$defaults['title'] ?? $this->title` (`BasicPage.php:209-216`); para qualquer página runtime,
`$this->title` é obrigatório e não vazio, então `bindBasicPage` sempre chama `setTitle($this->title)`
(`BasicPagePortalController.php:30-34`). O `siteTitle` só aparece se o título da página for vazio,
o que o `store` impede (`title` required).

**Impacto:** a reimplementação pode omitir o fallback para `v2_basic_pages.title` e produzir
`<title>` sem o título da página.

**Correção recomendada:** documentar a ordem `metadata.seo.title > defaults do slug > title da
linha`; `pageTitle`/`siteTitle` são apenas fallback do partial quando `$seoTitle` não é definido.

### G3-10 — Contagem do LSE contradiz W2

**Severidade: P3.** **Confiança: 0.95.**

`Layouts/BasicPage/spec.md:400` e `runtime-e-seo.md:341` dizem que o LSE tem “22
formulários/renderizadores”. O documento W2 (`03_servicos-ui/landing-sections-editor/spec.md:115,340`)
já corrigiu isso: são **21 tipos padrão** no registry, e o README diz 22; no boot normal, os
widgets de módulo (Homepage e Sobre Nós) elevam o total a 36. O G2 registrou o mesmo achado
(`review-g2.md`, G2-04).

**Impacto:** a comparação SLE × LSE × BasicPage usa um número que W2 declara incorreto,
enfraquecendo a consistência cruzada do corpus.

**Correção recomendada:** usar “21 tipos padrão (36 no boot normal, com os 15 widgets de módulo)”
ou apenas “seções tipadas”, sem repetir o 22 do README.

### G3-11 — Inventário de rotas CMS do BasicPage tem a divisão GET/POST invertida

**Severidade: P3.** **Confiança: 0.99.**

`Layouts/BasicPage/spec.md:24` declara “15 rotas CMS (10 `GET`, 5 `POST`)”. `Routes/cms.php:10-29`
declara 15 endpoints, mas são **9 GET** (`index`, 5 previews, `create`, `assets`, `editor/{slug}`)
e **6 POST** (`seo/update`, `store`, `upload`, `from-gallery`, `delete`, `save`). A tabela §5 lista
os 15 corretamente; só a ficha está errada.

**Impacto:** o inventário do gate de rotas fica inconsistente com o próprio documento e com o CSV.

**Correção recomendada:** trocar para “9 `GET` e 6 `POST`” na ficha.

### G3-12 — Faixas de citação fora do arquivo e âncoras deslocadas

**Severidade: P3.** **Confiança: 0.99.**

`lint_docs.py` encontrou 9 faixas que terminam depois da última linha do arquivo:

| Documento | Citação | Linhas reais |
|---|---|---|
| `SupportConfigs/spec.md:639` | `V2/Core/Middleware/MaintenanceMiddleware.php:1-56` | 52 |
| `SupportConfigs/spec.md:640` | `V2/Core/Services/ModuleFlagService.php:1-135` | 127 |
| `SupportConfigs/dados.md:666-667` | os mesmos dois arquivos | 127/52 |
| `SupportConfigs/rotas.md:559-560` | `MaintenanceMiddleware.php:33-55`; `ModuleFlagService.php:24-135` | 52/127 |
| `SupportConfigs/rotas.md:563` | `StructuralLandingEditor/Routes/cms.php:18-70` | 63 |
| `BasicPage/spec.md:778` | `ModuleAccessMiddleware.php:47-72` | 67 |
| `BasicPage/spec.md:855` | `create_basic_pages_table.php:17-115` | 91 |

Além disso, há âncoras dentro do arquivo mas deslocadas: `StructuralLandingEditor/ui.md:375` cita
`links-header.blade.php:32,41`, mas o botão “Adicionar” está em `:24` e o “Salvar” em `:33`;
`ui.md:576` cita `asset-sidebar.blade.php:117-120`, mas o `<script>` do jQuery está em `:130`;
`BasicPage/spec.md:418` cita `Views/cms/index.blade.php:34`, mas o `</header>` órfão está em `:35`.
No SupportConfigs, o trecho de `ModuleProvider.php:82-94` também desloca o `getConfig` (início em
`:80`).

**Impacto:** a citação é o mecanismo de verificação do corpus; faixas inexistentes ou deslocadas
obrigam o leitor a corrigir de cabeça e enfraquecem o gate de evidência.

**Correção recomendada:** corrigir as faixas para `MaintenanceMiddleware.php:1-52`/`:33-52`,
`ModuleFlagService.php:1-127`/`:24-127`, `StructuralLandingEditor/Routes/cms.php:18-63`,
`ModuleAccessMiddleware.php:47-67`, `create_basic_pages_table.php:17-91`,
`links-header.blade.php:24,33`, `asset-sidebar.blade.php:130` e `index.blade.php:35`; rodar
`lint_docs.py --strict-citations` antes de fechar a onda.

### G3-13 — Índices parciais não cobrem os blades de SupportConfigs/NoticiasImport

**Severidade: P2 (gate).** **Confiança: 0.99.**

`_indices/parcial-MOD-SUPPORT-SUPPORTCONFIGS/` não tem `componentes.csv`, embora a unidade tenha 5
blades (`Views/cms/supportConfigs/index.blade.php` e `tabs/{logs,manutencao,modulos,suporte}.blade.php`).
`_indices/parcial-MOD-SUPPORT-NOTICIASIMPORT/` também não tem `componentes.csv`, embora exista
`Views/cms/index.blade.php`. O índice canônico `_indices/componentes.csv` tem **0** ocorrências
desses 6 caminhos, enquanto SLE (35) e BasicPage (13) estão cobertos. Além disso,
`parcial-MOD-SUPPORT-SUPPORTCONFIGS/rotas.csv` não tem linhas `tipo=grupo` para os dois
`Route::group` do arquivo (`cms/infra/support-configs` e `cms/infra/user-configs`), contrariando
`TAXONOMIA.md` §8.

**Impacto:** a consolidação do G4 não consegue cobrir os 386 blades (Lei de telas) e a conciliação
de `Route::*` fica incompleta para SupportConfigs.

**Correção recomendada:** emitir os dois `componentes.csv` parciais (5 + 1 linhas) e as duas linhas
de grupo em `rotas.csv`; reexecutar `merge_indices.py` e conferir a contagem de `componentes.csv`
canônico.

## 3. O que foi conferido sem achado

Para registrar profundidade, além da forma:

- **SupportConfigs:** `active_pattern` com `|` realmente não casa (`Str::is` faz `preg_quote`);
  `V2PermissionMiddleware` só aplica o grupo `Support` com `$matched`; a guarda por action está
  nas 10 actions e falta em `resetCmsTheme` (SEC-G1-04); `maintenance_mode` só é ativo com `'true'`
  e o dashboard aceita `'1'` (`helpers.php:236-248`; `DashboardStatsService.php:394-398`);
  `toggleModule` cria `OUTROS`, `Cache::forget('v2_module_flags')` é inócuo e a exceção vira
  `false`; SMTP é cifrado com `Crypt` e `MailService` mantém o ciphertext quando o `decrypt` falha;
  `CmsThemeComposer` alimenta título/favicon/logo do CMS; os seeds 1/7/3/3/18 conferem; a migration
  de FK não toca nas cinco tabelas.
- **SLE:** `BlockRenderer` compila com `compileString` e executa com `eval`, com `$__env` e
  `EXTR_SKIP`; `$siteTitle` é `'Portal'` e `$portalTheme` é `[]` dentro de blocos; `$layoutConfigs`
  vem do fallback de banco; `lai_enabled` é `(bool)` de um array `{"enabled":false}` e por isso
  continua visível; favicon usa caminho fixo e validação de MIME/extensão/tamanho; assets CSS/JS
  gravam em `assets/structuralEditor/*` e imagens na galeria; `saveEditor` desativa áreas vazias;
  `resetCmsTheme` é alcançável por qualquer sessão CMS; SLE/LSE/BasicPage têm papéis distintos
  (casca × seções × página).
- **NoticiasImport:** códigos `401 missing_key`/`invalid_key`/`key_revoked`/`key_expired`,
  `403 ip_not_allowed`/`scope_not_allowed`/`consumer_mismatch`, `422 invalid_dir`/`regional_not_found`,
  `429` com `Retry-After`; ordem `dir` antes de consumidor; `consumer` validado no controller, não
  no middleware; chave estática vincula `null` e cai em `PE`; `resolveDir` usa `realpath` + prefixo
  textual (aceita `imports-backup`, rejeita symlink para fora); `key_hash` sha256 com unique;
  idempotência por `v2_news_import_keys` só em `apply`; contrato `run(dir, options)` e relatório
  conferem com `NoticiasImportService`/`NoticiasImportReport`.
- **BasicPage:** fallback `/{slug}` é do Core e exige `is_runtime=true`; regex `[a-z0-9-]+` retorna
  404 sem banco para maiúsculas/acentos/underscore; `metadata` é duplamente codificado em `store()`
  (cast `array` + `json_encode`) e `getSeoData`/`getLayoutConfig` caem nos defaults; `content` é
  ecoado cru sem Blade; `saved` reindexa em `v2_public_search_index`/`v2_cms_links_index`; cache
  Redis de 5 min não é invalidado ao salvar; assets não são injetados no portal; páginas sombra de
  `noticias`, `associados`, `convenios`, `agenda`, `fale-conosco` e `parceiros-institucionais`
  existem e não são renderizadas pelo catch-all.

## 4. Correções recomendadas por documento

| Documento | Correções |
|---|---|
| `SupportConfigs/spec.md` | G3-02 (500 → 302 + flash), G3-03 (string com `|`), G3-04 (escritores), G3-12 (faixas). |
| `SupportConfigs/dados.md` | G3-04 (Fale Conosco/StructuralPortalSeeder são leitores; incluir Unidade Regional), G3-12. |
| `SupportConfigs/rotas.md` | G3-03, G3-12; acrescentar linhas de grupo ao CSV parcial (G3-13). |
| `SupportConfigs/cms.md` | Sem erro semântico; apenas coerência de contagem de diagnóstico (10 chaves × 9 rótulos), se desejado. |
| `StructuralLandingEditor/spec.md` | G3-05 (`LG-SLE-01`), G3-07 (`PC-SLE-12`), G3-08 (`PC-SLE-08`), G3-12 (`ModuleProvider.php:35`). |
| `StructuralLandingEditor/editores-temas-e-assets.md` | G3-06 (merge de `comunicado-global`). |
| `StructuralLandingEditor/ui.md` | G3-12 (âncoras `links-header` e jQuery). |
| `NoticiasImport/spec.md` | Sem erro material encontrado; manter. |
| `NoticiasImport/contrato-api.md` | Sem erro material encontrado; opcional: ressalvar que 405 não é JSON. |
| `BasicPage/spec.md` | G3-01 (403), G3-09 (`<title>`), G3-10 (LSE), G3-11 (9/6), G3-12. |
| `BasicPage/runtime-e-seo.md` | G3-10 (LSE). |
| `_indices/parcial-MOD-SUPPORT-SUPPORTCONFIGS/` | G3-13: `componentes.csv` (5) e linhas de grupo em `rotas.csv`. |
| `_indices/parcial-MOD-SUPPORT-NOTICIASIMPORT/` | G3-13: `componentes.csv` (1). |
| `_indices/parcial-MOD-LAYOUTS-BASICPAGE/` | Sem lacuna de componentes; `permissoes.csv` é opcional porque o canônico já traz a linha de CORE-AUTH. |

## 5. Veredito

**Aprovado com ressalvas.** Os 11 documentos cumprem o objetivo central do G3: descrevem efeito
real de tema/flags/manutenção, o runtime por `eval`/`BlockRenderer`, o contrato da API interna, o
catch-all e o fallback Blade, com trechos literais e receitas. Não há achado P1 nem divergência
estrutural que impeça a Onda 4.

As ressalvas são: três P2 (contrato HTTP de `updateSeo` e de favicon, e cobertura de
`componentes.csv`) e os P3 de consistência (provider string, mapa de escritores, merge de banner,
critérios de paridade, fallback de `<title>`, contagem do LSE, inventário GET/POST e citações).
Corrigidos os P2 e as citações, o gate pode ser fechado sem nova leitura integral.
