# Aproveitamento no V7 — o que portar, adaptar e rejeitar

> Julgamento de portabilidade. ADRs relativas a `public-v7/wiki/adr/`.
> Caminhos legados relativos a `lotep/`; caminhos novos relativos a `src/`.
>
> **Veredito:** o contrato de **seções ordenadas** e o **catálogo de widgets** são aproveitáveis; o
> **armazenamento e a execução de HTML/Blade** não são. A reescrita troca "HTML no banco" por
> **blocos estruturados em `jsonb`**, mantendo ordem, ocultação, defaults e validação por tipo.

---

## 1. Portar

### 1.1 Contrato JSON de seções (o ativo principal)

Portar a **semântica** de `homepage_sections` v3:

- documento versionado (`version` inteiro);
- lista ordenada (`sections`);
- por item: `type`, `parameters`, `id` opcional, `hide` opcional;
- `home-header` fora da lista (config de slot);
- seções dinâmicas como marcadores de posição;
- "sem config = ordem default".

Isso é exatamente a forma de um layout como dado (ADR-016: "ordem/visibilidade/conteúdo em JSONB").
No V7, o documento vira **linhas ordenadas** (`layout_slots`) ou um JSONB de layout — a decisão de
schema está em `06-MIGRACAO-MODERNA.md` §2.

Parser puro, sem banco (exemplo ilustrativo):

```php
// exemplo ilustrativo
declare(strict_types=1);

namespace Layout\Portal;

final class ParseHomepage
{
    /** @param array<string, mixed> $payload */
    public function parse(array $payload): HomepageTree
    {
        if (($payload['version'] ?? 0) !== 4 || !is_array($payload['sections'] ?? null)) {
            return HomepageTree::default();
        }

        $order = [];
        $hidden = [];
        $blocks = [];

        foreach ($payload['sections'] as $item) {
            if (!is_array($item) || !is_string($item['type'] ?? null)) {
                continue;
            }

            $type = $item['type'];
            if (!HomepageTypes::knows($type)) {
                continue;
            }

            $order[] = $type;
            if (($item['hidden'] ?? false) === true) {
                $hidden[] = $type;
            }
            if (HomepageTypes::isStatic($type) && is_array($item['props'] ?? null)) {
                $blocks[$type] = $item['props'];
            }
        }

        return new HomepageTree($order, $hidden, $blocks, HomepageTypes::defaultOrder());
    }
}
```

Regras preservadas:

| Regra V2 | Teste que a protege |
|---|---|
| `null`/versão errada ⇒ ordem default | `HomepageSectionsServiceTest::test_parse_config_without_config_returns_defaults` |
| `hide` preserva posição | `HomepageSectionsServiceTest::test_parse_config_hide_flag_marks_hidden_and_preserves_position` |
| dinâmica não expõe `parameters` | `HomepageSectionsServiceTest::test_parse_config_dynamic_content_is_never_exposed_as_section_data` |
| header fora da ordem | `HomepageSectionsServiceTest::test_parse_config_extracts_header_widget_outside_sections_order` |
| opcional não entra no default | `HomepageSectionsServiceTest::test_jogos_autorizados_is_optional_not_in_default_order` |

### 1.2 Catálogo de widgets

Portar o **conceito** de `WidgetContract` + `WidgetRegistry`, não a implementação estática:

| Método V2 | Equivalente V7 |
|---|---|
| `getIcon()` / `getTitle()` | campos de `WidgetDefinition` |
| `getValidationRules()` | `LandingBlockRules::for($type)` |
| `getFormView()` | componente `<x-landing::block-form>` escolhido por `type` |
| `getRenderView()` | componente de render em código |
| `getAttachmentIds()` | `MediaReference::collect($props)` por tipo |
| `prepare()` | `MediaResolver::resolve($props, $media)` |
| `sanitize()` | **não existe**: dados são validados e a saída é escapada |

Tipos a portar:

