# TECNICO — contrato de implementação das fases F7+

> **O que é:** o detalhamento técnico que toda fatia de `wiki/implementation-phases/` deve seguir.
> Não é uma decisão nova: `AGENTS.md`, os ADRs e as specs vencem. Se este guia conflitar com eles,
> **pare**, registre no `kd` e corrija o guia — nunca “resolva” o conflito no código.
>
> **Quando ler:** antes de abrir qualquer `P*.md`; junto com o arquivo da fase, o ADR citado e o
> código real da área. É leitura obrigatória das fatias P0–P29.

## 1. Ordem de leitura e fontes

1. `AGENTS.md` — constituição, stack, boundaries, gates.
2. ADR da fase (e as ADRs transversais: 001, 007, 012, 013, 015, 016, 017, 024, 033, 034, 035,
   038, 040, 041, 042).
3. Este guia — como transformar a decisão em código sem furar o padrão.
4. O `P*.md` da fase — escopo e fatias.
5. Código real da área (o que já existe vence suposição).
6. Skills: `.agents/skills/php-84-correct`, `laravel-12`, `html-moderno`, `css-moderno`,
   `javascript-moderno`, `web-accessibility`, `postgresql`, `vite`, `playwright-ts-admin`,
   `sniff-css` — consultar o recorte necessário, não copiar receita de outro projeto.

**Regra de ouro:** uma fatia só começa quando o contrato está escrito (rota, tabela, porta, DTO,
permissão, evento, i18n, teste) e o caminho de rollback é conhecido. “Estrutura para depois” não é
entrega.

## 2. PHP 8.4 — o que usar e o que o projeto proíbe

O projeto é **mais restritivo** que a linguagem e que a skill `php-84-correct`:

| Regra do projeto | Consequência prática |
|---|---|
| `declare(strict_types=1)` em todo arquivo PHP | sem exceção; `php -l` deve passar |
| **Sem traits, sem abstract, sem generics, sem `Result`** | composição por DI e funções concretas; duplicação explícita é aceitável |
| **Sem `new`/`resolve()`/`app()` em corpo de domínio** | dependências no construtor; `app()` só em composition root/provider/teste |
| `final` por padrão | classes de domínio, Actions, DTOs e controllers `final`; `readonly` quando o estado é imutável |
| Uma Action por arquivo | foco, nome de verbo, `execute(...)`, testável isoladamente |
| Sem HTML/CSS/Blade no banco | conteúdo é AST/JSON validado; renderer em código |

### 2.1 Tipos e construções

- **Enums** para conjuntos fechados (`Status`, `Area`, `Permission`, `Locale`); persistência com
  `string` + cast, nunca “enum mágico” sem validação.
- **DTOs** `final readonly` para entrada/saída; não passar `array` solto entre camadas quando o
  shape importa. `fromArray()` valida/normaliza na borda.
- **`match`** para branching fechado; se o `match` não for exaustivo, trate o default com erro de
  domínio — não deixe cair em `null` silencioso.
- **`never`** para funções que sempre lançam; **`#[\Override]`** em implementações de contrato.
- **Property hooks/asymmetric visibility** só quando o modelo ficar mais simples. Não usar em
  modelos Eloquent para “esconder” atributos: use `$hidden`, casts e DTOs. Não substituir um cast
  nativo por hook sem teste.
- **`json_decode(..., true, flags: JSON_THROW_ON_ERROR)`** e `json_encode(..., JSON_THROW_ON_ERROR
  | JSON_UNESCAPED_UNICODE)`. `json_validate()` quando só se quer checar o JSON.
- **`array_find`/`array_any`/`array_all`** quando expressarem melhor a intenção; não reescrever
  laços simples só para “usar 8.4”.
- **Nunca** `eval`, `unserialize()` de entrada não confiável, `create_function`, `each()`,
  `FILTER_SANITIZE_STRING`, `rand()`/`mt_rand()` para token/segredo.
- Aleatoriedade/relógio de domínio: **injetar `Core\Contracts\Clock` e `Core\Contracts\IdGen`**.
  `random_int()`/`Randomizer` são para infraestrutura/segurança, não para IDs de domínio.
- Dinheiro: `numeric` no PostgreSQL + string/decimal no PHP; nunca `float` para valor fiscal.
- Datas: `timestamptz` + `CarbonImmutable`/`DateTimeImmutable`; exibição via `intl`
  (`Core\Localization\LocalizedDate`), nunca `date()` com timezone implícito.

### 2.2 Erros e exceções

