# Review — bipagem-cota-minima

**Base**: working tree (dev_team, sem commit) — `git diff HEAD` + `git status`
**HEAD**: 0fd221d2 (working tree não commitado)
**Arquivos tocados**: 14 modificados + 6 novos (Sonda, Laboratorio, 2 migrations, 3 testes) + artefatos PRD
**Linhas**: +977 / −18 (tracked) + arquivos novos

---

## Ciclo 3 — Veredito final ✅

**Data**: 2026-06-18 · **Escopo verificado**: o fix de 1 linha do B4 em `SeparacaoHandler.php::buildScanCotaDataForLuz` + regressão e suíte.

### Veredito: ✅ **0 blockers → MERGE LIBERADO**

**B4 resolvido.** `buildScanCotaDataForLuz` (`SeparacaoHandler.php:587`) agora executa o SELECT:
```php
$missao = $mModel->find("id = :id", "id={$missaoId}")->fetch();   // ✅ WHERE + fetch()
if (!$missao) return ['missao' => null, 'jsItensData' => []];
```
em vez do `find($int)` (query builder que devolvia `$this` com `configuracoes = NULL`). Cadeia de re-render verificada ponta a ponta:

| Caminho | Onde | Resultado |
|---------|------|-----------|
| Re-render pós-bipagem de **item** | `SeparacaoHandler.php:518-530` — `reencaminheLuz('SeparacaoItemCard', array_merge([...], buildScanCotaDataForLuz($missaoId)), ...)` | missao hidratada ✅ |
| Re-render pós-bipagem de **lote** | `SeparacaoHandler.php:562-575` — idem | missao hidratada ✅ |
| **Autoload** da Luz | `SeparacaoHandler.php:385-403` (`Luz_SeparacaoItemCard`) — mesmo `buildScanCotaDataForLuz` p/ `endereco_alocacao` + `missao`/`jsItensData` | missao hidratada ✅ |
| Consumo na view | `luz/separacao/item_card.php:141-154` — `empty($missao)` agora **false** → `montarScanCotaFront($missao)` → `disponivel` mission-wide → `'scan_cota' => $scanCotaFila` (linha 174) | `data-scan-cota-ativo="1"` ✅ |

**Por que funciona (prova de forma, não só empírica)**: em `ModelInteligencia::fetch()` (`controladores/_Controladores/ModelInteligencia.php:755`) o caminho single-row faz `fetchAll(FETCH_CLASS)[0]` e passa por `prepareReturnData()` (`:850-865`), que para entrada **não-array** retorna um único `stdClass` (`prepareObject`) — **sem** o embrulho `[obj]` da memória `modelinteligencia_save_gotchas` (esse wrap é de `findById`/`data()`, não de `find()->fetch()` single-row). Logo o unwrap sugerido no ciclo 2 é **dispensável** — a 1 linha do dev está correta. Em seguida `ScanCotaLaboratorio::lerSnapCota()` faz `json_decode($missao->configuracoes, true)` (`ScanCotaLaboratorio.php:209+`), tratando o gotcha de coluna JSON voltar como string crua (`sonda_configuracoes_json_string`). Resultado: o card re-renderizado **mantém** `data-scan-cota-ativo="1"` — a cota **não desliga** após a 1ª bipagem. B4 fechado.

### Nenhum blocker novo introduzido

A 1 linha não tocou em: raw SQL (grep `Connect::getInstance`/`->exec`/`->query` no handler e na Luz → **NONE**), herança V1, validação inline, i18n. O `endereco_alocacao` herdado — que no HEAD sofria do mesmo `find($int)` sem fetch — foi **corrigido de carona** (agora derivado do `$scanCotaData['missao']` hidratado em `Luz_SeparacaoItemCard:385-403`). `php -l` limpo nos 7 arquivos do caminho.

### Suíte da feature (rodada neste ciclo)

| Teste | Resultado |
|-------|-----------|
| `ScanCotaLaboratorioTddTest.php` (Laboratório puro) | **PASS=65 / FAIL=0** |
| `ScanCotaConfigSondaTddTest.php` (Sonda) | **PASS=25 / FAIL=0** |
| `ScanCotaIntegrationTest.php` (dev_gx_goldie, backstop AC4/AC6) | **PASS=36 / FAIL=0** |
| **Total** | **126 / 0** |

Warnings nos logs (`Undefined array key sectorControl/cacheControl`, `session_name … active`, `linguagens permission denied` no integration) são ruído de ambiente/framework — **pré-existentes** (já apareciam no ciclo 2), não falhas de teste. Órfãos `focustest.js` / `scroll3.js` ignorados (fora do escopo da feature).

### Débitos remanescentes (não-bloqueantes)

