# Legislação — Gaps e Dívidas

> **Slug:** `Legislacao` · Lacunas funcionais, bugs observáveis e riscos de dados.
>
> Caminhos relativos a `lotep/`.

---

## 1. Bugs e erros conhecidos

### 1.1 Colisão de slug em legislação gera erro 500

`V2Legislation` gera slug no `creating`/`updating` sem verificar existência
(`V2/Modules/Publicacoes/Legislacao/Models/V2Legislation.php:82-94`). Como
`slug` tem `UNIQUE`, cadastrar duas normas com o mesmo título (ou recadastrar
após excluir) viola a constraint e resulta em erro de banco, não em mensagem de
validação. O importador contorna com sufixo aleatório
(`Controllers/Admin/LegislationCmsController.php:444-451`), mas o formulário não.

### 1.2 Exclusão de legislação não libera o slug

`destroy` (`:130-145`) apenas soft-deleta a norma. O slug permanece ocupado
porque o índice único não é parcial. Recriar uma norma com o mesmo título depois
da exclusão cai no problema 1.1. Diferente do que acontece com categorias, não há
renomeação do slug na exclusão.

### 1.3 Reimportação apaga o conteúdo rico

`legislationImportAttrs()` sempre define:

```php
'content' => '{"type":"doc","content":[]}',
```

(`Controllers/Admin/LegislationCmsController.php:429`)

No caminho de atualização por `number+year`, o `$attrs` é aplicado inteiro
(`:353-357`), portanto **o texto rico existente é substituído por um documento
vazio**. Uma reimportação de planilha para corrigir a data apaga o conteúdo
editorial. Também força `is_highlighted = false` (`:433`).

### 1.4 XSS armazenado no conteúdo

O detalhe público imprime o HTML do renderer sem escape:

```blade
{!! $htmlContent !!}
```

(`V2/Modules/Publicacoes/Legislacao/Views/portal/show.blade.php:48`)

O parser escapa nós de texto, mas:

- `ContentRichHtmlFreeRenderer` devolve `attrs.html` cru
  (`V2/Services/ContentRichEditor/Parser/Nodes/ContentRichHtmlFreeRenderer.php:21-24`);
- `LinkRenderer` apenas faz `htmlspecialchars` no `href`, sem allowlist de
  esquema (`V2/Services/ContentRichEditor/Parser/Marks/LinkRenderer.php:30-36`),
  aceitando `javascript:`.

O comentário da view afirma que o HTML é "sanitizado na gravacao"; não há
sanitizador no fluxo do editor nem no módulo.

### 1.5 `view_pdf` sem efeito para não-PDF

`view_pdf` é aceito e persistido para qualquer arquivo. O portal só abre em nova
aba quando `view_pdf && is_pdf` (`portal/show.blade.php:99-109`); para os demais,
cai no download. A UI de upload esconde a opção para não-PDF
(`attachments-modals.blade.php:96-103`), mas a API/rota aceita o valor e o banco
guarda um estado sem efeito.

### 1.6 Categoria com slug longo pode estourar o limite

`categoriesDestroy` concatena `-deleted-` + 6 caracteres ao slug
(`:233-236`) sem checar o tamanho. Um slug próximo de 255 caracteres gera valor
maior que a coluna e falha na gravação, impedindo a exclusão.

### 1.7 `file_url` órfão e link principal sem validação

- O portal mostra "Baixar documento principal" para qualquer `file_url`,
  inclusive URL `javascript:` ou path inexistente; não há validação no request
  além de `string|max:500`.
- Anexos com `legislation_id = NULL` (após hard delete, que hoje não ocorre pela
  UI) ficam inacessíveis e podem permanecer no storage.

### 1.8 Exclusão física incompleta

`LegislationFileService::delete()` só remove o arquivo quando o caminho começa
por `legislacao/` ou `gallery/legislacao/`
(`V2/Modules/Publicacoes/Legislacao/Services/LegislationFileService.php:152-158`).
Uploads antigos, assets demo e caminhos migrados de outros prefixos permanecem no
disco após a exclusão do registro.

### 1.9 Sem reordenação na interface