- Exceção de domínio concreta (`InvalidTransition`, `ModuleEssential`, `SettingsValueInvalid`),
  não `RuntimeException` genérica para tudo.
- `catch` específico; se capturar `Throwable`, **rethrow** ou converta com contexto. Nunca
  “logar e seguir” em fluxo que precisa falhar.
- Não usar exceção para fluxo normal; validação de formulário é `FormRequest`/422.
- Não vazar mensagem técnica/stack trace para o usuário; RFC 7807 na API e mensagem i18n na UI.
- Não usar `@` para silenciar; corrigir a causa. `error_reporting(E_ALL)` em dev; produção sem
  `display_errors`.

### 2.3 Segurança de linguagem

- Saída HTML: `{{ }}`/`e()`; `{!! !!}` só para HTML produzido por renderer confiável
  (`ContentASTParser`), nunca para conteúdo bruto do CMS.
- SQL: Query Builder/Eloquent com bindings; `whereRaw('x = ?', [$v])`; nunca interpolar.
- Senhas: `Hash::make`/`Hash::check` (Argon2id configurado); nunca md5/sha1.
- Comparação de token: `hash_equals`.
- Upload: MIME pelo **conteúdo** (`finfo`), não pelo nome/extensão do cliente; limite de bytes;
  allowlist; armazenar no disco `s3`; nunca executar.
- Segredos: `encrypted` cast/`Crypt`; nunca em log, auditoria, URL ou mensagem de erro.

## 3. Laravel 12 — bordas de versão e uso correto

Laravel 12 é “manutenção” sobre 11; o que importa é **não escrever Laravel 10**:

| Não existe/está errado em 12 | Faça |
|---|---|
| `app/Http/Kernel.php`, `app/Console/Kernel.php`, `app/Exceptions/Handler.php` | `bootstrap/app.php` (`withMiddleware`, `withExceptions`, `withSchedule`) |
| Providers em `config/app.php` | `bootstrap/providers.php` |
| `$this->middleware()` no controller | `HasMiddleware::middleware()` ou middleware na rota |
| Base `Controller` com `AuthorizesRequests`/`ValidatesRequests` | `Gate`/`can()` explícito ou traits conscientes |
| `env()` fora de `config/` | `config('x.y')`; `env()` só no arquivo de config |
| Route closure | controller invokable; `route:cache` não serializa closure |
| `$request->all()` em create/update | `$request->validated()`/`safe()` |
| `Model::all()` e filtro em PHP | query com `where`, `paginate`, `cursor` |
| `latest()` sem tie-breaker | `orderByDesc('published_at')->orderByDesc('id')` ou keyset |

### 3.1 Rotas, controllers e requests

- Rotas em `Routes/{area}.php` do módulo, carregadas por convenção; **sem closure**; nomes
  `{module}.{area}.{action}`; métodos HTTP corretos; mutação só `POST/PUT/PATCH/DELETE`.
- Binding implícito com UUID: `whereUuid('user')` quando necessário; escopo aninhado
  (`scopeBindings`) para não aceitar id de outro pai.
- Controller: `final`, invokable quando possível; ordem: autorização → FormRequest → Action →
  redirect/view. Sem consulta Eloquent, sem regra de negócio.
- `FormRequest::authorize()` não é o lugar de recriar a matriz de permissão; a autorização do
  módulo é `Permission::authorize()`/`Gate` no controller/Action (padrão existente). O request
  valida **forma**; `prepareForValidation()` normaliza JSON/arrays.
- `validated()` devolve só o que tem regra. Cuidado com regras curinga em AST/JSON: elas podem
  descartar chaves aninhadas (o código de Notícias já documenta isso). Valide o array externo e
  valide o shape no DTO/parser.
- `Rule::unique('schema.table')` interpreta o ponto como conexão. Em tabela com schema, use
  `DB::table('schema.table')` + closure ou `Rule::unique` com conexão explícita; teste.
- Toda mutação: CSRF (`@csrf`), `@method`, confirmação nativa, mensagem de sucesso/erro i18n.
- Ações de linha: `POST` + CSRF; nunca `GET` que muta; nunca confiar em botão desabilitado.

### 3.2 Eloquent e banco

- `protected $fillable` explícito; `casts()` tipado; `$hidden` para segredo; sem `$guarded = []`.
- `Model::shouldBeStrict(! app()->isProduction())` e `preventLazyLoading` no provider de dev/teste.
- Eager loading: `with()`, `withCount()`, `withSum()`; `whereIn` para mídia; uma query por widget;
  `cursor()`/`lazyById()` para volume; nunca `count()` dentro de loop.