- **d1 (era o ponto cego do B4)** — `buildScanCotaDataForLuz` / wiring de `scan_cota` na Luz segue **sem teste automatizado**. O fix foi validado por inspeção da cadeia + prova de forma do `fetch()`, mas não há regressão guardando `missao->configuracoes !== null` no caminho de front. Recomenda-se (futuro) um teste de handler que asserte `buildScanCotaDataForLuz($id)['missao']->configuracoes !== null`. **Não bloqueia o merge.**
- **m1** — comentários stale do "Bug #2" em `ScanCotaConfigSondaTddTest.php:15-16,178-211` (insert já corrigido p/ `create()`; narrativa do teste ainda fala em falso positivo). TODO.
- **m2** — `MissoesStatus.php:296-316`: backstop faz leitura extra de itens no `finalizar` (funcional, só quando `scan_cota.ativo`). Otimização opcional.
- **n1** — migrations com nome de data (`migrations/2026_06_17_*.sql`) em vez de `NNN_*.sql`; consistente com a convenção local da constelação. N/A.

### Recomendação do ciclo 3

✅ **Merge.** Os 4 blockers do histórico (B1, B2, B3 + B4 do ciclo 2) estão fechados; majors zerados desde o ciclo 1. Carregamento, separação (fila única/Luz), Sonda, Laboratório e backstop server-side aderentes ao V2 (Otp/Edicaorota), sem raw SQL, i18n nos 3 idiomas, testes 126/0. Restam apenas 1 débito de teste (d1) + 2 minors + 1 nit, todos não-bloqueantes — podem entrar com TODO.

---

## Ciclo 2 — Verificação do último blocker (B1)

**Data**: 2026-06-18 · **Escopo verificado**: `separacao/item.php`, `luz/separacao/item_card.php`, `SeparacaoHandler.php`, `MissaoPresenter.php`.

### Veredito: 🔴 **1 blocker** (B1 estrutural OK na 1ª render + 1 regressão nova no re-render pós-bipagem)

O fix do B1 **migrou para o ramo correto** (Luz, não mais o `else`/modo-livre morto) e funciona na **primeira renderização** da fila única: sai `data-scan-cota-ativo="1"` com a regra ligada. Porém o caminho de **re-render pós-bipagem** (`reencaminheLuz`) usa `MissaoLogistica::find($id)` no formato errado (int, sem `->fetch()`) e devolve `configuracoes = NULL`, fazendo a cota **desligar silenciosamente a partir da 1ª bipagem**. É blocker porque a separação é bipa-bipa-bipa: o enforcement de tempo real precisa sobreviver às re-renderizações.

#### O que ficou correto (B1 estrutural)

| Camada | Evidência | Status |
|--------|-----------|--------|
| View wrapper | `separacao/item.php:415-427` — dentro do `if ($modoFilaUnica)` (linha 412, ramo ATIVO), passa `missao` + `jsItensData` à Luz | ✅ |
| Ramo morto limpo | O `else`/modo-livre (`separacao/item.php:430+`) **não carrega mais** `scan_cota` — o fix-fantasma do ciclo 1 foi removido | ✅ |
| Luz | `luz/separacao/item_card.php:139-154` chama `MissaoPresenter::montarScanCotaFront($missao)` e reconstrói `disponivel = manuais − manchados bipáveis` mission-wide; passa `'scan_cota' => $scanCotaFila` no `execucaoCard()` (linha 174) | ✅ |
| Contrato data-* | `execucao_card.php:378-382` emite `data-scan-cota-ativo` = `'1'` quando `scan_cota['ativo']`, e `data-scan-cota-disponivel` do valor pré-calculado | ✅ |
| Consumo no JS | `execucao_card.js:113` lê `ativo==='1'`; `:131-132` lê `data-scan-cota-disponivel` (não cai mais em `Infinity` quando ligado) | ✅ |
| Handler (re-render) | `SeparacaoHandler.php:518,564` — os dois `reencaminheLuz` agora dão `array_merge(..., buildScanCotaDataForLuz($missaoId))` para repopular `missao`+`jsItensData` | ✅ (estrutura) / ❌ (dados — ver B4) |

**Confirmação direta do pedido**: na **primeira render** com a regra ligada, sai `data-scan-cota-ativo="1"` (não `"0"`) — o `$missao` chega hidratado do controller via `separacao/item.php` e `montarScanCotaFront` lê o snapshot corretamente. Cadeia verificada ponta a ponta (PHP emite → JS consome `disponivel`).

#### Blocker novo introduzido no ciclo 2

