# PRD — Edicaorota: hardening da arquitetura V2

## Problema / dor

`componentes/Missoes/Logistica/sinais/Edicaorota/` é (junto com `Otp`) a **referência canônica V2** do projeto — é o que `genesis-arquitetura` aponta como modelo pra qualquer sinal novo. Uma auditoria arquitetural feita em 2026-05-27 mostrou que a estrutura está correta (Sondas V2, Validators dedicados, Backed enum, Registry+Interface, Adapters/<Dominio>/, Migrations versionadas, Testes espelhando layout), **mas o uso vazou**: o core do sinal acabou conhecendo os Adapters por nome, o bootstrap do callback está triplicado, há um segredo Scaledrone hardcoded em PHP versionado, há um UPDATE via raw SQL na Sonda e a "dispatcher fino" cresceu pra 434 linhas com lógica de domínio dentro.

Sintomas concretos hoje:

1. **Segredo Scaledrone commitado** em `Eventos/EdicaoRotaSocketNotifier.php:25-26` (`SCALEDRONE_CHANNEL_SECRET = 'pIe8Xn…'`). Está em git history — qualquer um com acesso ao repo lê e pode publicar nos rooms do Central.
2. **Core importa direto de `Adapters/<Dominio>/`** em 6 arquivos: `Edicaorota.php`, `AprovarEdicaoRotaLaboratorio.php`, `RejeitarEdicaoRotaLaboratorio.php`, `CancelarEdicaoRotaLaboratorio.php`, `SubmeterRotaPropostaLaboratorio.php`, `ConsultarEdicaoRotaLaboratorio.php`. O skill `genesis-callbacks-adapters` diz explicitamente "**Core do sinal não importa classes do `Adapters/<Dominio>/`**" — quando uma equipe nova ler isso como referência, vai copiar o vazamento.
3. **Bootstrap do callback Romaneio em 3 lugares**: `Edicaorota::bootstrapCallbacks()`, `Plataforma/Api/Bootstrap.php:79-86`, `AprovarEdicaoRotaLaboratorio::garantirCallbackRegistrado()`. Defensive coding repetido — todo lugar que invoca o Lab "por garantia" re-registra.
4. **Controller faz negócio**: `Edicaorota::processarReordenarDireto()` (linha 373-402) escreve em `RomaneioPedido` direto do controller, duplicando a `RomaneioVeiculoEdicaoRotaCallback::aplicarSequencia()`; `telaMotorista()` (linha 189-310) tem lógica de status × sessão × prazo × hidratação que devia ser de Laboratório.
5. **`EdicaoRotaSonda::reconciliarEspelhosTerminais()` faz `UPDATE ... FROM` via `_rawSql`** — viola a regra do projeto "nunca raw SQL pra writes; use `->update()` da Sonda". Pior: o método é **chamado pelo `Central.php:215`**, ou seja, a Sonda de Edicaorota está sendo usada por outro sinal pra reconciliar dados que o próprio Edicaorota deveria manter consistentes.
6. **Acoplamento circular com `Solicitacoes`**: Edicaorota escreve em `solicitacoes_globais` via `EspelhoSolicitacaoGlobalAdapter`; Solicitacoes (`Strategies/EdicaoRotaAprovadaStrategy`, `EdicaoRotaRejeitadaStrategy`) chama labs do Edicaorota de volta. O laço é quebrado por um guard `if (status !== 'pendente') return;` no Adapter — frágil.
7. **i18n ausente**: nenhum `show()` ou referência a `linguagens/` nas views (`html/*.php`) nem nas mensagens dos Laboratórios. Strings em PT-BR hardcoded em ~3000 linhas de view.
8. **OTP bootstrap duplicado**: `Edicaorota::bootstrapOtp()` registra `LogCanal` + `EdicaoRotaOtpEvento` + `OtpConfig` — `LogCanal` é território do sinal `Otp` (já registrado em `Plataforma/Api/Bootstrap.php`).