- `DB::transaction(fn () => ..., attempts: 3)`; efeitos externos (e-mail, fila, S3) **após commit**
  (`afterCommit`/`DB::afterCommit`); não chamar HTTP dentro da transação.
- Concorrência: `lockForUpdate()` **dentro** da transação; unique constraint como verdade final;
  capturar `QueryException` de violação e devolver 422/409; `lock_version` para edição otimista.
- `firstOrCreate`/`updateOrCreate` podem correr; garanta unique e trate a violação. `upsert` para
  lote idempotente.
- Não usar `Model::find()` para autorização; use policy/permissão e escopo.
- `down()` nunca dropa dado: renomeie/mova para `backup_<tabela>_<ts>` (ADR-017); migração
  idempotente (`Schema::hasTable`, `CREATE INDEX IF NOT EXISTS`, `ON CONFLICT`).

### 3.3 Cache, filas e storage

- Cache de configuração/consulta: chave versionada (`settings.{section}`, `navigation.{menu}.{locale}`),
  invalidação **síncrona** na escrita; teste de dois requests. Não cachear dado privado sem
  escopo de usuário/permissão.
- `Cache::rememberForever` só para dado imutável/explícito; para conteúdo, TTL + invalidação.
- Jobs: `ShouldQueue`, `$tries`, `backoff()`, `WithoutOverlapping`/`ShouldBeUnique` quando o efeito
  não pode duplicar; `afterCommit()`; payload com IDs, não models serializados; handler idempotente.
- `Storage` de domínio é o disco `s3` (RustFS/AWS); URL pública via rota/controller ou
  `temporaryUrl`; não expor path local.
- `Http` client: timeout, retry com backoff, `throw()`, allowlist de host, sem seguir redirect para
  host arbitrário; nunca segredo em query string.

## 4. Arquitetura e injeção

### 4.1 Boundaries (violação = build vermelho)

- `core` **nunca** importa `modules`/`services`; `services` **nunca** importa `modules`;
  `layout` consome `core`/`services`; `modules/*` **nunca** importa `modules/*`.
- Comunicação cross-module: **eventos/listeners nativos** + **portas do Core** (interfaces).
  Widget/banner: registry no Core, referência por `id`, embed permitido, import proibido.
- Serviço: contrato em `src/services/{Name}/Contracts`; módulo depende da interface, nunca da
  implementação; service headless; UI só superfície admin (`/cms`).
- Ordem de providers importa: `ModuleDiscoveryServiceProvider` antes de
  `ServiceDiscoveryServiceProvider`; registry que módulos usam no `register()` deve viver no
  **Core** (lição do P5.5), não dentro de um service descoberto depois.

### 4.2 Anatomia de uma fatia

```
src/modules/{Grupo}/{Modulo}/
  ModuleProvider.php              # register() + config(); permissões, rota, ícone, posição
  Routes/{portal,cms,api}.php     # sem closure; nomes canônicos
  Logic/Actions/                  # uma Action por arquivo; execute(); DI; span/log
  Logic/Controllers/              # final, fino, autorização + request + action
  Logic/Requests/                 # forma + normalização
  Logic/DTOs/                     # readonly, fromArray()
  Database/Models/                # fillable, casts, relationships, scopes
  Database/Migrations/            # idempotente, down() seguro
  Layout/Views/                   # um shell; só <main>
  Lang/{locale}/                  # 4 locales, namespace do token
  Test/                           # contrato, feature, autorização, i18n
```

- Action: `final readonly`, dependências no construtor, `execute()`; telemetria em volta da
  operação de domínio; `log_event('{module}.{action}', [...])` com escalares, sem PII.
- Controller: `Permission::authorize('{Module}', Permission::EDIT)` **antes** de mutar; use
  `Permission::allows` para render; 403 nunca 500.
- Não criar “service”/“repository” sem segundo consumidor real; não criar abstração para variar
  driver (PG é o único alvo).
- Não criar trait/abstract para “reuso”; duplicação explícita é preferível a hierarquia.

### 4.3 Shells e conteúdo

- Toda view de módulo usa **exatamente um** shell (`<x-layout::portal>` ou `<x-layout::cms>`),
  informa os campos padrão (`title`, `description`, `breadcrumbs`, `canonical`, `robots`, `image`,
  `schemas`) e preenche **somente** o `<main>`.
- O shell é dono de `<html>`, `<head>`, header/footer/sidebar, a11y bar e `<h1>` do CMS; a view não
  declara `<style>`, `style="..."`, `<script>` inline, `x-app-layout` nem outro shell.