#### B4 — `buildScanCotaDataForLuz` usa `find($id)` sem `->fetch()` → cota desliga após a 1ª bipagem
**Onde**: `componentes/Missoes/Logistica/sinais/App/Handlers/SeparacaoHandler.php:585` (dentro do novo `buildScanCotaDataForLuz`):
```php
$missao = $mModel->find($missaoId);   // ❌ int, sem ->fetch()
if (!$missao) return [...];            // sempre truthy: find() retorna $this
...
'missao' => $missao,                   // objeto com data NÃO populado
```
**Problema**: em `ModelInteligencia`, `find()` é um *query builder* que retorna `$this` e **não executa o SELECT** — só `->fetch()` popula `$this->data`. O `__get` faz `return $this->data->$name ?? null`, então `$missao->configuracoes` vem **`null`**. Confirmado empiricamente (bootstrap real): `find(2781)->configuracoes === NULL`. Consequência: `MissaoPresenter::montarScanCotaFront($missao)` → `ScanCotaLaboratorio::lerSnapCota($missao)` recebe `configuracoes=null` → retorna `null` → bloco volta `ativo=false`. Como `buildScanCotaDataForLuz` alimenta os **dois `reencaminheLuz`** (pós-bipagem item e pós-bipagem lote), todo card re-renderizado durante a separação sai com `data-scan-cota-ativo="0"`.
**Efeito prático**: 1ª render OK (cota ligada), mas **a partir da primeira bipagem** o card volta a renderizar sem cota → AC2.2/AC2.5/AC2.6 inertes no resto da separação. Mesma família de falha do B1, agora no sub-caminho de refresh.
**Por que é blocker**: o enforcement de tempo real precisa sobreviver às re-renderizações — é o fluxo dominante da separação. O `find(int)` sem fetch é justamente o anti-padrão; o próprio handler já usa a forma certa em `:189-192` (`->find("col = :p", "p=v")->fetch(...)`).
**Como corrigir** (1-2 linhas): trocar por WHERE explícito + fetch (espelhar o uso correto do mesmo arquivo). Atenção ao gotcha `modelinteligencia_save_gotchas`: `find()->fetch()` pode vir embrulhado em `[obj]` conforme o `tableConfig` — desembrulhar:
```php
$res = $mModel->find("id = :id", "id={$missaoId}")->fetch();
$missao = is_array($res) ? ($res[0] ?? null) : $res;
if (!$missao) return ['missao' => null, 'jsItensData' => []];
```
Bônus: o `endereco_alocacao` herdado (`SeparacaoHandler.php:386-394`, e o antigo `:388-389` do HEAD) sofre do **mesmo** `find(int)` sem fetch — já era um `null` silencioso antes; corrigir os dois de uma vez.

#### Por que os testes passam mesmo com B4

A suíte cobre o **Laboratório puro** e o **backstop server-side**, que leem a missão via PDO `fetch(FETCH_OBJ)` (`ScanCotaIntegrationTest.php:301`), não via `Model::find()`. O wiring de front da Luz (`buildScanCotaDataForLuz`) **não tem teste** — é o ponto cego que deixou B4 passar. Recomenda-se um teste de handler (ou ao menos um assert de que `buildScanCotaDataForLuz` devolve `missao->configuracoes !== null`).

### Suíte da feature (rodada neste ciclo)

| Teste | Resultado |
|-------|-----------|
| `ScanCotaLaboratorioTddTest.php` | **PASS=65 / FAIL=0** |
| `ScanCotaConfigSondaTddTest.php` (Sonda) | **PASS=25 / FAIL=0** |
| `ScanCotaIntegrationTest.php` (dev_gx_goldie) | **PASS=36 / FAIL=0** |

`php -l` limpo nos 7 arquivos do caminho B1. Nenhum raw SQL nos arquivos do ciclo 2 (`buildScanCotaDataForLuz` usa `Model::find` + Satélite `MissoesExecucao`, sem `Connect::getInstance`).

### Demais issues novas

Nenhuma além de B4. As mudanças de view + handler do ciclo 2 não introduziram: raw SQL, herança de `Inteligencia` em código novo, validação inline, string sem `show()`, nem regressão de i18n. `montarScanCotaFront` está bem documentado e desacoplado; a reconstrução de `disponivel` na Luz espelha a do carregamento.

### Recomendação do ciclo 2

⚠️ **Voltar pro dev** — 1 blocker (B4), fix de 1-2 linhas. O B1 está ~90% resolvido (render inicial correta, ramo morto limpo, cadeia PHP→JS verificada); falta só o `find()->fetch()` em `buildScanCotaDataForLuz` para a cota sobreviver às re-renderizações pós-bipagem. Corrigido isso (e idealmente um teste do wiring da Luz), **0 blockers → merge liberado**. Minors m1/m2 e nit n1 do ciclo 1 seguem como TODO, não bloqueiam.

---

## Ciclo 1 — Re-revisão (status dos itens anteriores)