| Grupo | Tipos | Ação |
|---|---|---|
| Home (9) | `home-hero`, `home-acesso-rapido`, `home-papel-regulatorio`, `home-destaques-cidadao`, `home-canais-atendimento`, `home-jogo-responsavel`, `home-imprensa`, `home-dashboard`, `home-agenda` | portar com `props` tipadas |
| Home opcional (2) | `home-jogos-autorizados`, `home-comunicado` | portar (`home-comunicado` vira AST) |
| Conteúdo (21) | `hero`, `navigation`, `icons-text`, `text-image`, `text-only`, `only-image`, `two-columns`, `boxes`, `carousel`, `cards-numbered`, `cards-links`, `step-by-step`, `video`, `video-playlist`, `interactive-table`, `grid`, `questions-answers`, `multi-card`, `text-carousel`, `command-line`, `buttons` | portar os úteis; descartar os que o design system cobre |
| Perigosos (2) | `html-free`, `bloco-sle` | **rejeitar** |

O catálogo do V7 é instância por request, registrado no manifesto do módulo (ADR-014), sem
`static` (ADR-013).

### 1.3 Preparação de mídia em lote

Portar a ideia de **uma consulta** por render:

```php
// exemplo ilustrativo
$ids = $this->references->collect($blocks);          // todos os media_id
$media = $this->gallery->findMany($ids);              // UMA query, sem deleted_at
$blocks = $this->mediaResolver->resolve($blocks, $media);
```

No V7 a consulta deve ir pelo serviço Gallery (não `DB::table`), filtrar `deleted_at` e devolver
variantes AVIF/WebP (ADR-026). O render usa `<x-service-media::picture>` com `media_id`, `alt`,
`width`, `height`.

### 1.4 Regras de validação

Portar os **limites** dos 31 schemas, não as 31 classes:

| Campo | Limite que deve sobreviver |
|---|---|
| títulos/eyebrows | 120–160 caracteres conforme o tipo |
| `lead` | 300–400 |
| itens de lista | `array`, no máximo 12 na acesso rápido |
| botões | 1–5 em `buttons` |
| cards | 1–3 em `multi-card` |
| URLs | `max:255`; `url` onde aplicável |
| mídia | `uuid` + `exists:gallery_media,id` |
| cores | `#rgb`/`#rrggbb` (ou token do tema) |
| enums | alinhamento, largura, altura, posição |
| `template` / `html_content` | **não portar** |

O `exists` deve virar `exists:gallery_media,id` com a tabela do serviço Media/Gallery; validação no
`FormRequest`, não no controller (ADR-020).

### 1.5 Padrões de UI

- Lista ordenável por botões de mover ou drag-and-drop.
- Modal por tipo, com o form carregado sob demanda.
- Preview em iframe espelhando o portal.
- Validação server-side antes de fechar o modal.
- Botão "salvar" protegido por permissão.
- Histórico de alterações visível.

Tudo isso pode ser reimplementado com JS mínimo e `<dialog>` nativo (ADR-010), sem jQuery.

---

## 2. Adaptar

| Ativo V2 | Como adaptar | Regra |
|---|---|---|
| `v2_layout_base_configs` | `layout_configs.key/value jsonb` com validação por chave; sem soft delete | ADR-016, ADR-017 |
| Áreas fixas (`header`, `footer`, `main_left`, `main_right`, `css_head`, `css_foot`, `js_head`, `js_foot`) | regiões de `layout_slots`; CSS/JS globais viram tema + build | ADR-016, ADR-022 |
| Tema visual e flags | `layout_configs` tipadas + tokens `oklch()`; acessibilidade como camada universal | ADR-016, ADR-010 |
| Logos/favicon/banner | IDs de `gallery_media` + `<x-service-media::picture>` | ADR-026 |
| Links/contato/redes/LAI | JSONB validado por schema; render em componentes | ADR-017 |
| `homepage_sections` | `layout_slots` da página `portal.home`; parser puro | ADR-016 |
| `home-comunicado` | corpo em AST TipTap, render semântico | ADR-028 |
| Metadados/SEO da tela | serviço SEO com `@graph` | ADR-027 |
| Permissão | `layout.ver/editar/aprovar/arquivar` | ADR-033 |
| Auditoria | `audit_log` nativo com `before/after` | ADR-035 |
| Ativação do módulo | `CheckModuleActivation` | ADR-034 |
| Preview | cache por usuário, TTL curto, sem token público | ADR-020, ADR-022 |
| Área restrita | `cms.[base]`, `noindex`, CSP estrita | ADR-043 |