- Conteúdo é dado: AST/JSON validado por schema; renderer Blade concreto por tipo; valor inválido é
  ignorado com log, nunca renderiza HTML arbitrário.

## 5. Dados (PostgreSQL 18)

- Tipos: `text` (não `varchar(n)`), `numeric`, `timestamptz`, `jsonb`, `boolean`, `uuid` com
  `uuidv7()`; `NOT NULL` por padrão; default explícito.
- PK UUID; FKs com comportamento explícito (`nullOnDelete`, `restrictOnDelete`, `cascade` só para
  dependente real); índice em toda FK consultada.
- `jsonb`: `CHECK (jsonb_typeof(col) = 'array'|'object')` quando o shape é crítico; validação de
  schema em código; não guardar HTML/CSS/classes.
- Unicidade: `UNIQUE`; case-insensitive com `lower(email)`/`lower(slug)` em índice parcial
  `WHERE deleted_at IS NULL`; slug/rota com colisão tratada.
- Índices: nome do módulo/schema; compostos na ordem de filtro + ordenação; parciais para status
  ativo; GIN para `jsonb`/FTS; `INCLUDE` só com `EXPLAIN` que justifique; não indexar por reflexo.
- Migração de coluna: nullable → backfill em lote/transação → `CHECK NOT VALID` → `VALIDATE` →
  `SET NOT NULL`; nunca `NOT NULL` direto em tabela com dado.
- Backfill/importação: idempotente, com contagem antes/depois e relatório; uma linha inválida não
  aborta o lote quando o contrato permite falha parcial.
- Dinheiro: `numeric(14,2)`/`numeric(18,2)`; somas no SQL; arredondamento explícito; nunca float.
- PII: classificação; `encrypted` cast para dado pessoal sensível; blind index BLAKE3 quando busca
  exata é necessária; retenção/expurgo; DSAR; sem IP/UA cru em métricas.
- Auditoria não é analytics: `core_audit_logs` é evidência imutável; ranking/contadores usam
  `core_metrics_daily`, nunca a trilha.

## 6. HTTP, cache e API

- Nomes de rota canônicos; `route()` para navegação interna (carrega locale); `localized_url()`
  para path sem rota nomeada; nunca `url('/path')`/`href="/path"` no portal.
- Status: 404 para inexistente/área restrita; 403 para autenticado sem permissão; 422 para
  validação; 409 para conflito/versão; 429 com `Retry-After`; 503 manutenção.
- API: JSON RFC 7807 (`type`, `title`, `status`, `detail`, `instance`), `X-Request-ID`,
  `Content-Type: application/problem+json`; sem stack trace.
- Conteúdo público: `Cache-Control` + `ETag`/304; CMS/private: `private, no-store` quando
  necessário; CSRF fica fora de HTML cacheável; sem nonce em HTML cacheado (CSP sem inline).
- Paginação: view do projeto; `<nav>` semântico, `aria-current`, `rel=prev/next`, alvos ≥2.75rem;
  ordenação sempre com tie-breaker único; keyset para trilha/volume.
- Preview/rascunho: `temporarySignedRoute` TTL curto + `X-Robots-Tag: noindex, nofollow`; nunca
  rota pública de draft.
- Upload: limite de bytes/linhas, MIME por conteúdo, allowlist, S3 privado, antivírus/scan quando
  aplicável, nome determinístico por hash; imagem via Media com `alt`, `width`/`height`.
- Rate limiting: login (e-mail+IP), busca, formulários públicos, API; cache store Valkey; não
  consultar DB no hot path.

## 7. HTML, Blade e acessibilidade

### 7.1 Estrutura

- `<!DOCTYPE html>`, `<html lang>` do locale, `<meta charset>`/viewport primeiro; um `<h1>` por
  página; headings sem pular nível; landmarks (`header/nav/main/footer`), skip link.
- Tabela: `<caption>`, `<th scope="col|row">`, `aria-sort` quando ordenável, empty state; não usar
  tabela para layout.
- Formulário: `<label for>`, `fieldset/legend` para grupo, `type`/`autocomplete`/`required`,
  `aria-invalid` + `aria-describedby` no erro, resumo de erros `role="alert"`, foco no primeiro
  erro; validação server-side é a verdade; JS só melhora.
- Botão vs link: ação = `<button>`; navegação = `<a href>`; nunca `<div onclick>`.
- `<dialog>` nativo para modal; **nunca** `<form>` aninhado; aplicar por botão
  `[data-dialog-apply]`; foco retorna ao gatilho; `Esc` fecha.