A rota `v2.legislacao.files.reorder` e o método `reorder()` existem e são
testados, mas nenhuma view faz a chamada. O ícone de arraste é decorativo
(`admin/includes/attachments.blade.php:14`).

### 1.10 Destaque sem UI

`is_highlighted` está no schema, no `fillable` e nas regras do request, mas não
aparece em nenhum formulário. Só o importador legado o define.

### 1.11 Observabilidade silenciosa no SEO

`show()` envolve as chamadas de `SEOService` em `try/catch` vazio
(`Controllers/Portal/LegislationPortalController.php:70-77`): uma falha de SEO
não gera log nem telemetria.

## 2. Funcionalidades incompletas ou mortas

| Item | Estado | Evidência |
|---|---|---|
| API pública de legislação | Inexistente na V2 | O módulo não possui arquivo de rotas de API. |
| Exportação CSV/XLS/PDF | Inexistente | Única exportação é o template de importação. |
| Restauração de norma excluída | Inexistente na UI | `restore()` existe no Eloquent, sem rota. |
| Reordenação de anexos | Backend pronto, UI ausente | `attachments.blade.php` sem chamada. |
| Drag-and-drop de categorias | Inexistente | Ordem é campo numérico. |
| Histórico/versionamento | Inexistente | Sem tabela de versões nem auditoria de texto. |
| Revogação/alteração de norma | Inexistente | Sem campo de vigência ou ato alterador. |
| Órgão emissor/tipo normativo | Inexistente | Só `number`, `year` e categoria. |
| Destaque na UI | Inexistente | Campo sem formulário. |
| Redirect 301 de slug antigo | Inexistente | Mudança de título quebra URL. |
| Antivírus / verificação de conteúdo de arquivo | Inexistente | Só MIME + magic bytes. |
| Cache de página pública | Inexistente no módulo | Sem middleware de cache/ETag. |

## 3. Itens que existem na V1 e foram reduzidos ou removidos na V2

| V1 | V2 | Observação |
|---|---|---|
| Árvore `category_contents` com qualquer profundidade | Categoria plana de um nível | Importador usa a categoria **direta** do conteúdo (`LegislacaoLegacyImporter.php:20-27`). |
| Múltiplos PDFs em `archives` com `order` | Tabela `v2_legislation_files` | Preservado pelo importador; o primeiro vira `file_url`. |
| `archives.description` e `view_pdf` | Colunas equivalentes na tabela de anexos | Acrescentadas em 2026-09-28. |
| Destaque (`highlights`) | `is_highlighted` | Preservado na importação, sem UI. |
| Campos HTML do conteúdo | AST TipTap convertido | Importador converte HTML em AST. |
| API `/api/v1/legislacoes` | Não portada para V2 | O grupo `legislacoes` existe em `routes/api.php:24` (camada legada, fora do módulo V2); o módulo V2 não expõe API. |
| Contador de acessos (`clicks`) | Não existe | Sem métrica de acesso por norma. |

## 4. Riscos de dados na migração de schema

| Risco | Detalhe | Mitigação sugerida |
|---|---|---|
| `content` longText com JSON serializado | Pode haver linha com HTML puro (seed antigo) ou JSON inválido. | Validar `json_decode` por linha; importar como rascunho e sinalizar em `metadata`. |
| `publication_date` nula | Item existe no CMS, mas nunca aparece no portal. | Decidir: herdar `created_at` ou marcar rascunho explícito. |
| Timezone | `datetime` sem fuso comparado com `now()`. | Converter para `timestamptz` no fuso oficial do município. |
| Unicidade `number+year` | Não há índice; pode haver duplicatas reais. | Relatório de conflitos antes de criar o índice único parcial. |
| Slugs repetidos entre soft-deleted e ativos | Índice global impede reaproveitamento. | Unicidade parcial `WHERE deleted_at IS NULL` + histórico de slugs. |
| Anexos com prefixos fora de `legislacao/` | Não foram enviados pelo service e não serão apagados. | Inventariar caminhos, migrar para `Media`, verificar existência física. |
| Anexos com `legislation_id = NULL` | Órfãos após operações administrativas. | Associar por `v1_id`/heurística ou arquivar no catálogo de mídia. |
| `metadata` com `imported_from` | Único uso atual do campo. | Preservar em `metadata.migracao` e não expor na API. |
| Texto rico sem versão | Edição sobrescreve o ato publicado. | Congelar versão inicial na importação e versionar toda alteração. |