---

## 3. Rejeitar

| Item V2 | Evidência | Por que rejeitar | ADR |
|---|---|---|---|
| `BlockRenderer` (`compileString` + `include`) | `V2/Modules/Support/StructuralLandingEditor/Services/BlockRenderer.php:143-231` | executa código arbitrário vindo do banco; impede CSP sem inline; RCE | ADR-017, ADR-020, ADR-022 |
| HTML/CSS/JS em `text` | `V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_03_000000_create_v2_structural_portal_blocks_table.php:26`; `V2/Modules/Support/StructuralLandingEditor/Views/cms/tabs/css-head.blade.php` | apresentação não é dado | ADR-016, ADR-017 |
| `snippet` com Blade | `V2/Modules/Support/StructuralLandingEditor/Services/AssetManagerService.php:216-246` | trecho executável persistido e copiado | ADR-017, ADR-022 |
| `strip_tags` como sanitização | `V2/Services/LandingSectionsEditor/Widgets/BaseWidget.php:62-72` | não é sanitizador de HTML; destrói texto rico | ADR-020, ADR-028 |
| Sanitize por regex | `V2/Services/LandingSectionsEditor/Widgets/HomeComunicadoWidget.php:22-34` | bypass trivial; falsa sensação de segurança | ADR-020 |
| `html-free` | `V2/Services/LandingSectionsEditor/Widgets/HtmlFreeWidget.php` | HTML arbitrário no portal | ADR-016, ADR-017 |
| `bloco-sle` | `V2/Services/LandingSectionsEditor/Widgets/BlocoSleWidget.php`; `V2/Services/LandingSectionsEditor/Views/render/bloco-sle.blade.php` | template executável; pior risco do módulo | ADR-017, ADR-020 |
| Editor de código CSS/JS no CMS | `V2/Modules/Support/StructuralLandingEditor/Views/cms/submodules/editor/index.blade.php` | CSS/JS são build, não dado | ADR-002, ADR-022 |
| Upload de JS/CSS | `V2/Modules/Support/StructuralLandingEditor/Services/AssetManagerService.php:26-55` | script same-origin arbitrário | ADR-002, ADR-020 |
| Gate por e-mail fixo | `V2/Modules/Support/StructuralLandingEditor/Controllers/SleCmsController.php:35-40`; `V2/Modules/Support/StructuralLandingEditor/ModuleProvider.php:37` | não é RBAC; um login concentra tudo | ADR-033 |
| Serviço com rotas/UI | `V2/Services/LandingSectionsEditor/README.md:17-34` | serviço deve ser headless | ADR-013, ADR-015 |
| Serviço importando módulos | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:16`; `V2/Services/LandingSectionsEditor/Views/preview.blade.php:44-50` | viola a fronteira física | ADR-013 |
| Módulo importando módulo | `V2/Modules/Institucional/BasicPage/Services/HomepageDataService.php:12` | idem | ADR-013 |
| Registry estático | `V2/Services/LandingSectionsEditor/Services/WidgetRegistry.php:9` | estado global, difícil testar | ADR-013 |
| Preview por token de cliente | `V2/Services/LandingSectionsEditor/Controllers/LandingSectionsEditorController.php:133-171` | cache global, sem vínculo com usuário | ADR-020 |
| `transaction_snapshot_id` sem uso | `V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_03_000001_create_v2_layout_base_configs_table.php:28`; nenhuma escrita | ruído de schema | ADR-017 |
| Soft delete em config com `key` UNIQUE | `V2/Modules/Support/StructuralLandingEditor/Models/LayoutBaseConfig.php:41`; `V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_03_000001_create_v2_layout_base_configs_table.php:26` | colisão no próximo save | ADR-017 |
| `array_set()` | `V2/Services/LandingSectionsEditor/Services/LandingSectionsEditorRendererService.php:364` | helper removido do Laravel moderno | ADR-001 |
| Duplicação de defaults PHP/Blade | `V2/Modules/Support/StructuralLandingEditor/Services/HomepageSectionsService.php:97-170`; `V2/Modules/Institucional/BasicPage/Views/portal/homepage/partials/` | duas fontes de verdade | ADR-016 |

---

## 4. Mapa concreto V2 → V7

| V2 | V7 |
|---|---|
| `V2/Modules/Support/StructuralLandingEditor` | `src/modules/Support/StructuralLandingEditor` (CMS) |
| `V2/Services/LandingSectionsEditor` | `src/services/Landing` (headless) + `src/layout/Portal` (componentes) |
| `v2_structural_portal_contents` | `layout_slots` + componentes |
| `v2_layout_base_configs` | `layout_configs` |
| `v2_sle_assets` | `gallery_media` + build Vite |
| `HomepageSectionsService` | `Layout\Portal\ParseHomepage` + `HomepageTypes` |
| `WidgetRegistry` | `Services\Landing\LandingWidgetCatalog` |
| `WidgetContract` | `WidgetDefinition` (value object) |
| `LandingSectionsEditorRendererService::prepareSections` | `Services\Landing\MediaResolver` + Gallery |
| 31 `*Schema` | `LandingBlockRules` + Form Requests |
| `SleCmsController` | `LayoutCmsController` (ações pequenas) |
| `LayoutBaseComposer` | resolver de layout cacheado |
| `V2AuditLogger` | auditoria nativa ADR-035 |
| rotas `/cms/infra/structural-editor/*` | `/cms/layout/*` (CMS próprio) |
| rotas `/cms/v2/landing-sections/*` | `/cms/layout/{key}/*` com Gate |

---

## 5. O que **não** pode mudar

| Invariante | Verificação |
|---|---|
| Home sem config = 9 seções na ordem atual | snapshot de HTML |
| `hide` mantém posição e configuração | teste de contrato |
| Header é configuração, não seção | teste do parser |
| Dinâmicas vêm do módulo de origem | teste de render |
| Opcional (`jogos-autorizados`) fora do default | teste do parser |
| Comunicado pode substituir a home com aviso acessível | teste de render + `role=status` |
| Acessibilidade liga/desliga por flag | teste por flag |
| Nenhum HTML do legado é executado | teste que rejeita `template`/`html_content` |

ADR-042 fixa que o conteúdo é migrado **à mão**; portanto não há obrigação de conversão automática
de HTML legado. Isso autoriza descartar os blocos de código livre em vez de emulá-los.

---

## 6. Ordem de portabilidade

1. **Semana 1:** `HomepageTypes`, `ParseHomepage`, testes de contrato (sem banco).
2. **Semana 2:** migrations `layout_configs`/`layout_slots`, seed da home default, cache de resolução.
3. **Semana 3:** catálogo + 9 widgets da home + componentes de render.
4. **Semana 4:** editor (form por tipo, drag, hidden), Gate e auditoria.
5. **Semana 5:** preview por sessão, publicação, testes de autorização.
6. **Semana 6:** migração manual do conteúdo, remoção do legado, snapshot final.

---

## 7. Critérios de aceite do aproveitamento

- [ ] O parser novo passa os 11 casos de `HomepageSectionsServiceTest` (adaptados).
- [ ] Nenhuma tabela nova tem coluna de HTML/template.
- [ ] Nenhum `eval`, `include` de arquivo compilado ou `compileString` no código novo.
- [ ] Toda rota de escrita tem Gate `layout.{ação}`.
- [ ] Preview não é acessível por token de outra sessão.
- [ ] Mídia resolvida em uma única consulta e via serviço Gallery.
- [ ] Auditoria registra ator e `before/after`.
- [ ] O HTML da home default é idêntico ao aprovado na baseline visual.