- Popover/details nativos quando possível; menu não pode depender de hover/JS; `aria-expanded`
  apenas quando o padrão nativo não cobrir.
- Imagens: `<x-service-media::picture>`/`<picture>` com AVIF/WebP, `width`/`height`, `alt`
  obrigatório (vazio só decorativo), `loading="lazy"`/`decoding="async"` abaixo da dobra; sem CLS.
- Links externos: `rel="noopener"` com `target="_blank"`; avisar mudança de contexto quando
  necessário; nunca `javascript:`/`data:` em href.
- Conteúdo rico: somente `ContentASTParser`; nenhum nó HTML livre; links/imagens do AST passam por
  validação.

### 7.2 CSS e JS

- Tailwind 4 + daisyUI 5 + tokens; cores em `oklch()`; `rem`/`clamp()`/`fr`/`dvh`; `px` só como
  última exceção justificada (print pode usar unidades físicas).
- Sem `!important`, sem style inline, sem CSS no banco; custom CSS em `@layer components` e
  `@layer a11y` por último; propriedades lógicas; container queries/subgrid com fallback.
- Foco visível, contraste, `prefers-reduced-motion`, `forced-colors`; alvos ≥2.75rem.
- JS ES2025, ESM, `const`/`let`, `===`, `??`/`?.`, `async/await`; sem framework; sem inline;
  progressive enhancement; conteúdo público funciona sem JS.
- TipTap com `injectCSS: false`; CSS do ProseMirror no bundle; diálogos nativos; sem HTML cru.
- Zero CDN: nenhum asset externo; allowlist apenas VLibras e GA opt-in (ADR-002).
- Build: Vite 7 com assets em `src/**/Assets/{js,css}` (não recriar `resources/js`); `@vite` só no
  shell; CSS no bundle/`@layer`; sem script inline; `.br`/`.gz` determinísticos no build
  (`concordia:precompress`) e servidos pelo Caddy.
- `sniff capture/check/diff` é a fonte para layout/estilo medido; não “chutar” CSS.

## 8. i18n

- 4 locales (`pt_BR`, `en`, `es`, `fr`) em `Lang/{locale}/{section}.php`; namespace = token do
  módulo; o nome do arquivo é a seção (`Lang/pt_BR/index.php` → `token::index.*`).
- Nenhuma string visível hardcoded; `__()`, `trans_choice`, `@lang`; labels/help/erros traduzidos.
- Conteúdo traduzível em `jsonb` com `LocaleCast` (título, summary, AST); CMS com tabs por locale.
- Locale vem da URL; portal registra o grupo duas vezes (sem prefixo + `{locale?}`); middleware
  remove `locale`; `URL::defaults` mantém `route()` no locale ativo.
- Navegação interna: `route()`; path sem nome: `localized_url()`; nunca `url()`/`href` cru.
- Datas via `intl`/`LocalizedDate`; `hreflang` + `x-default` do shell; manifest guarda chave de
  tradução, não texto.
- Gates: `ModuleI18nTest` (4 locales, paridade, namespace) e `LocaleUrlInvariantTest` (HTML não
  sai do locale).

## 9. Observabilidade e auditoria

- Injetar `Core\Contracts\Telemetry`; span envolve a **operação de domínio**, nome
  `{module}.{action}` / `{service}.{operation}` / core `{domain}.{op}`; atributos `scalar|null`,
  sem PII, sem objeto/array.
- `log_event('{module}.{action}', ['module'=>..., 'action'=>..., 'id'=>...])`: escalares, sem
  PII; é efêmero, não é auditoria.
- Toda Action instrumentada emite **span + log_event com o mesmo nome**; `Clock`/`IdGen` não são
  spans por chamada.
- Auditoria: `AuditWriter::record(AuditEntry)`; `GlobalAuditSubscriber` enriquece mutações; eventos
  explícitos (`AuditEntry::event/access`) são sempre gravados; ator é snapshot `{id,name,email}`;
  `before/after` sem segredo; `audit.access` só CMS; retenção/legal hold.
- Não colocar evento técnico de alta cardinalidade na trilha; não usar audit para métrica.
- Logs técnicos com `module`, `action`, `id`, `user`, `ts`, `duration_ms`; nunca corpo de e-mail,
  token, senha, IP cru, conteúdo de formulário.

## 10. Segurança e LGPD