A solução proposta é dividir esse hardening em frentes independentes que possam virar PRs sequenciais, mantendo o sinal funcionando em produção durante a refatoração.

## Personas (alto nível)

- **Dev Genesis** lendo `Edicaorota` como referência V2 — hoje copia padrões corretos misturados com vazamentos. Precisa de uma referência limpa.
- **Operador logístico** usando o Central pra criar/aprovar/rejeitar edições — não pode ter regressão de comportamento durante a refatoração.
- **Motorista** validando OTP e submetendo proposta de rota pelo link público — fluxo crítico, zero tolerância a downtime.
- **Security / dono do repo** — quer o segredo Scaledrone fora do código fonte e a chave rotacionada.
- **Tech Lead** que vai aprovar o plano técnico — precisa decidir até onde refatorar agora vs. deixar pra próximo ciclo (alguns achados são dívida arquitetural sem urgência operacional).

## O que precisa fazer (alto nível)

Organizado em 3 frentes. As 5 frentes do desenho original ficaram assim:

- **Frente 1 (Scaledrone secret) — FORA do escopo deste PRD.** Decisão 2026-05-27 do dono: vira PRD separado quando o dono da conta Scaledrone estiver disponível pra rotacionar a chave. Sem rotação, "tirar da constante e botar em env" é meio-fix (a chave antiga continua válida em git history). Tracked como dívida.
- **Frente 5 (i18n) — FORA do escopo deste PRD.** Decisão 2026-05-27: vira PRD separado, paralelizável. Mantém esse PRD focado em arquitetura.

As três frentes ativas (2, 3, 4 do desenho original — renumeradas aqui pra evitar gap):

### Frente A — Reverter vazamento Core → Adapter (callback registry sem nome de Adapter)

**Princípio**: o core do Sinal só pode importar de `Callbacks/` (Interface + Registry). Quem registra implementação é o **consumidor** (Plataforma/Api ou outro sinal).

- **Remover imports de `Adapters/Romaneio/`** em:
  - `Edicaorota.php:27,87-91` (bootstrap do controller web).
  - `Laboratorios/AprovarEdicaoRotaLaboratorio.php:6,89,138-150` (incluindo o método `garantirCallbackRegistrado` inteiro).
- **Mover responsabilidade de registrar callbacks core** para um único bootstrap simétrico:
  - `Plataforma/Api/Bootstrap.php` já registra para o caminho API REST → manter.
  - Criar um equivalente para o caminho web Galaxia (ou centralizar num só bootstrap chamado de ambos os entrypoints) — TL decide o mecanismo (autoload, classmap, hook do GalaxiaRoute).
- **Trocar chamadas diretas a `EspelhoSolicitacaoGlobalAdapter::criar/sincronizar`** por **chamadas via Registry**:
  - Estender `EdicaoRotaCallbackInterface` com `aoSubmeter(object $edicao)` (motorista submeteu proposta), `aoCancelar(object $edicao)` (cancelamento) — paralelos a `aoAprovar` / `aoRejeitar` que já existem.
  - `EspelhoSolicitacaoGlobalAdapter` passa a ser um **callback** que se registra no Registry sob nome `'solicitacao_global'` (não mais singleton estático).
  - Cada Lab itera **todos** os callbacks ativos do registry e invoca o hook correspondente, em vez de chamar adapters nominalmente.
- **Remover `Edicaorota::bootstrapOtp()` parcialmente**: deixar só o `OtpConfig::registrar` específico do contexto `edicao_rota` (validade, max_tentativas, etc.); tirar registro de `LogCanal` e `EdicaoRotaOtpEvento` (já feito em `Api/Bootstrap.php`).

### Frente B — Mover negócio do controller pra Laboratórios

- **`processarReordenarDireto()`** (`Edicaorota.php:373-402`) vira `ReordenarRotaDiretoLaboratorio`:
  - Recebe `token` + `nova_rota`.
  - Valida sessão motorista, monta payload, **delega ao callback Romaneio** via Registry — não escreve direto em `RomaneioPedido` (elimina duplicação com `RomaneioVeiculoEdicaoRotaCallback::aplicarSequencia`).
  - Retorna array padrão Galaxia; controller só serializa pra JSON.