## 5. Perguntas em aberto para o negócio

1. **Tipologia normativa:** a categoria atual (Leis/Decretos/Portarias) é o
   "tipo" oficial? O backlog V7 pede `tipo` como campo próprio; a categoria pode
   virar taxonomia secundária.
2. **Órgão emissor:** qual é o vocabulário (LOTEP, Governo do Estado, Assembleia)?
   É obrigatório para citação?
3. **Vigência e revogação:** uma norma revogada continua publicada no portal com
   marcação, ou sai da listagem? Quem aprova a revogação?
4. **Consolidação:** o texto exibido deve ser sempre o consolidado, com opção de
   ver o original? Quem é responsável pela consolidação editorial?
5. **Numeração:** pode haver dois atos com mesmo número e ano de órgãos
   diferentes? Isso invalida a chave natural `number+year` atual.
6. **Assinatura/carimbo do tempo:** todo ato é oficial? Só os publicados no Diário
   Oficial? (backlog ICP-Brasil).
7. **Dados abertos:** qual periodicidade, licença e formato (CSV/JSON/XML) do
   acervo? (ADR-018).
8. **Retenção:** por quanto tempo um rascunho descartado e um anexo removido
   devem ser preservados para auditoria?
9. **URL canônica:** aceita-se `/legislacao/{ano}/{numero}` em vez de slug
   mutável? O backlog pede URL estável para citação.
10. **Migração de slugs:** redirecionar 301 os slugs antigos ou congelar os
    atuais como canônicos?

## 6. Mapa V1 → V2 (para orientar a migração V7)

| V1 | V2 | Regra de conversão |
|---|---|---|
| `contents` da árvore `legislacao` | `v2_legislation` | Título íntegro; `number`/`year` extraídos por regex `n[ºo°] num - ano`. |
| `contents.publication_date` | `publication_date` | Sem data, fica nulo (e invisível no portal). |
| `contents.status` nulo | `is_active = true` | Nulo é tratado como ativo na importação. |
| `contents.highlights` | `is_highlighted` | Preservado. |
| Categoria filha da raiz | `v2_legislation_categories` | Slug V1 preservado; categoria **direta**, não a raiz. |
| `archives` PDF | `v2_legislation_files` | `order` original; menor ordem também vira `file_url`. |
| `archives.description`/`view_pdf` | colunas homônimas | Migração de 2026-09-28. |
| HTML do conteúdo | AST TipTap | Conversão `HtmlToTiptapConverter`. |
| `contents.id` | `v1_id` + `imported_at` | Unique parcial garante idempotência. |

O importador está em
`V2/Modules/Support/MigracaoLegada/Services/LegislacaoLegacyImporter.php` e é
testado por `tests/Unit/V2/Modules/Infraestrutura/MigracaoLegada/LegislacaoLegacyImporterTest.php`.

## 7. Comparação com o backlog V7

`wiki/backlog/modulo-legislacao.md` pede explicitamente:

| Requisito do backlog | Situação no legado |
|---|---|
| Entidade com tipo, número, ano, ementa, órgão, data, situação | Só número, ano, resumo, data e flags; sem tipo, órgão ou situação explícita. |
| Texto consolidado + histórico de alterações/revogações | **Ausente**: uma linha mutável. |
| Busca por tipo/número/ano/órgão + full-text com GIN | Busca parcial por título/resumo/número com `ILIKE`, sem índice. |
| Anexos PDF e URLs canônicas/estáveis | Anexos existem; URL é slug mutável, sem canonical. |
| Exportação de dados abertos | Ausente. |
| Assinatura/carimbo do tempo | Ausente. |

Portanto, o aproveitamento é de **regras de publicação, categorias, anexos e
importação**; o núcleo normativo (versão, consolidação, revogação, citação) é
construção nova, não portabilidade.