- CSP estrita + Trusted Types; sem inline; `X-Content-Type-Options`, `Referrer-Policy`,
  `Permissions-Policy`; TLS 1.3/HSTS no edge; cookie de sessão `Secure`/`HttpOnly`/`SameSite`.
- Autenticação CMS: sessão nativa, Argon2id, `session()->regenerate()` no login, logout `POST` +
  CSRF, mensagem genérica, throttle e-mail+IP, auditoria de login/falha/logout.
- Autorização deny-by-default: token `{module}.{ver|editar|aprovar|arquivar}` declarado no
  manifesto; perfil `suporte` protegido; sem herança; sem bypass por e-mail.
- Mass assignment: `$fillable`/`validated()`; nunca `$request->all()`.
- XSS: escape na saída; AST renderer; sem `{!! !!}` de conteúdo bruto; sanitizar links.
- SSRF: allowlist de host, timeout, sem redirect aberto, sem IP privado; P13.
- Segredos: configuração de infraestrutura em `.env`; settings cifrados; nunca em audit/log/URL.
- LGPD: minimização, finalidade, base legal, retenção, DSAR, incidente; dados sensíveis com DPIA
  (P21); open data com `containsPersonalData` + base legal + allowlist; métricas sem PII.
- Áreas restritas invisíveis: `cms.`/`api.` fora de robots/sitemap/canonical/hreflang/JSON-LD/feed;
  `X-Robots-Tag: noindex`.

## 11. Testes, determinismo e gates

- **Red → green**: bug tem teste que falha antes; feature tem teste de contrato durante a fatia.
- Testar comportamento observável, não implementação; não testar framework, texto de código,
  wiring, mock eco, `assertTrue(true)`.
- Determinismo: `freezeTime`/`travel`; `Clock`/`IdGen` fixos; sem `now()`/`rand()` no domínio;
  factories com dados determinísticos; ordem explícita; sem `sleep`.
- Banco: `RefreshDatabase` (ou isolamento por worker), sem depender de estado entre testes;
  `assertDatabaseHas/Missing/Count`; unique/constraint testada.
- Fakes: `Http`, `Mail`, `Queue`, `Event`, `Notification`, `Storage`, `Bus`; nenhum e-mail/rede/
  S3 real.
- Autorização: matriz de perfis (sem permissão, com permissão, suporte, último admin); 403/404/422.
- Cache: teste de dois requests (escrita invalida); idempotência: repetir ação não duplica.
- E2E: cada spec cria os próprios dados; `fullyParallel: false`; ordem dentro do arquivo é
  contrato; axe + no-JS; perf/CLS; sem depender de dado de outro spec.
- Gates: `make check` 2× sem flaky; `E2E=1` quando UI/HTML/CSS/JS/rota; `SNIFF_A11Y_STRICT=1`;
  cobertura `src/` ≥90%; PHPStan max 0; arch/name/migration/vendor; `php artisan optimize`.
- Não “consertar” teste flaky com retry/sleep; corrigir a causa (tempo, ordem, shared state).

## 12. Armadilhas recorrentes (e correção)

| Sintoma | Causa provável | Correção |
|---|---|---|
| `env()` retorna null em produção | `config:cache` | mover para `config/`; `config()` no código |
| rota some após `optimize` | closure na rota | controller invokable |
| 500 em `Rule::unique('schema.table')` | ponto interpretado como conexão | `DB::table` + closure/`Rule` com conexão explícita |
| `validated()` perde AST aninhado | regra curinga no array | validar o array externo + shape no DTO/parser |
| link interno troca de idioma | `url()`/`href` cru | `route()`/`localized_url()` |
| N+1 no widget/listagem | relação lazy | `with()`/`withCount()`/`whereIn`; teste de query count |
| cache stale após salvar | chave não invalidada | invalidar na escrita; teste de 2 requests |
| duplicação em `firstOrCreate` | corrida | unique + tratar `QueryException`; `upsert` |
| edição sobrescreve outra | lost update | `lock_version`/`lockForUpdate` + 409 |
| e-mail enviado antes do commit | efeito na transação | `afterCommit` |
| métrica quebra request | ingestão síncrona | `terminate` + fail-open |
| módulo desativado ainda aparece | cache/manifest não invalidado | `ModuleActivation` + guard + teste |
| auto-lockout do painel | desativar módulo essencial | `essential_modules` + recusa no servidor |
| super-destaque duplicado | só validação de app | índice único parcial + 422 |
| página sombreia rota | fallback mal posicionado | fallback da área + checar router |
| PDF “válido” sem TSA | simulação | exigir TSA/OCSP; declarar limite |
| CSV injection | célula `=+-@` | prefixo/neutralização + teste |
| tabela sem caption/scope | HTML genérico | componentes `x-cms::table` + axe |
| modal com form aninhado | HTML inválido | `<dialog>` + botão `data-dialog-apply` |
| contraste quebra com tema | cor livre | `ThemeSchema` + axe + `@layer a11y` |
| `px` no CSS | hábito | `rem`/`clamp`/`dvh`; gate de tokens |
| CDN/iframe externo | conveniência | vendorizar/link externo; `vendor-check` |
| teste flaky por relógio | `now()` real | `freezeTime`/`Clock` fixo |
| e2e 99/100 | `fullyParallel: true` | `fullyParallel: false` + dados por spec |