- **`telaMotorista()`** (`Edicaorota.php:189-310`) vira `MontarTelaMotoristaLaboratorio`:
  - Recebe `token`.
  - Retorna o "estado da tela": `{view: 'status|editar_rota|sessao_expirada|validar_codigo', vars: [...], mensagem: ?}`.
  - Controller só decide qual `visual()` chamar com base no `view` retornado.
- **`expirar_sessao`** (`Edicaorota.php:147-159`) vira `ExpirarSessaoMotoristaLaboratorio` — invoca callback `aoCancelar` (não chama adapter nominalmente).
- **`__call` que faz `echo + exit`** (`Edicaorota.php:58-62`) — substituir por retorno via `addResponse` com mensagem de erro padrão Galaxia (segue padrão do framework, vira testável).

Critério: depois da Frente B, `Edicaorota.php` deve ficar abaixo de 200 linhas, sem `if/else` de domínio nem `update()` direto em entidade externa.

### Frente C — Persistência: zero raw SQL writes + fim do circular dep

- **`EdicaoRotaSonda::reconciliarEspelhosTerminais()`**:
  - Reescrever sem `_rawSql` — usar `find()` + iterar com `update()`. Operação roda em background; performance batch não é crítica.
  - Avaliar com TL se esse método **deveria mesmo morar em Edicaorota**: ele é chamado por `Central.php:215` antes de listar solicitações. Se a invariante é "o espelho de Solicitação reflete o estado da edição", então o Adapter de Solicitacoes é quem deve reconciliar — e seria chamado pelo lado Solicitacoes/Central, não pela Sonda de Edicaorota.
- **Quebrar o laço Edicaorota ↔ Solicitacoes**:
  - Após Frente A, `EspelhoSolicitacaoGlobalAdapter` é callback registrado; Edicaorota não escreve mais em `solicitacoes_globais` por nome.
  - Definir invariante única: **quem manda no status é Edicaorota; Solicitacoes consome, nunca escreve de volta**. As Strategies `EdicaoRotaAprovadaStrategy` / `EdicaoRotaRejeitadaStrategy` em Solicitacoes precisam ser revistas — ou desligadas se o callback inverso já cobre, ou explicitamente documentadas como "fluxo de entrada do operador no painel Solicitações" (e o callback de status volta).
  - Resultado esperado: o guard `if ($espelho->status !== 'pendente') return;` em `EspelhoSolicitacaoGlobalAdapter::sincronizar:81` deixa de ser necessário.

## Fora do escopo

- **Frente 1 — Scaledrone secret hardening**: decisão 2026-05-27 do dono — vira PRD separado quando o owner da conta Scaledrone (channel `paKUSNCLRO6yHHZ8`) estiver disponível pra rotacionar a chave. Sem rotação, extrair pra env é meio-fix (a chave continua válida em git history). O segredo segue hardcoded em `EdicaoRotaSocketNotifier.php:25-26` até esse PRD subsequente.
- **Frente 5 — i18n nas 3 línguas**: decisão 2026-05-27 do dono — vira PRD separado. Strings PT-BR hardcoded nas views (`html/*.php`) e mensagens dos Laboratórios permanecem como estão.
- **Migrar Romaneio (V1 Inteligencias) para Sondas V2**: o Adapter de Edicaorota consome `Romaneio`, `RomaneioVeiculo`, `RomaneioPedido` que ainda são V1. É dívida do sinal Romaneio, não desse PRD. Aceitável como temporário.
- **Refatorar o `Otp` para deixar de ter LogCanal como "stub padrão"**: o LogCanal é registrado em 2-3 lugares como fallback de canal não configurado. Não mexer nessa camada; só remover o registro duplicado de dentro do Edicaorota.
- **Rever todas as Strategies de Solicitacoes**: Frente 4 toca só nas duas estritamente acopladas a Edicaorota (`EdicaoRotaAprovadaStrategy`, `EdicaoRotaRejeitadaStrategy`). Strategies de outros tipos (Designação, Falta, Recusa) continuam como estão.
- **Mover `EdicaoRotaSocketNotifier` para um sinal genérico de notificação**: hoje é específico de Edicaorota; quando outros sinais quiserem broadcast Scaledrone via Lab, viramos sinal de capability (estilo `Otp`). Não nesse PRD.
- **Reescrever as views grandonas** (`editar_rota.php` 2094 linhas, `_layout.php` 1742 linhas) **em JS pareado modular**: dívida real, mas é refactor de frontend independente.
- **Backfill / migração de dados**: nenhum schema novo introduzido; só rearranjo de código. Migrations existentes não mudam.