| Item | Status | Resumo |
|------|--------|--------|
| **B1** — fila única não reconstrói cota mission-wide | 🔴 **NÃO resolvido** | Fix aplicado no ramo errado (modo livre, que é dead code para separação). O fluxo ativo (fila única / Luz) continua sem `scan_cota`. |
| **B2** — decremento (`−`) consome/bloqueia cota | ✅ **Resolvido** | Gate `delta > 0` nos 3 JS. |
| **B3** — backstop com PT hardcoded | ✅ **Resolvido** | `show('scan_cota_finalizacao_recusada')` com `{qualificados}/{meta}/{faltam}`. |
| **M1** — sem teste de isolamento de tenant da Sonda | ✅ **Resolvido** (com ressalva) | `ScanCotaConfigSondaTddTest.php` cobre AC5.2/5.3/upsert/guards. Comentários do teste descrevem o "Bug #1" como se ainda existisse (stale). |
| **M2** — `montarScanCotaFront($jsItensData)` morto | ✅ **Resolvido** | Param removido; docblock documenta que o front reconstrói. |
| **Bug #2** — insert da Sonda via `make()/save()` | ✅ **Resolvido** | `salvarConfig` usa `$this->create()` (insert) e `$this->update()` (update). Sem raw SQL. |

**Veredito do ciclo**: 5 de 6 itens fechados com qualidade. **B1 reaparece** porque a correção foi instalada no caminho de renderização que a separação não usa. Continua sendo blocker — é a razão de ser de US2/US3 no fluxo principal de separação.

---

## Sumário

O ciclo 1 corrigiu B2, B3, M1, M2 e o Bug #2 com aderência ao padrão V2 (Sonda sem raw SQL, i18n consumido, gate de cota só no incremento, teste de tenant). **O B1, porém, foi resolvido apenas no ramo `modo livre` de `separacao/item.php` (linhas 518–530), que é código morto**: o handler de separação força `modoFilaUnica => true` (`SeparacaoHandler.php:217`), e nesse modo a tela renderiza exclusivamente a Luz `SeparacaoItemCard` (`separacao/item.php:412-425`), cujo `item_card.php` chama `execucaoCard()` **sem** a chave `scan_cota` (`luz/separacao/item_card.php:137-156`). Resultado: `data-scan-cota-ativo="0"` → `ExecucaoCard.initScanCota()` sai cedo com `disponivel = Infinity` → o bloqueio em tempo real **nunca dispara na separação**. O backstop server-side ainda segura a finalização (AC4), mas a UX de tempo real de US2/US3 continua inerte no fluxo principal de separação, exatamente como na rodada anterior. O carregamento, por outro lado, está correto e completo (via `execucao_bipagem.php` + Presenters). Recomendação: **voltar pro dev** — 1 blocker remanescente, fix localizado (passar `scan_cota` no `execucaoCard()` da Luz).

## Issues

### 🔴 Blocker (1 issue)

#### B1 (reaberto) — Fila única da separação continua sem cota: fix aplicado em ramo morto
**Onde**:
- Fix instalado (inerte): `componentes/Missoes/Logistica/sinais/App/html/separacao/item.php:518-530` (computa `disponivel` e passa em `scan_cota`), dentro do `<?php else: ?>` do `modoFilaUnica` (linha 427) — só executa no **modo livre**.
- Caminho real (sem cota): `componentes/Missoes/Logistica/sinais/App/html/luz/separacao/item_card.php:137-156` (`execucaoCard([...])` **sem** chave `scan_cota`).
- Gatilho do modo: `componentes/Missoes/Logistica/sinais/App/Handlers/SeparacaoHandler.php:217` (`'modoFilaUnica' => true` hardcoded).
- Consequência no JS: `componentes/Missoes/Logistica/sinais/App/html/shared/components/execucao/execucao_card.js:123-126` (`if (!this.scanCota.ativo) { this.scanCota.disponivel = Infinity; return; }`) + `execucao_card.php:52,378` (`$scanCota` default `ativo=false`).

**Problema**: A separação em execução sempre roda em `modoFilaUnica`. Nesse modo, `separacao/item.php` cai no ramo `if ($modoFilaUnica)` (linha 412) e renderiza a Luz `SeparacaoItemCard`; nunca entra no `else` (linha 427) onde a correção do `scan_cota`/`disponivel` foi colocada. A Luz delega para `item_card.php`, que invoca o `execucaoCard()` genérico **sem passar `scan_cota`** — então `execucao_card.php` usa o default `['ativo'=>false,...]`, emite `data-scan-cota-ativo="0"`, e o `ExecucaoCard.initScanCota()` curto-circuita com `disponivel = Infinity`. O `+`/`−` nunca bloqueia entre itens na separação. (O wrapper `luz/separacao/item_card.js` é só um stub de 11 linhas — o controller ativo é o `ExecucaoCard` genérico auto-iniciado por `execucao_card.php:869`.)

**Por que é blocker**: Mesma falha da rodada anterior (AC2.2/AC2.5/AC2.6 inertes no fluxo de separação). O fix endereçou o ramo errado; o caminho que o operador realmente usa segue sem enforcement de tempo real. Carregamento está OK, separação não.