## 13. Watchlist técnica por fase

> Não substitui o `P*.md`; aponta o ponto que mais costuma quebrar naquela fase.

| Fase | Foco técnico que não pode escapar |
|---|---|
| **P0** | settings como JSON por escopo (`theme`/`widgets`), schema em código, cache por seção + invalidação, segredo cifrado/mascarado, audit before/after sem valor, uma query por seção, valor inválido ignorado, sem endpoint de chave avulsa |
| **P1** | `ThemeSchema`/`CmsShellResolver` como função pura; `theme.css` ETag; DOM segue posição; menu 3 níveis/ciclo/rota viva; porta `Navigation` sem importar módulo; ação de layout da área com fallback global; `localized_url` |
| **P2** | `deactivated_at` + sessão encerrada; `lower(email)`; catálogo de tokens declarados; último admin/self/suporte; matriz desabilita ação não declarada; sem GET mutando; Argon2id/regenerate |
| **P3** | essenciais protegidos; grupo `Suporte` intocável (invisível/404 exceto founder); uma Action UI/CLI; invalidação imediata; auditoria imutável; manutenção com allowlist; diagnóstico com timeout e sem segredo |
| **P4** | sem PII (IP/UA/URL/query), `terminate` + fail-open, agregado idempotente, HLL só em Valkey, cards por permissão, sem audit como analytics, CSV injection |
| **P5** | schema fechado/`hidden` preserva posição; bloco inválido ignorado; página fallback não sombreia rota; banner com vigência/alt; widget sem N+1; preview autenticado; cache invalidado |
| **P5.5** | registry no Core (ordem de provider); id duplicado falha; pull sem cópia de linhas; PII/base legal/allowlist; bytes determinísticos; sem `services/Export`; módulo inativo 404 |
| **P6** | backfill de categoria sem dupla verdade; tags normalizadas; galeria com alt e sem apagar mídia; super-destaque único por índice; transições válidas; preview assinado; keyset; dataset sem PII |
| **P7** | FTS gerada + GIN; `websearch_to_tsquery`; `ts_headline` escapado; reindex idempotente; sem ILIKE como solução; sitemap/IndexNow só portal |
| **P8** | anexos privados com MIME/limite; `show_value`; valores `numeric`; status/transições; sem PII; links e prazos |
| **P9** | service só com 2º consumidor; objeto separado `portal`/`cms` e integração opcional; cutover de índice (uma verdade); CSV streaming/anti-injection/Poka-yoke; job idempotente; notificação sem corpo/PII; feed XML escapado |
| **P10** | versão/consolidação/revogação imutáveis; URL estável; PDF pesquisável; busca reindexada; sem sobrescrever histórico |
| **P11** | protocolo determinístico; prazos com Clock; dados pessoais cifrados; timeline pública sanitizada; rate limit; auditoria de acesso; e-SIC/LGPD |
| **P12** | `DadosAbertos` é o único owner público; páginas/módulos são views; valores fiscais `numeric`; filtros allowlist; 3 anos; sem PII; cache/ETag |
| **P13** | **adiada (pós-P29)**; OIDC state/nonce/PKCE; sem auto-provisionamento; token fora de log/URL; ICP com TSA/OCSP; SPF/DKIM/DMARC; CDN só por medição |
| **P14** | institutional como dado; relações e cache; sem duplicar fonte; sem iframe de mapa; 4 locales |
| **P15** | edição oficial/diário com versionamento; PDF pesquisável; URL estável; arquivo sem DELETE; sem ETL |
| **P16** | dado pessoal cifrado/classificado; base legal para publicar; retenção/DSAR; CSV mínimo; audit de acesso; sem PII em log |
| **P17** | obras/contratos/sanções com valores e status; diárias sem PII; vínculos por id; filtros e 3 anos |
| **P18** | **D11: implantação posterior**; sessões/pauta/votos/atas; link de transmissão (sem embed); dados públicos; sem PII de terceiros |
| **P19** | formulários com honeypot/time-trap/consentimento; cifra; retenção; spam/rate limit; a11y; sem PII em log |
| **P20** | **D14: implantação posterior**; cliente de associação; dado com base legal; sem grupo de auth paralelo; retenção |
| **P21** | **D15: adiada**; saúde é dado sensível: DPIA, cifra, agregação, sem paciente identificável, retenção; e-Vacina sem terceiro |
| **P22** | receita/despesa/dívida/transferências em `numeric`; exercício; OpenData; filtros; 3 anos |
| **P23** | contratos/aditivos/fiscais/ordens; valores e vigência; SRP/PAC; anexos; sem PII desnecessária |
| **P24** | balanço/RGF/RREO/PPA/LDO/LOA; exercício; PDF; OpenData; período correto; sem stale |
| **P25** | SIC/classificados; `Solicitacoes` módulo com protocolo/form schema, sem helpdesk; DPO/satisfação; prazos; LGPD; audit |
| **P26** | a11y real (contraste, fonte, breadcrumb, mapa, Radar); axe/sniff; sem selo falso; mobile 44px |
| **P27** | renúncias/emendas/educação; datasets; valores; período; sem PII; cross-module via porta |
| **P28** | proposições/comissões/votações/contas/cotas/atividades; P17 é dono de VIAP/cotas; 3 anos; sem PII |
| **P29** | “Atualizado em” do dado, não do deploy; CSV/filtro sem login; declaração estruturada; PDF gerado com texto, externo sem OCR; `pntp:audit` operacional |