## Restrições / dependências conhecidas

- **Edicaorota é referência V2 ativa**: o skill `genesis-arquitetura` aponta pra cá. Qualquer mudança aqui que viole padrão V2 piora o problema. As mudanças desse PRD devem **reforçar** o padrão, não criar exceções.
- **Memory rule `feedback_no_raw_sql`**: "Never use raw SQL via Connect::getInstance; use model's `update()` method for batch updates". A Frente C implementa essa rule no Edicaorota.
- **Memory rule `feedback_referencias_v2`**: "OTP e Edicaorota são as referências canônicas V2". Esse PRD existe pra deixar essa afirmação verdadeira de novo.
- **Memory rule `feedback_sondas_v2`**: Adapter Romaneio mantém V1 (Inteligencias) — é exceção registrada e fora-do-escopo.
- **Fluxo em produção**: o sinal está em uso ativo no Central — Frente A e B mexem em hot path (aprovação, submissão, tela do motorista). Plano técnico deve prever:
  - Cada Frente (A, B, C) vira PR independente, testável em isolamento.
  - Testes TDD atuais (`Testes/Laboratorios/*TddTest.php`, `Testes/Adapters/Solicitacoes/EspelhoSolicitacaoGlobalAdapterTddTest.php`) precisam continuar passando — provavelmente alguns asserts mudam quando o Adapter vira callback.
  - Smoke test manual em dev: criar edição → motorista valida OTP → submete → gestor aprova → confirmar `sequencia_roteirizacao` atualizada no romaneios_pedidos + espelho em solicitacoes_globais com status=aprovada.
- **`Plataforma/Api/Bootstrap.php` já registra Romaneio callback**: o fluxo API REST já funciona sem o bootstrap do controller web. Validar antes de remover que o autoload do controller web também passa por algum bootstrap simétrico, ou criar um.
- **`reordenar_direto` no controller é fluxo do motorista no editor**: chamado pelo JS quando o motorista arrasta-e-solta paradas (sem passar pelo fluxo de aprovação). Frente B transforma isso em Laboratório — verificar que o JS continua falando o mesmo protocolo (POST + JSON de resposta) ou se vira endpoint REST formal.

## Métricas de sucesso

- **Frente A**: `grep -rn "Adapters\\\\\\(Romaneio\\|Solicitacoes\\)\\\\" componentes/Missoes/Logistica/sinais/Edicaorota` retorna **só dentro de `Adapters/` e `Testes/Adapters/`** (não no core nem em Labs core nem no controller).
- **Frente B**: `Edicaorota.php` ≤ 200 linhas; 0 chamadas a `->update(` ou Sonda de outro domínio fora de Laboratórios; nenhum `echo/exit` no controller; `__call` retorna via `addResponse`.
- **Frente C**: `grep -n "_rawSql" Sondas/EdicaoRotaSonda.php` mostra apenas SELECTs (joins complexos aceitáveis); nenhum UPDATE/INSERT via `_rawSql`. Guard `if ($espelho->status !== 'pendente') return;` em `EspelhoSolicitacaoGlobalAdapter` pode ser removido sem regressão.
- **Zero regressão funcional**: fluxo motorista (pedir código → validar → editar → submeter) + fluxo gestor (listar pendentes → aprovar/rejeitar) + integração Central (criar edição → broadcast → aprovar via mapa) operacionais em dev e prod após cada PR.
- **Testes TDD continuam passando**: `Testes/Laboratorios/*TddTest.php`, `Testes/Eventos/EdicaoRotaOtpEventoTddTest.php`, `Testes/Callbacks/EdicaoRotaDiaEfetivoResolverTddTest.php`, `Testes/Adapters/**/*TddTest.php`.