**Como corrigir**: A Luz é onde a separação renderiza — passar `scan_cota` (com `disponivel` já computado) no `execucaoCard()` de `luz/separacao/item_card.php`. O handler `Luz_SeparacaoItemCard` (`SeparacaoHandler.php:383`) precisa fornecer `$missao` (ou o bloco já montado) + os itens com `bipavel/manchado` (já vêm de `SeparacaoPresenter`/`montarJsonFrontendBase`). Ex., no `item_card.php`:
```php
// no array do execucaoCard([...]):
'scan_cota' => (function () use ($missao, $jsItensData) {
    $bloco = \GalaxiaConstelacoes\Logistica\MissaoLogistica\Presenters\MissaoPresenter::montarScanCotaFront($missao);
    if (!empty($bloco['ativo'])) {
        $manchados = 0;
        foreach (($jsItensData ?? []) as $it) {
            if (!empty($it['bipavel']) && !empty($it['manchado'])) $manchados++;
        }
        $bloco['disponivel'] = max(0, (int) $bloco['manuais'] - $manchados);
    }
    return $bloco;
})(),
```
…garantindo que o handler passe `missao` e `jsItensData`/`todosItens` ao reencaminhar a Luz (`SeparacaoHandler.php:511-520, 552-561` hoje só passam `missaoId/itemAtual/progresso/luzId`). Alternativa mais limpa: dado que o fluxo de separação é sempre fila única, **remover** o bloco `else`/modo-livre morto de `separacao/item.php` e concentrar o wiring de `scan_cota` na Luz, evitando que a correção "pareça" feita sem estar no caminho ativo.

> **Status no ciclo 2**: esta orientação foi seguida (wiring na Luz + handler passando `missao`/`jsItensData`, ramo morto limpo). A render inicial ficou correta. Restou só o detalhe do `find()->fetch()` no `buildScanCotaDataForLuz` → ver **B4** no topo.

### 🟡 Major (0)

Nenhum remanescente. M1 e M2 do ciclo anterior resolvidos.

### 🟢 Minor (2)

#### m1 — Comentários do `ScanCotaConfigSondaTddTest` descrevem o Bug #2 (insert) como se ainda existisse
**Onde**: `componentes/Constelacoes/Logistica/MissaoLogistica/Testes/Sondas/ScanCotaConfigSondaTddTest.php:15-16, 178-211`.
**Problema**: O insert agora usa `create()` (corrigido). Mas o teste mantém o texto do "BUG #1" (`salvarConfig(insert): retorna true (mas é falso positivo)`, `make()/save() não existem...`). A lógica do teste continua válida — ele de fato verifica que 1 linha é criada (linhas 203-215) e passará no caminho feliz —, porém os comentários e o rótulo `A::bug(...)` induzem o leitor a achar que o insert ainda está quebrado.
**Como corrigir**: Atualizar a narrativa do teste para refletir o estado corrigido (insert via `create()` cria a linha); remover/ajustar o bloco `A::bug` ou convertê-lo em assert direto `A::eq(1, contarLinhas(...))`.

#### m2 — Backstop refaz query de itens já carregados em `finalizar` (carry-over do ciclo anterior)
**Onde**: `componentes/Constelacoes/Logistica/MissaoLogistica/Satelites/MissoesStatus.php:296-316`.
**Problema**: O gate de pendentes usa `$itemModel->findAny(...)`; o gate de cota faz novo `find(...)->fetch(true)`. Funcional (só roda quando `scan_cota.ativo`), mas é uma leitura extra no caminho de finalização.
**Como corrigir**: Opcional — reaproveitar uma única leitura nos dois gates.

### ⚪ Nit (1)

#### n1 — Migrations com nome de data (carry-over, intencional)
**Onde**: `componentes/Constelacoes/Logistica/MissaoLogistica/migrations/2026_06_17_*.sql`.
**Problema**: Checklist global pede `Migrations/NNN_*.sql`; esta constelação usa convenção de data localmente (`2026_01_22_*` etc.), então o dev seguiu o padrão da pasta. Sem ação.
**Como corrigir**: N/A (manter consistência local).

> Resolvidos neste ciclo e removidos da lista: console.log `[ScanCota]` (m3 anterior — confirmado removido dos 3 JS) e o typo `mostrarAvíso` (n1 anterior — agora `mostrarAvisoCotaBloqueada` em todos).

## Pontos fortes