## 14. Checklist de prontidão da fatia

Antes de escrever código:

- [ ] Li o `P*.md`, o ADR e este guia; sei qual camada é dona da mudança.
- [ ] Contrato definido: rota/nome, método, permissão, tabela/migração, DTO/porta, evento/span,
      chaves i18n, cache/invalidação.
- [ ] Não estou criando dependência cross-module, service sem 2º consumidor ou abstração sem dor.
- [ ] Sei como testar (feature/contrato/negativo/autorização/determinismo) e como reverter.
- [ ] Migração é aditiva/idempotente; `down()` não dropa dado.
- [ ] A tela usa um shell; o HTML é semântico; o fluxo funciona sem JS.
- [ ] Não há segredo/PII em log, audit, URL ou fixture.

Antes de fechar:

- [ ] Teste vermelho antes da correção; verde depois; sem `sleep`/retry.
- [ ] `make check` 2×; `E2E=1` se UI; `SNIFF_A11Y_STRICT=1`; axe zero A/AA.
- [ ] PHPStan max 0; cobertura ≥90%; arch/name/migration/vendor; `optimize`.
- [ ] `log_event` + span + auditoria quando aplicável; sem PII.
- [ ] 4 locales; `route()`/`localized_url`; nenhuma string visível hardcoded.
- [ ] `INVENTARIO.md`/fase atualizados; `kd task close --outcome`; commit é do humano.

## 15. Anti-padrões (recusa imediata)

- `env()` fora de `config/`; closure em rota; `$request->all()`; `Model::all()` + filtro em PHP.
- `new`/`resolve`/`app` em Action/Controller; service locator como arquitetura.
- `modules/A` importando `modules/B`; `core` importando módulo/service; `layout` importando módulo.
- HTML/Blade/CSS/`style=` no `jsonb`; `{!! !!}` de conteúdo bruto; `iframe`/CDN externo.
- Qualquer trait/abstract/generic/`Result`; herança para “reaproveitar” 3 linhas.
- `now()`/`rand()`/`time()` no domínio; teste dependente de relógio/ordem; `sleep` para sincronizar.
- `firstOrCreate` sem unique; `lockForUpdate` fora de transação; e-mail/S3 dentro da transação.
- `Cache::remember` sem invalidação; cache de dado privado sem escopo; `withoutMiddleware` para
  “resolver” permissão.
- `GET` que muta; botão desabilitado como autorização; 500 para regra de negócio.
- `<div onclick>`, modal com `<form>` aninhado, tabela sem `caption`/`scope`, erro só por cor.
- `px` como unidade padrão; `!important`; style inline; CSS fora de `@layer`.
- CSV sem neutralização; upload confiando no MIME do cliente; path local em vez de S3.
- Seed em produção, ETL de legado, `services/Export` duplicado, métrica em `core_audit_logs`.
