diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index bbdd9615..e65c39eb 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -7,6 +7,7 @@ alwaysApply: false ## Status +- **E13 entregue em código (2026-07-27):** planejador de presença/giros. Núcleo decisório: `visitEligibility.ts` (puro, input explícito espelhando `TerritorialClassInput`) resolve as **5 condições ✓/—** (volume, headroom, rede, janela, encaixe) e as 3 contraindicações (`perdida`/`sem_rede`/`no_teto`, sempre aviso com contra-oferta, nunca bloqueio); `visitPlannerAnchors.ts` guarda as datas das fases (`construcao|consolidacao|ativacao`, cortes ilustrativos no precedente de `territorialClassAnchors.ts`, comparação por data civil da Bahia via `formatBahiaCivilDate` novo em `campaignTime.ts`); `visitPlannerData.ts` (`server-only`) agrupa candidatas por TI. Superfícies: card "Elegibilidade para visita" no `OverviewTab` do detalhe do município e compositor em `/campanha/atividades/giros` (TI → âncora/satélites/semente → gerar rascunhos), alcançado por botão "Planejar giro" na página de Atividades, sem entrada nova no sidebar. **A auditoria pré-implementação derrubou duas premissas do plano de 24/07:** `kind: 'visita'` nunca entrou na migration do C12 e **não entra** — `activity.deputyPresent` já É o marcador de presença do candidato (o giro é derivável por `deputyPresent` + TI + janela de datas), e criar o enum custaria migration por um kind sobreposto a `reuniao_apoio`/`ato`; e `allocationDecision` não tem write path em `src/` (só o int spec), então o "não vá" fica textual e gravar o override virou gatilho do **E11**, que precisa desse write path de qualquer forma. Terceira correção: "satélites contíguos" era impossível — `bahiaTerritories.ts` só tem pertencimento, adjacência de polígono existe apenas nos scripts de build, então **encaixe = mesmo TI**, dito com essas palavras na UI (construir adjacência é o rabbit hole "otimizador de rota" que o plano já proibia). **Duas decisões de implementação que valem por si:** (1) o teto-do-campo-não-capturado e as medianas do catálogo saíram do classificador do **E10** para `municipalityPotential.ts` (`uncapturedFieldVotes`, `catalogMedianUncapturedFieldVotes`, `catalogMedianProjectedValidVotes`, memoizadas) — sem isso o card "Elegibilidade" e a coluna "Classe" seriam duas contas do mesmo número na mesma tela, e ninguém saberia qual estava certa; (2) `createTourDraftActivitiesRecord` relê os ids das paradas com `overrideAccess: false` **dentro** da transação e aborta se algum não voltar, porque o `create` de `activity` é um booleano de staff e Payload não expressa restrição por município no create — buraco achado por int test, sem o qual um assessor semeava rascunhos no estado inteiro postando ids que o compositor nunca lhe ofereceu. Também: `loadMunicipalityVisitEligibility` roda o **mesmo** loader recortado no TI em vez de um segundo avaliador (encaixe é afirmação sobre vizinhos — não é avaliável para um município sozinho, e dois avaliadores começariam a discordar); `composeTourSuggestion` prioriza âncora → semente → satélites (a primeira versão deixava os satélites consumirem as vagas e a semente de expansão nunca aparecia); caps `TOUR_MAX_SATELLITES` 3 e `MAX_TOUR_STOPS` 8, verificados no cliente e revalidados na action; `TourRegionPicker` monta href com `usePathname` + `encodeURIComponent` em vez do serializador canônico (lição do **B14**: arrastaria `bahiaTerritories` + `municipalityCatalog` para o bundle). Dois verbetes E18 (`elegibilidade-para-visita`, `fase-do-calendario`). Suítes **fundidas, não acrescentadas** (arquivos separados empurraram unit e int para timeout): views do planejador em `tests/unit/visitEligibility.unit.spec.ts`, compositor em `tests/int/campaignActivity.int.spec.ts`; `CALENDAR_PHASES`/`VISIT_CONDITIONS`/`VISIT_CONTRAINDICATIONS` são exportadas e varridas lá para provar completude dos rótulos (um `const` usado só como tipo é warning de ESLint, e a varredura é a razão honesta de ser público). First Load JS: `/campanha/atividades/giros` 8,59 kB / 226 kB; detalhe do município 7,3 kB / 232 kB. Sem migration, sem collection, sem `Consent`, sem entidade `tour` persistida. Gate verde e bare (tsc, lint, format:check, knip com o P3 pré-existente, check:cycles, 429 unit+int, build local); Aikido **0 findings**. **e2e não serviu de veredito:** `campaignMunicipalities.e2e.spec.ts` falha em árvore stashada também, em teste diferente a cada rodada — o flake já ledgerizado. **Adiados com gatilho:** override "não vá" gravando `allocationDecision` → **E11**; níveis N0–N4 no gatilho do "não vá" → **E14** (até lá, classe E10 + rede vazia); padrões J como sugestão automática → E11 fase 2; entidade `tour` persistida → 3º giro real composto. Plano as-built: [`planejador-de-giros.md`](../../../docs/plans/planejador-de-giros.md). - **B42 entregue em código (2026-07-27):** polimento mobile de `/campanha/municipios`. **Escopo ampliado na sessão (decisão do usuário): Votos estimados entrou** junto com Tendência e Assessores, o que fez o item passar de 2 para **3 controles com a mesma casca** — critério de extração do repo — e **inverteu a questão em aberto do plano** (que recomendava branch local e extração só no `/simplify`): nasceu `src/components/campaign/shared/CampaignCellEditOverlay.tsx`, Popover em `md+` / Drawer abaixo. A casca carrega **só o container**: cada controle segue dono do seu auto-save, e como o `handleOpenChange` de cada um passa direto para `onOpenChange`, fechar o Drawer grava exatamente como dispensar o Popover (é o que preserva o flush da justificativa do **B24 ✓**). O **Sinal (B26 ✓) não migrou, e o motivo que registrei primeiro estava errado** — não é que "a casca não expressa isso sem virar genérica": o `