- **B2/B3/M2/Bug#2 corrigidos com precisão.** Gate `isManualIncremento = (codigo === 'MANUAL' && delta > 0)` nos 3 JS (`carregamento/item.js:2769`, `separacao/item.js:2139`, `execucao_card.js:479-484`); decremento agora é sempre correção, sem consumir nem bloquear cota, e sem disparar o modal de "última cota" indevidamente. Backstop usa `show('scan_cota_finalizacao_recusada')` com os 3 placeholders (`MissoesStatus.php:334-338`).
- **Carregamento 100% wired.** `execucao_bipagem.php:1072-1087,1121-1123` chama `montarScanCotaFront($missao)`, emite os `data-scan-cota-*`, o i18n e `data-itens-json` com `bipavel/manchado` (via `CarregamentoPresenter`→`montarJsonFrontendBase`). A reconstrução mission-wide do `disponivel` no `CarregamentoV2.initScanCota` (`carregamento/item.js:1147-1155`) está correta e idempotente no refresh (AC2.6).
- **M1 entregue e robusto.** `ScanCotaConfigSondaTddTest` cobre default desligado (AC5.2), isolamento empresa A×B (AC5.3), upsert idempotente e guards de `empresaId<=0`. Integration-style (PDO) — roda em `dev_gx_goldie` conforme a memória `feedback_integration_tests`.
- **Sonda V2 desacoplada e sem raw SQL.** `salvarConfig` faz insert via `create()` e update via `update()`, `companyControl=false` + filtro explícito por `block_empresa_id`, default de fábrica desligado. Laboratório puro com 65/0 TDD passando.
- **php -l limpo** em todos os arquivos novos/alterados.

## Recomendação

⚠️ **Voltar pro dev** — 1 blocker remanescente (B1), de fix localizado:
- O backend já entrega tudo (`montarScanCotaFront`, `bipavel/manchado` no `jsItensData`). Falta apenas passar `scan_cota` (com `disponivel`) no `execucaoCard()` da **Luz** `luz/separacao/item_card.php` e fazer o handler `Luz_SeparacaoItemCard` repassar `missao` + itens. Idealmente, remover o ramo `else`/modo-livre morto de `separacao/item.php` para a correção não ficar "fantasma".
- Carregamento, Sonda, Laboratório, backstop e testes estão sólidos e aderentes ao V2 — nada a re-arquitetar.
- Minors m1 (comentário stale do teste) e m2 (query extra) podem entrar com TODO.

Endereçado o B1 no caminho da Luz, o merge fica liberado.

---

# Tela Admin — review focado (follow-up que não passou por review antes)

**Base**: working tree (dev_team, sem commit) — `git diff HEAD` + `git status`
**Escopo**: tela dev-only de configuração geral da cota (`Admin::scanCotaConfig`, `Laboratorios/ScanCotaConfig`, view/form/js `scan_cota_config`, `Sondas/ScanCotaConfigSonda`, snapshot em `GestaoMissoesBase::congelarScanCota`, i18n).
**Arquivos**: 4 novos (Lab, view, form, js no sinal Admin) + Sonda + GestaoMissoesBase + 3 idiomas.

## Sumário

Os 4 bugs estruturais do follow-up estão de fato corrigidos e verificados: (1) todas as 15 chamadas `show()` da feature usam **texto literal PT** e existem keyed por **md5** nos 3 idiomas; (2) view e form leem **variáveis extraídas** (`$scanCotaConfig`, `$empresaId`, `$formSalvarScanCota`), não `$data[...]`; (3) o construtor do Laboratório bate com o contrato do `goSatelite` (`new $call($this, $exe, $this->data)` — `GalaxiaRoute.php:481`); (4) save e snapshot resolvem o tenant pela **mesma** `ScanCotaConfigSonda::resolverEmpresaIdAtual()`. Sem SQL cru — `salvarConfig` usa `create()`/`update()` do model; o teste de Sonda confirma que o **insert agora persiste** (PASS=25/0). **Porém há 1 blocker remanescente**: o pré-preenchimento da tela (`Admin.php:493-496`) duplica a resolução de tenant com 2 tiers em vez de chamar `resolverEmpresaIdAtual()` (3 tiers) — exatamente a classe de divergência que o bug #4 visava eliminar. Além disso, **falta teste para o Laboratório novo** (gate + validação + orquestração de save), e algumas mensagens de erro do Lab escaparam do `show()`. Recomendação: **voltar pro dev** para fechar B1 e B2.

## Issues

### 🔴 Blocker (1)

