# Arquitetura da camada V2 (LOTEP)

> **Escopo.** A camada `V2/` do repositório `lotep/` — não a V1 (`app/`, `resources/views/prefecture`,
> `routes/basepro.php`). Análise estática; nada foi executado neste projeto (ver [`07-DIVERGENCIAS-E-LICOES.md`](07-DIVERGENCIAS-E-LICOES.md)).
>
> **Stack:** Laravel 5.5 (`composer.json:8`), PHP 7.4.33 (`composer.json:20`), PostgreSQL com FTS,
> Sentinel (`cartalyst/sentinel`), AdminLTE 1.22 no CMS (`composer.json:39`), Bootstrap + jQuery no
> front, Intervention/Image, Maatwebsite Excel, FPDI/TCPDF. A camada V2 é carregada por PSR-4
> `V2\` → `V2/` (`composer.json:87`).

---

## 1. Boot e auto-descoberta

O provider é registrado em `config/app.php:218`, **antes** do `RouteServiceProvider` (`:223`), e faz,
em `V2/Core/Providers/V2ServiceProvider.php:26`:

| Ordem | O que faz | Evidência |
|---|---|---|
| 1 | Diretivas Blade `@v2can` / `@endv2can` | `:29-34` |
| 2 | Namespaces de view `v2-cms` e `v2-portal` para `V2/layouts/*` | `:37-42` |
| 3 | ViewComposer da sidebar (`SidebarComposer`) | `:47` |
| 4 | ViewComposer global do portal — consulta `PortalConfigsService` em **toda** render `v2-portal::*`, com `try/catch` silencioso | `:54-71` |
| 5 | Migrações do Core (`V2/Core/Migrations`) | `:78` |
| 6 | Rotas do Core (login/logout/dashboard/fallback) | `:81` |
| 7 | Varredura `V2/Modules/<Grupo>/<Modulo>` | `:85-127` |
| 8 | Para cada módulo: instancia `ModuleProvider`, injeta `group` = nome do diretório, carrega `Migrations/`, `Views/` (`v2-<modulo>::`) e `Routes/{web,cms,api}.php` | `:101-124` |
| 9 | Publica `v2.modules.configs` no container | `:130` |
| 10 | Fallback `GET /{slug}` → `BasicPagePortalController@runtime` | `:134`; `V2/Core/Routes/fallback.php:20` |
| 11 | `refreshNameLookups()`/`refreshActionLookups()` após o boot | `:138-139` |
| 12 | Varredura `V2/Services/<Servico>` (migrations, views `v2-service-<slug>::`, rotas, comandos, providers opcionais) | `:174-201` |
| 13 | Registro de comandos por convenção (`Console/Commands`) | `:211-247` |
| 14 | Canal Monolog/Graylog e retenção | `:601-640` |

**Diagrama do ciclo de uma requisição CMS:**

```mermaid
flowchart LR
    R[Request /cms/...] --> M1[web middleware<br/>CSRF + sessão]
    M1 --> M2[v2.authenticate<br/>CmsAuthenticate]
    M2 --> M3[v2.permission<br/>V2PermissionMiddleware]
    M3 --> M4[module.active:Modulo<br/>ModuleAccessMiddleware]
    M4 --> C[Controller do módulo]
    C --> S[Services do módulo/Core]
    S --> DB[(PostgreSQL)]
    C --> V[v2-&lt;modulo&gt;::cms.*]
    V --> L[v2-cms::layouts.app]
    C --> A[V2AuditLogger / CmsActionLogger]
```

**Consequência de desenho:** não há manifesto compilado nem cache de configuração. Cada request
executa `File::directories`/`File::exists`, `class_exists` e **instancia todos os `ModuleProvider`**
(os construtores registram singletons — ex.: `V2/Modules/Support/SupportConfigs/ModuleProvider.php:14-71`).
A ordem vem de `File::directories`, sem `sort`; a chave do array é apenas o nome do módulo
(`:107`), então dois grupos com o mesmo nome de módulo colidem silenciosamente. O `README` do Core
admite dependência de ordem alfabética (`V2/Core/README.md:20`). `[INFERENCE]` Route cache é
inviável: há `require` de rotas e fallback no boot.

## 2. Contrato de módulo (de facto)

| Item | Regra | Evidência |
|---|---|---|
| Namespace | `V2\Modules\{Grupo}\{Modulo}` | `V2/Modules/README.md:28` |
| Pastas | `Controllers/`, `Models/`, `Routes/`, `Views/`, `Migrations/` (+ `Services/`, `Seeds/`, `Requests/`, `assets/`) | `V2/Modules/README.md:13-25` |
| Provider | `ModuleProvider implements ModuleProviderInterface` com **um** método `getConfig(): array` | `V2/Core/Contracts/ModuleProviderInterface.php:7-11` |
| Manifesto | `title`, `description`, `icon`, `position`, `active_pattern[]`, `route`, `restricted?`, `subModulesLevel1[]`, `subModulesLevel2[]`; `group` é injetado do filesystem | `V2/Modules/Publicacoes/Noticias/ModuleProvider.php:46-57` |
| Views | namespace automático `v2-<modulo-minusculo>::` | `V2ServiceProvider.php:120` |
| Rotas | `web.php` (portal), `cms.php` (admin), `api.php` (AJAX/API) | `:508-536` |
| Auth do CMS | **não** é injetada pelo Core: cada `cms.php` declara `v2.authenticate` + `v2.permission:<chave>` | `V2/Modules/Publicacoes/Noticias/Routes/cms.php:11-13` |
| Ativação | `module.active:<Modulo>` é aplicado pelo Core a **todo** `cms.php` | `V2ServiceProvider.php:522` |
| Assets | fora do módulo, em `public/V2/assets/**` (Nginx `immutable`, cache-buster por `filemtime`) | `layouts/cms/layouts/app.blade.php:36`; `Seeds/SyncPublicAssetsSeeder.php` |

**Duas armadilhas do contrato:**

1. `active_pattern` é um regex de URL que mapeia rota→módulo→permissão. O middleware **ignora** o
   parâmetro declarado na rota (`v2.permission:noticias`) e resolve o módulo pelo primeiro
   `active_pattern` que casar (`V2/Core/Middleware/V2PermissionMiddleware.php:40-48`). Rota fora de
   qualquer padrão é **fail-open** (`:75-76`).
2. A permissão é binária (`none|read|write`): `GET/HEAD` passam no nível `read` (`:104`) e qualquer
   `write` autoriza *todas* as ações de escrita — não existe “aprovar” destacado de “editar”
   (contraste com ADR-033 do V7).

## 3. Isolamento de domínio e grupos

- Grupos são pastas: `DadosAbertos`, `Infraestrutura`, `Institucional`, `Publicacoes`, `Relacionamentos`
  (vazio), `Servicos`, `Support`, `Utilitarios`, `API` (vazio) e `_Exemplos` (andaime, ignorado).
- Há suporte a “isolamento por host”: com `v2.is_default=false` as rotas são montadas sob
  `v2.<host>` (`V2ServiceProvider.php:436-443`, `:468-474`). É escopo por **host**, não multi-tenant
  por linha.
- O menu é derivado do manifesto: `SidebarComposer` filtra por feature flag, remove `API`, esconde
  `Support` de quem não é o e-mail de suporte, aplica RBAC e agrupa/ordena
  (`V2/Core/ViewComposers/SidebarComposer.php:20-135`). Filho único é “flatten” (`:112-135`).

## 4. Configuração e dados de referência

- Configs de portal/CMS/tema/e-mail vivem em tabelas (`v2_portal_configs`, `v2_cms_theme_configs`,
  `v2_system_mail_configs` com senha `Crypt`, `v2_system_controls`) — o padrão “shell como dado”
  que o V7 formalizou na ADR-036.
- `Seeds/V2EnvironmentSeeder.php:30-50` é o único entrypoint: assets → estrutura → dicionários;
  demo é opt-in (`V2_DEMO_DATA`). Seeds institucionais (`BasicPages/*`) carregam conteúdo real do
  portal; `Sle*Seeder` embute CSS/JS/HTML de um tenant específico (SUCESU) em heredoc — **HTML em
  dado**, incompatível com ADR-017.
- Migrações do Core usam `DB::beginTransaction()` + `SET lock_timeout='3s'` e `down()` que **renomeia**
  para `backup_<tabela>_<ts>` em vez de dropar (`V2/Core/Migrations/2026_06_01_000005...:63-101`) —
  política de preservação de dados de governo.

## 5. Qualidade embutida no desenho

| Mecanismo | Onde | Observação |
|---|---|---|
| `declare(strict_types=1)` em 567/567 arquivos | `V2/LINT_BASELINE.md:14` | exigido como primeira instrução |
| PHPStan nível 9 | `phpstan.neon` | “0 erros” sustentado por ~60 `ignoreErrors` amplos (`phpstan.neon:18-260`) |
| php-cs-fixer | `composer v2:lint` | Blade excluído por desenho (`LINT_BASELINE.md:70-74`) |
| PHPUnit | 650 testes / 2107 asserções / 9 skipped | `V2/TEST_BASELINE.md:11-22` |
| Playwright | 89 e2e | usa `X-CMS-AI-Token` de avaliação (`TEST_BASELINE.md:59-72`) |
| sniff (a11y/visual) | advisory | não bloqueia (`TEST_BASELINE.md:80-88`) |

## 6. O que a arquitetura ensina (independente de stack)

1. **Módulo = pasta + manifesto + convenções** funciona: 31 módulos de negócio e 15 serviços sem um
   `ServiceProvider` por módulo.
2. **A descoberta precisa de cache e de contrato tipado.** O V7 já corrigiu o custo (manifesto
   cacheado, ADR-014) e o contrato (`config(): array` com permissões, ADR-033); o V2 mostra o custo
   de não tê-los.
3. **O manifesto é a fonte do menu, não o menu.** Sidebar, breadcrumb e busca do CMS saem do mesmo
   `v2.modules.configs` (`SidebarComposer`, `SearchService::cmsSearch`).
4. **Permissão por padrão de rota é frágil.** O V7 deve amarrar a permissão ao *route name*/grupo
   declarado, não a um regex de URL com fallback aberto.
5. **Feature flag de módulo com efeito em menu + rota + “não perde dados”** é exatamente a ADR-034;
   o V2 implementa a versão síncrona (tabela + cache estático), sem invalidação consistente.