## Inspirações / referências

- **Sinal `Mensageria/Otp`** — é a outra referência V2 canônica. O padrão de callback registry dele (`OtpCanalRegistry`, `OtpEventoRegistry`) é exatamente o que a Frente A quer trazer pro Edicaorota: o core não conhece nenhum canal/evento específico, todos se auto-registram.
- **Skill `genesis-callbacks-adapters`** — checklist explícito que esse PRD remedia, especialmente "Core do sinal não importa classes do `Adapters/<Dominio>/`" e "Implementações de outros módulos registradas pelo lado consumidor com guard `temRegistro`".
- **`EdicaoRotaCallbackInterface` + `EdicaoRotaDiaEfetivoResolverInterface`** — já implementam ISP (capability opcional segregada). Frente A estende esse padrão pra cobrir `aoSubmeter` / `aoCancelar`.
- **`Plataforma/Api/Bootstrap.php`** — modelo de bootstrap idempotente único. Frente A quer reduzir o resto pra usar esse padrão.
- **Auditoria arquitetural de 2026-05-27** — documento original que gerou esse PRD (conversa do chat); pontos numerados aqui derivam dos achados de lá.

## Dúvidas em aberto pro PO

(Decisões de escopo já travadas pelo dono em 2026-05-27: Frente 1 e Frente 5 ficam fora; pipeline roda nas Frentes A, B, C.)

1. **Como o controller web Galaxia roda bootstrap idempotente hoje?** Memory não tem isso explícito. Se a única forma é via `Edicaorota::__construct`, Frente A precisa achar/criar um hook simétrico ao `Api/Bootstrap.php` ou aceitar que o controller registra **callbacks neutros** (sem importar do core).
2. **`EdicaoRotaSocketNotifier` é responsabilidade do Edicaorota mesmo?** Hoje vive em `Eventos/`. Quando criarmos outro sinal que quiser broadcast pro Central, vamos copiar? Vale virar `sinais/Notificacao` ou ficar com Edicaorota até o segundo cliente aparecer? Default: ficar; YAGNI.
3. **Frente C — Solicitacoes Strategies revertem ou só mudam comportamento?** Se a inversão de fluxo é radical (Solicitacoes só lê, nunca escreve), as Strategies `EdicaoRotaAprovadaStrategy` / `EdicaoRotaRejeitadaStrategy` ficam órfãs. Removê-las inteiramente OU mantê-las como caminho legítimo de "operador decidiu no painel de Solicitações" (chama de volta Edicaorota.AprovarLab)? Hoje fazem isso — mas o que invalida a inversão é se Edicaorota também escreve no espelho. Definir.
4. **`processarReordenarDireto` é fluxo público OU privado**? Hoje vive no dispatcher web, validando sessão de motorista (`sessaoMotoristaValida($token)`). Se virar Laboratório com Validator próprio, ainda chamado pela mesma URL `/missao/Logistica/Edicaorota/ver_status?acao=reordenar_direto`, OK. Mas se a ideia é tornar um endpoint REST `/api/v1/edicao-rota/{token}/reordenar`, vira API change.
5. **Ordem de execução das Frentes**: recomendação é A → B → C. Frente A desbloqueia B e C (Lab passa a chamar callback genérico em vez de adapter nominal; controller pode então delegar pro Lab sem ainda carregar import de Adapter). Faz sentido?