#### B1 — Pré-preenchimento da tela resolve `empresa_id` por caminho diferente do save/snapshot
**Onde**: `componentes/Missoes/Logistica/sinais/Admin/Admin.php:493-496`
**Problema**: O comentário diz "MESMA fonte que GestaoMissoesBase::congelarScanCota()", mas o código **não** chama `ScanCotaConfigSonda::resolverEmpresaIdAtual()`. Ele reimplementa inline só 2 tiers:
```php
$empresaId = (int) ($userInfo->principalEmpresa->empresa_id
    ?? $userInfo->userData->empresa_id
    ?? 0);
```
A fonte canônica (`ScanCotaConfigSonda.php:68-96`) tem **3 tiers** — o 3º é o fallback `SessionGalaxia mainCompany->id`, que é **exatamente** o id que o `create()` do framework usa quando `principalEmpresa` é nulo (`ModelInteligencia::companyControl` linha do `mainCompany->id`). O save (`ScanCotaConfig.php:90`) e o snapshot (`GestaoMissoesBase.php:790`) usam `resolverEmpresaIdAtual()`. 
**Por que é blocker**: numa sessão sem `principalEmpresa->empresa_id` nem `userData->empresa_id` mas com `mainCompany->id` (cenário real de acesso dev/admin a empresa por sessão), a tela **lê e exibe a config da empresa errada** (`$empresaId=0` → `obterConfig(0)` → default desligado), enquanto o save grava na empresa do `mainCompany`. O dev vê "sem configuração", salva achando que está criando, e na verdade sobrescreve a config existente de outra empresa — ou pré-preenche com defaults que não são os reais. Viola a memória `feedback_block_columns`/bug #4 (save e snapshot DEVEM bater) e a própria afirmação do comentário.
**Como corrigir**: trocar as 4 linhas por:
```php
$empresaId = ScanCotaConfigSonda::resolverEmpresaIdAtual();
```
(import já existe no topo do arquivo, linha 12). Aí prefill, save e snapshot passam a usar a única fonte de verdade.

### 🟡 Major (2)

#### M1 — Laboratório novo sem teste
**Onde**: `componentes/Missoes/Logistica/sinais/Admin/Laboratorios/ScanCotaConfig.php` (sem espelho em `Testes/`)
**Problema**: existe `Testes/Sondas/ScanCotaConfigSondaTddTest.php` (cobre a Sonda, PASS=25/0) e o `ScanCotaLaboratorioTddTest.php` (regra pura), mas **não há** teste para o Laboratório de admin — o componente que concentra a lógica nova de runtime: gate dev (`gateDev`/`resolverUserId`), validação de `pct` (numeric/range) e dos toggles (0/1), o branch `default` ('Opção inválida'), e a orquestração save→`obterConfig`→payload `scanCotaConfig`. Nenhum desses contratos é coberto.
**Por que é major**: checklist hard — "Sem testes para Laboratório novo?". Esse Lab é justamente o que acumulou os 4 bugs por não ter passado por review/teste. As validações têm ramos que `php -l` não pega (ex.: `pct` não-numérico, toggle fora de {0,1}, gate negando user 0).
**Como corrigir**: criar `componentes/Missoes/Logistica/sinais/Admin/Testes/Laboratorios/ScanCotaConfigTddTest.php` (sufixo `TddTest`, espelho de pasta) cobrindo: gate nega user fora da allowlist; `pct` inválido/fora de range retorna `error`; toggles inválidos retornam `error`; save válido persiste e devolve `scanCotaConfig`; `opcao` desconhecida cai no `default`.

#### M2 — Mensagens de erro do Laboratório fora do `show()` (i18n incompleto)
**Onde**: `componentes/Missoes/Logistica/sinais/Admin/Laboratorios/ScanCotaConfig.php:40, 77, 129, 136, 177` (e `success('Config obtida')` na :184, esta interna)
**Problema**: as strings visíveis ao usuário `'Opção inválida'` (:40), `'Acesso restrito a desenvolvedores.'` (:77), `'Campo inválido: '.$campo` (:129), `'Empresa não identificada na sessão.'` (:136, :177) chegam ao toastr **sem** `show()`. As demais mensagens do mesmo arquivo (:116, :121, :151, :158) já usam `show()` — inconsistência interna. Confirmado por grep: dessas, nenhuma tem entrada nos `.json`.
**Por que é major**: checklist hard — "String visível sem `show()`". Em locale en/es essas mensagens vazam em PT. O bug #1 foi sobre isso; estas escaparam da varredura.
**Como corrigir**: envolver com `show()` os literais voltados ao usuário e adicionar as entradas keyed por md5 nos 3 idiomas (mesmo processo das outras 15 chaves da feature). `'Campo inválido: '.$campo` deve virar `show('Campo inválido.')` (sem concatenar o nome do campo na string traduzível) ou usar placeholder. `success('Config obtida')` (:184) é resposta de leitura interna não exibida — pode ficar, mas idealmente também via `show()` por consistência.

### 🟢 Minor (2)

#### m1 — Labels dos botões da index/voltar em texto cru
**Onde**: `componentes/Missoes/Logistica/sinais/Admin/html/admin.php:14` (`<span>Cota Bipagem</span>`) e `html/scan_cota_config.php:96` (`<i ...></i> Admin`)
**Problema**: o link novo "Cota Bipagem" e o texto "Admin" do botão voltar são literais sem `show()`. O `title` do link (:13 de admin.php) já usa `show()`. Observação: o botão "Ver Grupos" pré-existente (admin.php:8) também é cru — padrão legado da própria index, então o novo apenas seguiu o vizinho.
**Como corrigir**: `<span><?= show('Cota Bipagem') ?></span>` e `<?= show('Admin') ?>`, adicionando as 2 entradas md5 nos 3 idiomas. Baixa prioridade (tela dev-only), mas fecha o i18n.

#### m2 — Comentário stale no teste de Sonda diz que o insert "não persiste"
**Onde**: `componentes/Constelacoes/Logistica/MissaoLogistica/Testes/Sondas/ScanCotaConfigSondaTddTest.php:12-16, 174-211`
**Problema**: o bloco de comentário e a label da asserção descrevem "BUG #1 — make()/save() não persiste / falso positivo". O código atual da Sonda (`ScanCotaConfigSonda.php:170`) usa `$this->create($dadosInsert)` direto, e o teste **passa pelo ramo `else`** (`:213-214`, "1 linha criada") — ou seja, o insert persiste e o bug não existe mais. Os comentários ficaram defasados e confundem quem lê.
**Como corrigir**: limpar o bloco `BUG #1` (linhas 12-16 e 174-211) deixando só a asserção positiva "primeiro save insere 1 linha". Pode entrar com TODO.

### ⚪ Nit (2)

#### n1 — `start()` acessa `$data['sinalGxData']` sem `isset`
**Onde**: `componentes/Missoes/Logistica/sinais/Admin/Admin.php:529`
**Problema**: `if ($data['sinalGxData'] == 'scan-cota-config')` emite "Undefined array key" quando a rota `logistica/admin` é chamada sem `sinalGxData`. Não é regressão: os branches pré-existentes `grupos`/`editarGrupoModal` (`:532, :535`) já faziam igual; o novo só seguiu o padrão. Idealmente `($data['sinalGxData'] ?? '')`.

#### n2 — `block_usuario_id => userData->id ?? 1` é código morto no insert da Sonda
**Onde**: `componentes/Constelacoes/Logistica/MissaoLogistica/Sondas/ScanCotaConfigSonda.php:167`
**Problema**: o `create()` do framework (`ModelInteligencia.php:935`) **sobrescreve** `block_usuario_id` com `$galaxiaUserInfo->userData->id` incondicionalmente, então o `?? 1` da Sonda nunca tem efeito. Idem `block_filial_id => 1` (:166) é mantido (framework não mexe nele). Harmless; remover o `block_usuario_id` do array de insert deixaria a intenção mais clara.

## Pontos fortes

- **Bugs #1–#4 fechados e verificáveis**: as 15 strings `show()` conferem md5 e existem em pt/en/es (script de md5sum bateu 1/1/1 em todas); view e form usam variáveis extraídas com o cabeçalho documentando o porquê (Plates); construtor do Lab `($galaxia, string $opcao, ?array $data)` casa exatamente com `goSatelite` (`GalaxiaRoute.php:481`).
- **Tenant centralizado na Sonda**: `resolverEmpresaIdAtual()` documenta e replica a cadeia do `companyControl` do framework; save e snapshot já a consomem (só o prefill ficou de fora — B1).
- **Zero SQL cru**: `salvarConfig` faz upsert via `find()->fetch()` + `create()`/`update()` do model, com `turnOff(['customerControl','companyControl'])` consciente porque a tabela usa `block_empresa_id` e não `block_company_id` (coerente com a migration e o comentário da Sonda). Memória `feedback_no_raw_sql` respeitada.
- **Migration correta**: `Migrations`-style numerada por data, colunas `block_empresa_id/filial/usuario` presentes **sem** `DEFAULT 1`, índice único em `block_empresa_id` para o upsert idempotente (memória `feedback_block_columns`).
- **Gate dev em duas camadas**: controller (`Admin.php:487`, redireciona antes de renderizar, `redirect()` faz `exit`) e Laboratório (`gateDev()` em toda opção). `isDev(0)` retorna false — sessão ausente não vaza.
- **JS pareado defensivo**: `ajaxComplete` só age quando `notification_class==='success' && scanCotaConfig` presente — não interfere nos outros forms da index do Admin.
- **Sonda testada e verde**: `ScanCotaConfigSondaTddTest` PASS=25/0, cobrindo isolamento de tenant A↔B e os guards de `empresaId<=0`; o insert real persiste.

## Recomendação

⚠️ **Voltar pro dev** — 1 blocker (B1, divergência de resolução de tenant no prefill) + 2 majors (M1 teste do Lab, M2 i18n das mensagens de erro). B1 é uma correção de 1 linha; M2 é mecânico; M1 é o item que justamente fecharia o gap que originou esses 4 bugs. Fechados B1 e M2 (M1 pode entrar com TODO se houver pressa), o merge da tela admin fica liberado — a arquitetura V2 (Sonda/Laboratório/Sinal desacoplados, imitando Edicaorota) está correta, **não** precisa repensar techplan.
