From aa12e173d8eeaaae92dc0ba736f594d372d10238 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?=
Date: Thu, 23 Jul 2026 19:12:22 -0300
Subject: [PATCH] =?UTF-8?q?Ship=20A10=20vote=20estimate=20scenarios=20(pes?=
=?UTF-8?q?simistic/central/optimistic)=20for=20Pra=C3=A7as.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Staff surfaces share one Cenário control; leaders keep a single declaredVotes value. Unlocks E8.
---
.cursor/rules/campanha-edit-where-you-see.mdc | 11 +-
.cursor/rules/projects/nucleos-eleitorais.mdc | 7 +-
...mponents-campaign-plazalistoverview-tsx.md | 102 ++++++++
...ents-campaign-plazamappanel-tsx-cenario.md | 92 +++++++
AGENTS.md | 4 +-
PRODUCT.md | 9 +-
docs/plans/cenarios-estimativa-votos.md | 47 ++--
docs/roadmap.md | 17 +-
.../campanha/(app)/pracas/[slug]/page.tsx | 13 +-
.../(app)/pracas/[slug]/pledgeFormActions.ts | 9 +-
.../(app)/pracas/expected-votes/route.ts | 92 +++++++
.../(app)/pracas/expected-votes/types.ts | 12 +
.../(campaign)/campanha/(app)/pracas/page.tsx | 129 +++++-----
.../(app)/pracas/plazaStaffFormActions.ts | 8 +-
src/app/(campaign)/campanha/actions/plaza.ts | 12 +-
.../(campaign)/campanha/actions/votePledge.ts | 11 +-
src/collections/Plaza.ts | 63 ++++-
src/collections/VotePledge.ts | 73 ++++--
src/components/campaign/CampaignDashboard.tsx | 17 +-
src/components/campaign/CampaignInfoHint.tsx | 27 +++
.../campaign/CampaignMetricStrip.tsx | 12 +
.../campaign/PlazaEstimateScenarioContext.tsx | 34 +++
src/components/campaign/PlazaList.tsx | 29 +--
.../PlazaListExpectedVotesControl.tsx | 227 +++++++++++++-----
src/components/campaign/PlazaListOverview.tsx | 154 +++++++++---
src/components/campaign/PlazaMapPanel.tsx | 56 +++--
src/components/campaign/PlazaPledgesPanel.tsx | 80 +++---
src/components/campaign/PlazaStrategyCard.tsx | 15 +-
src/components/campaign/PlazaStrategyForm.tsx | 18 +-
.../campaign/PledgeEstimateForm.tsx | 82 ++++---
.../campaign/StaffPlazaVotesDisplay.tsx | 115 +++++++--
.../campaign/VoteEstimateScenarioField.tsx | 55 +++++
.../campaign/VoteEstimateScenarioInputs.tsx | 167 +++++++++++++
.../campaign/VoteEstimateScenarioStrip.tsx | 147 ++++++++++++
src/lib/formData.ts | 34 +++
src/lib/schemas/plaza.ts | 3 +-
src/lib/schemas/votePledge.ts | 25 +-
...0723_124200_add_vote_estimate_scenarios.ts | 50 ++++
src/migrations/index.ts | 52 ++--
src/payload-types.ts | 32 ++-
src/utilities/campaignDashboardData.ts | 20 +-
src/utilities/plazaMapData.ts | 70 +++++-
src/utilities/plazaPageData.ts | 19 +-
src/utilities/plazaViewModels.ts | 18 +-
src/utilities/voteEstimate.ts | 154 ++++++++++++
src/utilities/voteEstimateScenarioFields.ts | 32 +++
src/utilities/votePledgeData.ts | 146 +++++++++--
tests/int/campaignPlazaAccess.int.spec.ts | 18 +-
tests/int/campaignVotePledge.int.spec.ts | 16 +-
tests/int/plazaPageData.int.spec.ts | 17 +-
tests/unit/campaignComponents.unit.spec.ts | 58 ++++-
tests/unit/plazaStaffVoteTotal.unit.spec.ts | 33 ++-
tests/unit/voteEstimate.unit.spec.ts | 146 +++++++++++
vitest.setup.ts | 5 +
54 files changed, 2388 insertions(+), 506 deletions(-)
create mode 100644 .impeccable/critique/2026-07-23T21-29-20Z__src-components-campaign-plazalistoverview-tsx.md
create mode 100644 .impeccable/critique/2026-07-23T21-52-39Z__src-components-campaign-plazamappanel-tsx-cenario.md
create mode 100644 src/app/(campaign)/campanha/(app)/pracas/expected-votes/route.ts
create mode 100644 src/app/(campaign)/campanha/(app)/pracas/expected-votes/types.ts
create mode 100644 src/components/campaign/CampaignInfoHint.tsx
create mode 100644 src/components/campaign/PlazaEstimateScenarioContext.tsx
create mode 100644 src/components/campaign/VoteEstimateScenarioField.tsx
create mode 100644 src/components/campaign/VoteEstimateScenarioInputs.tsx
create mode 100644 src/components/campaign/VoteEstimateScenarioStrip.tsx
create mode 100644 src/migrations/20260723_124200_add_vote_estimate_scenarios.ts
create mode 100644 src/utilities/voteEstimate.ts
create mode 100644 src/utilities/voteEstimateScenarioFields.ts
create mode 100644 tests/unit/voteEstimate.unit.spec.ts
diff --git a/.cursor/rules/campanha-edit-where-you-see.mdc b/.cursor/rules/campanha-edit-where-you-see.mdc
index 528978e6..e33b68af 100644
--- a/.cursor/rules/campanha-edit-where-you-see.mdc
+++ b/.cursor/rules/campanha-edit-where-you-see.mdc
@@ -6,14 +6,14 @@ alwaysApply: false
# Edit where you see (`/campanha`)
-Canonical principle: `PRODUCT.md` § Design Principles — **Edit where you see**. Precedente: B9 (`PlazaList*Control` + `plazaStaffFormActions`).
+Canonical principles: `PRODUCT.md` § Design Principles — **Edit where you see** + **Auto-save, no Save button**. Precedente: B9 (`PlazaList*Control` + `plazaStaffFormActions`).
When adding or changing a **staff** (coordinator / advisor) view that displays mutable campaign data:
1. **Default to in-context edit** if the role can update that field — list cell, detail card, queue row — not a dead readout that only links to `/editar`.
2. **Reuse existing server actions** and the **same access** as the full form. Thin route wrappers OK; do not invent a generic quick-edit service (<3 call sites → named controls).
-3. **UX pattern:** named Popover / select / short number input per affordance; touch `min-h-11`; save → server revalidate / RSC refresh. Prefer Enter + explicit save over accidental blur writes on mobile.
-4. **Keep `/editar` (or long Sheet)** for multi-field strategy forms, rare fields, and flows that need a note or confirmation.
+3. **UX pattern:** named Popover / select / short number input per affordance; touch `min-h-11`. **Auto-save** on select change, blur, or short debounce — no “Salvar” button on single-field / in-context edits. Show pending / saved / error on the control (Feel the action). Then server revalidate / RSC refresh.
+4. **Keep `/editar` (or long Sheet) + explicit submit** only for multi-field strategy forms, rare fields, and flows that need a note, confirmation, or legal consent.
5. **Never** expose staff-only fields to `leader`. View models stay role-split.
## Anti-goals
@@ -22,7 +22,10 @@ When adding or changing a **staff** (coordinator / advisor) view that displays m
- Always-mounted inputs on every list row
- Generic `EditableCell` / contentEditable mass edit
- Optimistic UI that skips server refresh when the list is source of truth
+- Save buttons on Popovers / quick-edit cells “just in case”
+- Silent auto-save with no status feedback
+- Saving on every keystroke for free-text without debounce
## Plan / Impeccable brief
-For UI items (B/C/D): answer explicitly — _does this surface need in-context edit?_ Cite this principle and B9 when yes; name rabbit hole “spreadsheet mode” when scoping.
+For UI items (B/C/D): answer explicitly — _does this surface need in-context edit?_ Cite these principles and B9 when yes; name rabbit hole “spreadsheet mode” when scoping. Prefer auto-save over an explicit Save button unless the write is multi-field / confirmatory / consent.
diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc
index 604c8049..1549d379 100644
--- a/.cursor/rules/projects/nucleos-eleitorais.mdc
+++ b/.cursor/rules/projects/nucleos-eleitorais.mdc
@@ -7,8 +7,10 @@ alwaysApply: false
## Status
+- **A10 entregue em código (2026-07-23; deploy pendente com remodelagem):** cenários pessimista/média/otimista em `votePledge.estimatedVotes` e `plaza.expectedVotes` (groups + migration `20260723_124200_add_vote_estimate_scenarios`, backfill scalar→`central`); agregação por cenário em `votePledgeData`/`plazaMapData`/overview/dashboard; seletor de cenário no mapa 2026; lista/dashboard com média + faixa; liderança intacta (`declaredVotes` único). Plano: [`cenarios-estimativa-votos.md`](../../../docs/plans/cenarios-estimativa-votos.md). Desbloqueia **E8**.
+- **Polish overview “Votos estimados” (2026-07-23):** `PlazaEstimateScenarioProvider` sincroniza Cenário do mapa com overview + células da lista; seletor Cenário visível em qualquer ano (hint quando ≠2026); labels `Média nas Praças filtradas` + `Pessimista · Otimista`; `sem estimativa` sob o hero. Critique: `.impeccable/critique/2026-07-23T21-29-20Z__src-components-campaign-plazalistoverview-tsx.md`.
+- **Cenário no overview (2026-07-23):** seletor relocado para a visão geral; no mapa só com Ano=2026; disclaimer inline → `CampaignInfoHint` (Popover `?`); flash no overview ao trocar. Critique: `.impeccable/critique/2026-07-23T21-52-39Z__src-components-campaign-plazamappanel-tsx-cenario.md`. **capture-review-debts:** 0 IDs no roadmap; flash compartilhado adiado no plano A10 (gatilho: 2º flash client-side em `/campanha`).
- **Programa WhatsApp interno D3–D5 registrado (2026-07-23):** comunicação 1:1 staff↔lideranças via bridge não oficial (não WABA / não massa); **número pessoal** de cada assessor/CG com pareamento QR no app (parece chat pessoal — pedido produto); fatias D3 multi-sessão → D4 envio pela sessão do ator → D5 inbox da própria sessão→rascunhos; baixa/média prioridade; Consent `campanha-whatsapp-canal`. Plano-mestre: [`whatsapp-interno-campanha.md`](../../../docs/plans/whatsapp-interno-campanha.md).
-- **A10 registrado no roadmap (2026-07-23):** cenários pessimista/média/otimista nas estimativas staff (`votePledge` + `plaza.expectedVotes` + lista/mapa); liderança continua com um `declaredVotes`; E8 passa a depender de A10. Plano: [`cenarios-estimativa-votos.md`](../../../docs/plans/cenarios-estimativa-votos.md). Fonte: feedback coordenação geral (áudio em `docs/general-coordinator-interview/`).
- **Discovery "Inteligência de campanha" aprovado (2026-07-21):** ciclo literatura→persona→entrevista sintética concluído e colaborado; embasamento canônico em [`docs/research/`](../../../docs/research/) (compêndio ~67 fontes, persona, relatório com kernel Rumelt + playbook de 25 padrões dado→decisão P/T/I/J/K + anti-goals). Programa de produto registrado no roadmap como **E8–E15 + B13 + C12** ([plano-mestre `inteligencia-campanha.md`](../../../docs/plans/inteligencia-campanha.md)) — conta da cadeira, fila de alocação, registro-fundação (trajetória de pledge/sinais/decisões), classificação relativa, escala relativa no mapa, níveis N0–N4, motor de sugestões (humano no loop), camada TI, planejador de giros, backtest. Decisões de produto travadas: % estadual absoluto é anti-métrica; denominadores usam válidos projetados (nunca aptos); sugestão nunca decide sozinha; nível de envolvimento não vaza para fora do staff.
- **B12 entregue em código (2026-07-21; deploy pendente com remodelagem):** `fitToKeys` + `interactiveKeys` no Mapa das Praças (`plazasByIbgeCode`); `canonicalMapKeysKey`; hover stroke-only (fill inalterado); clear síncrono no mouseout; fit ao footprint filtrado sem re-zoom em Ano/Escala. Plano: [`aproximar-mapa-pracas.md`](../../../docs/plans/aproximar-mapa-pracas.md).
- **B6 entregue em código (2026-07-21; deploy pendente com remodelagem):** `BahiaMap` setStyle incremental — layer GeoJSON estável entre troca de ano/métrica/escala; `pathByKeyRef` + restyle O(2) no hover/select; `fitBounds` só em `mode`/`highlightKeys`; helpers puros em `bahiaMapStyle.ts` + unit `bahiaMapStyle.unit.spec.ts`. Plano: [`escala-dry-pos-b3.md`](../../../docs/plans/escala-dry-pos-b3.md). Fecha débitos de perf de **B10** e **B11**.
@@ -68,11 +70,12 @@ alwaysApply: false
## Decisões
+- Em 2026-07-23, **auto-save sem botão Salvar:** campos de edição devem persistir automaticamente sempre que for seguro (change de select, blur ou debounce curto); sem botão “Salvar” em edição in-context / campo único. Submit explícito só para formulários multi-campo atômicos, nota/confirmação ou consentimento. Princípio 4 de [`PRODUCT.md`](../../../PRODUCT.md); regra Cursor [`.cursor/rules/campanha-edit-where-you-see.mdc`](../campanha-edit-where-you-see.mdc). **Rejeitado / supersede:** preferência B9 por Enter + botão Salvar em quick-edit; Save “por precaução” em Popover.
- Em 2026-07-23, **WhatsApp interno 1:1 (D3–D5):** bridge não oficial; **sessão = número pessoal** de cada `coordinator`/`advisor` (QR no `/campanha`, estilo WhatsApp Web); liderança recebe no chat de sempre; Teqo = system of record; humano no loop em D5; sidecar multi-sessão. **Rejeitado:** WABA; massa; chip institucional único; auto-commit; inbox cruzado CG↔assessores na v1. Cortável (`wa.me`).
- Em 2026-07-23, **estimativas staff em três cenários (A10):** pessimista / média (`central`) / otimista no pledge e no total da Praça; liderança declara um valor; default de leitura = média; não fundir com `voteGoals`. **Rejeitado:** um único estimado; expor faixa à liderança; três choropleths. Desbloqueia E8.
- Em 2026-07-21, **embasamento de produto/domínio via `docs/research/`:** decisões de análise territorial e mapa citam o relatório de discovery (aprovado 2026-07-21) antes de inventar métrica nova. Regras derivadas: leitura relativa (quantis/LQ/rank/captura) sobre % absoluto; limiares defesa/ataque 35/20/10 degeneram para DF e serão recalibrados com âncoras relativas (E10); roll-off (brancos+nulos DF − majoritária) é o headroom de orientação; abstenção estrutural corrige denominador (válidos projetados) e não é mercado; agregação regional default = Território de Identidade com salvaguardas MAUP (praça manda no voto, TI na logística). **Rejeitado:** cartogramas; pesquisa estadual de intenção para DF; persuasão de território hostil como funcionalidade.
- Em 2026-07-21, **toolchain TypeScript dual:** `typescript` → `@typescript/typescript6@6.0.2` (API/editor/ESLint + `tsc6`); `typescript-7` → `typescript@7.0.2` (Go `tsc` para `pnpm typecheck`). Motivo: typescript-eslint peer `<6.1.0` e API estável só em TS ~7.1. Não usar `@typescript/native-preview`/`tsgo` (canal nightly). `typecheck:tsc6` existe para paridade. Ambient CSS/SCSS em `src/css-modules.d.ts` (TS6 `noUncheckedSideEffectImports`).
-- Em 2026-07-21, **decisão de produto (Field Desk):** em `/campanha`, superfícies staff de visualização que mostram dados mutáveis devem oferecer edição no contexto quando o access permitir (“edit where you see”) — princípio 3 de [`PRODUCT.md`](../../../PRODUCT.md); regra Cursor [`.cursor/rules/campanha-edit-where-you-see.mdc`](../campanha-edit-where-you-see.mdc). Precedente: **B9** ([`edicao-rapida-lista-pracas.md`](../../../docs/plans/edicao-rapida-lista-pracas.md) — `PlazaList*Control` + `plazaStaffFormActions`). **Rejeitado:** lista/detalhe só-leitura com toda mutação em `/editar`; data-grid / planilha full-row; service genérico de “quick edit”. `/editar` e Sheets longos ficam para formulários multi-campo, campos raros e fluxos com nota/confirmação. Liderança continua no escopo já definido (sem campos staff-only).
+- Em 2026-07-21, **decisão de produto (Field Desk):** em `/campanha`, superfícies staff de visualização que mostram dados mutáveis devem oferecer edição no contexto quando o access permitir (“edit where you see”) — princípio 3 de [`PRODUCT.md`](../../../PRODUCT.md); regra Cursor [`.cursor/rules/campanha-edit-where-you-see.mdc`](../campanha-edit-where-you-see.mdc). Precedente: **B9** ([`edicao-rapida-lista-pracas.md`](../../../docs/plans/edicao-rapida-lista-pracas.md) — `PlazaList*Control` + `plazaStaffFormActions`). Complementado em 2026-07-23 pelo princípio **Auto-save, no Save button**. **Rejeitado:** lista/detalhe só-leitura com toda mutação em `/editar`; data-grid / planilha full-row; service genérico de “quick edit”. `/editar` e Sheets longos ficam para formulários multi-campo, campos raros e fluxos com nota/confirmação. Liderança continua no escopo já definido (sem campos staff-only).
- Núcleo Eleitoral é a unidade operacional; Zona TSE é uma referência oficial adicional.
- Todo núcleo exige geografia; natureza organizativa e setor são dimensões complementares.
- Exclusão no app é substituída por arquivamento.
diff --git a/.impeccable/critique/2026-07-23T21-29-20Z__src-components-campaign-plazalistoverview-tsx.md b/.impeccable/critique/2026-07-23T21-29-20Z__src-components-campaign-plazalistoverview-tsx.md
new file mode 100644
index 00000000..bc3f78d1
--- /dev/null
+++ b/.impeccable/critique/2026-07-23T21-29-20Z__src-components-campaign-plazalistoverview-tsx.md
@@ -0,0 +1,102 @@
+---
+target: /campanha/pracas overview Votos estimados no conjunto
+total_score: 20
+p0_count: 0
+p1_count: 3
+p2_count: 2
+p3_count: 1
+timestamp: 2026-07-23T21-29-20Z
+slug: src-components-campaign-plazalistoverview-tsx
+---
+Method: dual-agent (A: c0c0346d-83f2-4aa7-b728-c883635b063e · B: 104c4dcf-5d13-4351-b6fe-db8d2d632179)
+
+## Design Health Score
+
+| # | Heuristic | Score | Key Issue |
+|---|-----------|-------|-----------|
+| 1 | Visibility of System Status | 2 | Número muda com filtros, mas nunca diz que é o cenário Média; mapa pode divergir |
+| 2 | Match System / Real World | 2 | “Faixa” e “no conjunto” não nomeiam pessimista–otimista nem o recorte filtrado |
+| 3 | User Control and Freedom | 3 | Agregado read-only correto; filtros controlam o conjunto; sem seletor de cenário no strip |
+| 4 | Consistency and Standards | 2 | Dashboard: “Votos estimados” + célula própria p/ sem estimativa; lista empacota no meio |
+| 5 | Error Prevention | 2 | `—` quando total=0; faixa some se falta um extremo (pós-backfill só central) |
+| 6 | Recognition Rather Than Recall | 1 | Exige lembrar A10: default=média; Faixa=P–O; conjunto=filtrado |
+| 7 | Flexibility and Efficiency | 2 | Denso p/ Alex; cenário só no mapa / Popover da linha |
+| 8 | Aesthetic and Minimalist Design | 3 | Field Desk limpo; ênfase justificada; leve hero-KPI |
+| 9 | Error Recovery | 2 | Qualidade do total (`sem estimativa`) enterrada em Declarações |
+| 10 | Help and Documentation | 1 | Sem microcopy “Média” / “Pessimista–Otimista” |
+| **Total** | | **20/40** | **Acceptable — craft visual ok; informação do cenário fraca** |
+
+## Anti-Patterns Verdict
+
+**LLM assessment:** Não é slop SaaS genérico (sem purple, side-stripe, card grid). Field Desk se lê: stone, Inter, priority lift permitido. Resíduo de **hero-metric template**: número grande enfatizado + detalhe tipográfico fraco, sem o vocabulário A10 (Média / Pessimista / Otimista) que forms e mapa já usam.
+
+**Deterministic scan:** CLI `detect.mjs` em `PlazaListOverview.tsx` + `CampaignMetricStrip.tsx` → **exit 0, `[]`** (0 findings).
+
+**Visual overlays:** Login wall em `/campanha/pracas` → overlay só no login (2 warnings fora de escopo: `flat-type-hierarchy`, `nested-cards` shadcn). Strip autenticado não inspecionado ao vivo. Screenshot: `.impeccable/assessment-b-campanha-pracas-login.png`.
+
+## Overall Impression
+
+O strip está certo em *estrutura* A10 (média grande + faixa secundária, não três KPIs) e no lugar certo (acima da tabela, no recorte filtrado). Falha em *rotular a semântica*: o número hero não diz “Média”, “Faixa” não diz pessimista–otimista, e o mapa acima pode mostrar outro cenário sem o strip confessar o lock em `central`. Em reunião, Marcos não sabe se está olhando chão, caso-base ou teto.
+
+## What's Working
+
+1. **Priority strip 3 células** — overview acima da lista, não card grid; lift alinhado a DESIGN.md.
+2. **Rollup filtrado** — `rollupPlazaStaffVotes` torna “conjunto” funcionalmente o recorte, mesmo com label mole.
+3. **Restrição de produto** — sem % estadual, sem gauge, sem três KPIs; absolute votos + tabular nums.
+
+## Priority Issues
+
+### [P1] Número grande sem rótulo “Média”
+- **Why:** Default A10 é `central`; a UI só diz “Votos estimados no conjunto”. Coordenação pergunta “isso é o otimista?”.
+- **Fix:** Label ou detail explícito: “Média” / “Cenário média (padrão)”.
+- **Suggested command:** `/impeccable clarify`
+
+### [P1] “Faixa X–Y” sem pessimista–otimista
+- **Why:** Pode ser CI, meta ou goals; conflita com vocabulário A10 já nos forms/mapa.
+- **Fix:** “Pessimista X · Otimista Y” ou mini `VoteEstimateScenarioStrip` endpoints.
+- **Suggested command:** `/impeccable clarify`
+
+### [P1] Mapa Cenário vs strip dessincronizado
+- **Why:** Mapa (acima) troca P/M/O; strip fica em central sem cue — duas verdades na mesma scroll.
+- **Fix:** Âncora explícita (“sempre média”) **ou** strip segue o cenário do mapa (estado local compartilhado).
+- **Suggested command:** `/impeccable layout`
+
+### [P2] `sem estimativa` enterrado em Declarações
+- **Why:** Qualidade do hero number some no meio da célula; dashboard já tem célula própria.
+- **Fix:** Footnote sob o total ou alinhar layout ao dashboard.
+- **Suggested command:** `/impeccable layout`
+
+### [P2] “No conjunto” subespecificado
+- **Why:** Não diz “Praças filtradas”; fácil ler como estadual/escopo total.
+- **Fix:** “Média nas Praças filtradas” (resolve P1+P2 de naming).
+- **Suggested command:** `/impeccable clarify`
+
+### [P3] Linguagem visual ≠ linhas da tabela
+- **Why:** Linhas usam ScenarioStrip; overview só tipografia.
+- **Fix:** Strip compacto endpoints no detail.
+- **Suggested command:** `/impeccable polish`
+
+## Persona Red Flags
+
+**Alex (CG):** Usa o número no briefing sem notar mapa em pessimista vs strip em média.
+
+**Jordan (assessor novo):** “Faixa” + “conjunto” + número sem “Média” → confunde com `voteGoals`.
+
+**Sam (a11y):** Detail em `
` solto; nomes de cenário ausentes do summary falado do hero.
+
+**Marcos (CG em reunião):** Precisa chão/caso-base/teto num glance; recebe KPI absoluto + faixa críptica.
+
+## Minor Observations
+
+- Chrome duplo (section priority + dl card) um pouco pesado.
+- Dashboard vs lista: labels e packing de `missingEstimateCount` divergem.
+- `staffVoteTotal > 0 ? format : '—'` trata zero como vazio.
+- Pós-backfill só-central: some a faixa e o número parece mais certo do que é.
+- Agregado read-only correto (edit na linha) — sem violação Edit-where-you-see.
+
+## Questions to Consider
+
+- O strip é âncora fixa de caso-base ou gêmeo esquecido do seletor do mapa?
+- Trocar “Faixa” por “Pessimista · Otimista” ensina o modelo sem custar density?
+- “Média nas Praças filtradas” aposenta três ambiguidades de uma vez?
+- `sem estimativa` deveria ser footnote do hero, não do meio?
diff --git a/.impeccable/critique/2026-07-23T21-52-39Z__src-components-campaign-plazamappanel-tsx-cenario.md b/.impeccable/critique/2026-07-23T21-52-39Z__src-components-campaign-plazamappanel-tsx-cenario.md
new file mode 100644
index 00000000..c7d766e0
--- /dev/null
+++ b/.impeccable/critique/2026-07-23T21-52-39Z__src-components-campaign-plazamappanel-tsx-cenario.md
@@ -0,0 +1,92 @@
+---
+target: /campanha/pracas Cenário selector + disclaimer
+total_score: 18
+p0_count: 0
+p1_count: 2
+p2_count: 2
+p3_count: 1
+timestamp: 2026-07-23T21-52-39Z
+slug: src-components-campaign-plazamappanel-tsx-cenario
+---
+Method: dual-agent (A: c3613972-8d1f-41bb-b210-501b0ddc14c9 · B: 8ae72814-5de3-4010-9ebb-7c9d64d8ccec)
+
+## Design Health Score
+
+| # | Heuristic | Score | Key Issue |
+|---|-----------|-------|-----------|
+| 1 | Visibility of System Status | 1 | Ano default 2022: mudar Cenário não pinta o mapa; efeito no overview abaixo do fold |
+| 2 | Match System / Real World | 1 | “overview” / “preenchimento” ≠ linguagem de campo; toolbar diz “filtro do mapa” |
+| 3 | User Control and Freedom | 3 | Três opções reversíveis; baixo risco |
+| 4 | Consistency and Standards | 2 | Mesmo NativeSelect que Ano/Escala, mas escopo de página (não só mapa) |
+| 5 | Error Prevention | 1 | Posição convida o modelo errado; disclaimer é pós-fato |
+| 6 | Recognition Rather Than Recall | 2 | Labels P/M/O ok; regra “só 2026” exige ler o rodapé |
+| 7 | Flexibility and Efficiency | 2 | Sync global é eficiente se entendido; sem atalho Ano→2026 |
+| 8 | Aesthetic and Minimalist Design | 2 | Quarto controle + disclaimer xs no estado comum (Ano≠2026) |
+| 9 | Error Recovery | 2 | Mapa “não fez nada” sem live region no overview |
+| 10 | Help and Documentation | 2 | Hint existe; copy de engenheiro; sempre ligada no first paint |
+| **Total** | | **18/40** | **Poor — labels A10 ok; assento + disclaimer quebram o controle** |
+
+## Anti-Patterns Verdict
+
+**LLM assessment:** Não é slop SaaS. Padrão Field Desk (label + NativeSelect + min-h-11). Falha = **apology UI**: controle de escopo de página sentado na toolbar do mapa, coberto por nota de rodapé. “overview” / “preenchimento” soam PR comment, não Field Desk.
+
+**Deterministic scan:** CLI `detect.mjs` em `PlazaMapPanel.tsx` + `PlazaEstimateScenarioContext.tsx` → **exit 0, `[]`**.
+
+**Visual overlays:** Autenticado em `/campanha/pracas`. Overlay runtime 12 hits (maioria FP de shell/Leaflet/Inter). Disclaimer medido: 70 chars, wrap estreito ~176×84. Screenshot: `.impeccable/assessment-b-campanha-pracas.png`. Cenário + disclaimer visíveis com Ano≠2026.
+
+## Overall Impression
+
+A10 labels e sync via context estão certos. O bug de UX é estrutural: **Cenário mora na fila Ano/Escala/Comparar**, então promete pintar o mapa; no first paint (Ano=2022) só move overview/lista — e o disclaimer admite isso em jargão. Corrigir assento > alongar copy.
+
+## What's Working
+
+1. **Pessimista / Média / Otimista** — vocabulário A10 certo; default Média.
+2. **Um cenário sincronizado** (context) — melhor que três toggles órfãos.
+3. **Disclaimer admite o acoplamento** — honestidade > mentira silenciosa; o lugar da honestidade é o problema.
+
+## Priority Issues
+
+### [P1] Assento na toolbar do mapa implica choropleth quando Ano≠2026
+- **Why:** First paint comum: muda Cenário → mapa igual → sensação de broken.
+- **Fix:** Relocar ao overview; **ou** desabilitar/ghost + explicar; **ou** Cenário força Ano→2026; **ou** esconder no mapa quando ≠2026 *e* hospedar o controle onde o efeito é visível.
+- **Suggested command:** `/impeccable layout` (ou `/impeccable shape`)
+
+### [P1] Disclaimer em português de engenheiro
+- **Why:** “overview” não está na UI; “preenchimento” é jargão de mapa.
+- **Fix:** Uma linha de resultado (“Muda os totais acima da lista. No mapa, só com Ano 2026.”) — ou matar a linha relocando o controle.
+- **Suggested command:** `/impeccable clarify` / `/impeccable distill`
+
+### [P2] Feel the action: efeito fora do viewport
+- **Why:** Mapa acima; overview/lista abaixo. Em mobile parece no-op.
+- **Fix:** Highlight/pending no overview; ou co-localizar o controle.
+- **Suggested command:** `/impeccable animate` + layout
+
+### [P2] Always-show na fila do mapa vs hide
+- **Why:** Sync global é racional; *nesta* fila treina o affordance errado.
+- **Fix:** Não esconder sem relocação; se global, não vestir como filtro do mapa.
+- **Suggested command:** `/impeccable shape`
+
+### [P3] Densidade Ano / Escala / Cenário / Comparar
+- **Why:** Quarto peer + disclaimer wrap em narrow (Casey).
+- **Suggested command:** `/impeccable quieter`
+
+## Persona Red Flags
+
+**Alex:** Quer pessimista no footprint; mapa não muda → desconfiança.
+**Jordan:** “overview” / “preenchimento” opacos.
+**Casey:** Disclaimer engole vertical; efeito abaixo do mapa.
+**Sam:** Disclaimer sem `aria-describedby`; sem live region nos totais.
+
+## Minor Observations
+
+- Hide em compare alinhado a Escala — ok.
+- Readout 2026 já inclui cenário — forte quando Ano certo.
+- Ordem Map → filtros → overview inverte o controle global no mapa.
+- Polish anterior priorizou discoverability do sync às custas da honestidade do mapa.
+
+## Questions to Consider
+
+- Se o job é overview+lista, por que o controle vive na fila que pinta o mapa?
+- Alex prefere perder o disclaimer ou perder Cenário da toolbar?
+- Auto Ano→2026 é Feel the action ou sequestro da leitura histórica?
+- Se o disclaimer é o estado comum do first paint, a IA default não está errada?
diff --git a/AGENTS.md b/AGENTS.md
index e91a64d4..ca8ace16 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -59,6 +59,8 @@ Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap.
**Recently resolved (2026-07-23):** **Export CSV de assinaturas e contatos** no admin Payload — `@payloadcms/plugin-import-export@3.82.0` habilitado em `signature` e `contact` (`import: false`, export CSV síncrono); flatten de `contact`/`petition` via `toCSV` em [`src/utilities/signatureExport.ts`](src/utilities/signatureExport.ts) (só assinaturas); `consent` excluído do export de assinaturas; collections `exports`/`imports` com access `isPayloadAdmin`; migration `20260723_025513_add_import_export_plugin`. Plano: [`docs/plans/exportar-csv-assinaturas.md`](docs/plans/exportar-csv-assinaturas.md).
+**Recently resolved (2026-07-23):** **A10** Cenários de estimativa (pessimista/média/otimista) — `votePledge.estimatedVotes` e `plaza.expectedVotes` viram groups (`pessimistic|central|optimistic`); migration `20260723_124200_add_vote_estimate_scenarios` (backfill scalar→`central`); agregação por cenário em `votePledgeData` com default `central`; mapa 2026 com `values2026ByScenario` + seletor local; lista/overview/dashboard com média + faixa secundária; liderança continua com um `declaredVotes` sem ver estimativas. Plano: [`docs/plans/cenarios-estimativa-votos.md`](docs/plans/cenarios-estimativa-votos.md). Desbloqueia **E8**.
+
## Posts & Tags (news / publications)
The public news/publications system is backed by two collections (both in the `Publicações` admin group) plus the helpers in `src/utilities/posts.ts`.
@@ -118,7 +120,7 @@ The internal `/campanha` area is gated by its own authentication, deliberately k
- **Praça definition:** 436 seeded rows — one per Bahia municipality (415), except Salvador (19 zone Praças, ZE 1–19) and Camaçari (2 zone Praças, ZE 170/171). TSE zones shared across municipalities (141, 162) or leaking outside the RMS (128, 185, 200) are cut at the municipal boundary, so every Praça maps to exact `cityCode × zones` election cells. The canonical catalog is `src/lib/plazaCatalog.ts` (slug/name identity frozen by `tests/fixtures/plaza-catalog.snapshot.json`); the migration `20260721_020109_remodel_plazas` seeds it. Nobody creates/edits plaza geography in the app — identity fields are admin-only; staff edit only strategy fields (goals, priority, intel, political trend) and the coordinator assigns `advisors`.
- **Collections (admin group `Campanha`):** `plaza` (strategy + advisors + manual `politicalTrend`), `leadership` (ONE per person: `contact` UNIQUE + `plazas` hasMany + `organizations` hasMany + staff-only `supportStatus`), `votePledge` (leadership×plaza UNIQUE: `declaredVotes` leader-visible vs `estimatedVotes` staff-only), `organization` (sindicatos/associações/movimentos with reverse leadership + actionPlan views), `campaignDemand` (leader-opened workflow `aberta → em_analise → escalada → aprovada|rejeitada` with staff-only `cost`/`receipts`), `plazaUpdate` (immutable field reports maintaining `plaza.lastUpdateAt` under advisory lock), `campaignInvite` (unchanged: single-use SHA-256 hashes hanging off `leadership`), plus adapted `supporter` (optional `plaza`) and `actionPlan` (single `plaza`, `advisors`, `organizations`, `deputyPresent`, `resultSummary`/`resultMedia`).
- **Roles:** `coordinator` ("Coordenador Geral") sees/manages everything and is the only one who assigns advisors and decides escalated demands; `advisor` ("Assessor") sees and manages ONLY plazas listed in `plaza.advisors` (list, map, dashboard, leaderships, pledges, demands are all scoped); `leader` ("Liderança") sees only plazas linked through their engaged leadership, declares own votes, opens demands, and writes own updates.
-- **Vote pledge asymmetry (product-critical):** the leader INFORMS `declaredVotes` per linked plaza and may update it; staff record `estimatedVotes`/`estimateNote`, which field access denies to leaders (same technique as `supportStatus`). Leader-facing view models never aggregate estimates; staff aggregates use `estimatedVotes ?? declaredVotes` (`src/utilities/votePledgeData.ts`). Guarded by `tests/int/campaignVotePledge.int.spec.ts`.
+- **Vote pledge asymmetry (product-critical):** the leader INFORMS `declaredVotes` per linked plaza and may update it; staff record `estimatedVotes` (group: pessimistic/central/optimistic) and `estimateNote`, which field access denies to leaders (same technique as `supportStatus`). Leader-facing view models never aggregate estimates; staff aggregates use `estimated[S] ?? declared` per scenario, default read `central` (`src/utilities/votePledgeData.ts`). Guarded by `tests/int/campaignVotePledge.int.spec.ts`.
- **Political trend:** manual conjuncture reading on the plaza (staff-only group `politicalTrend`: `favoravel|neutra|desfavoravel` + note + derived author/date). The numeric series is deliberately labeled "Evolução de votos" in the UI to keep "tendência" for the political reading.
- **Elections intelligence:** plaza geography → election cells via `src/utilities/plazaElectionGeography.ts`; baseline + Solla series 2014/2018/2022 (`plazaElectoralBaseline.ts`); multi-candidate comparison table on the plaza detail (`plazaCandidateComparison.ts`, up to 5 candidates via `?compare=`); map (`plazaMapData.ts` + `PlazaMapPanel`) colors municipalities by year (2026 = pledge estimates) with scale selector **Total (votos)** | **% dos válidos** (default %; fixed 0–100% domain via `validVotesByYear` + `scaleMax` on `BahiaMap`; 2026 uses 2022 valid votes as denominator; compare mode hides % and stays absolute diff) and has a diverging compare mode (campaign red `#c51414` where Solla leads ↔ white tie ↔ blue where the compared candidate leads). Zone Praças aggregate into the municipality polygon with a per-zone breakdown list (zone polygons remain a rabbit hole).
- **URLs:** `/campanha/pracas[/slug]` (seeded slugs from the catalog), `/campanha/liderancas[/id]`, `/campanha/organizacoes[/slug]`, `/campanha/demandas[/slug]`, `/campanha/planos`, `/campanha/apoiadores`. Route params stay English keys; slugs/enums stay pt data values.
diff --git a/PRODUCT.md b/PRODUCT.md
index bdd5055a..87cc0c0c 100644
--- a/PRODUCT.md
+++ b/PRODUCT.md
@@ -43,10 +43,11 @@ Not generic SaaS dashboards (purple gradients, hero-metric templates, identical
1. **Sovereignty as the horizon** — own audience, data, and ops over time; Big Tech shortcuts (WhatsApp kits, share links) are fine for now when they help mobilization, but design should keep the campaign’s own records and workflows as the source of truth and leave room to reduce platform dependency later.
2. **Clarity under pressure** — every screen earns its keep for field use; prefer plain language and obvious next actions over decoration.
3. **Edit where you see** — on `/campanha` staff surfaces (coordinator / advisor), mutable data that the role can update should be editable in context whenever it fits (list cell, detail card, queue row), so assessors and the general coordinator do not burn time navigating to a separate edit screen for every correction. Prefer named Popovers / selects / short inputs that reuse existing server actions and the same access rules as the full form. Keep dedicated `/editar` (or long Sheets) for multi-field strategy forms, rare fields, and anything that needs a note or confirmation flow. Leadership paths stay read-or-declare as already scoped — never expose staff-only fields. Anti-goals: full-row spreadsheet / data-grid mode; always-mounted inputs on every row; a second generic “editable cell” design system; optimistic _writes of list/aggregate results_ that skip server refresh when the list is the source of truth (optimistic control state is fine — see Feel the action).
-4. **Depth and simplicity as peers** — staff workflows can be rich; leadership paths stay light and phone-first without becoming a second-class product.
-5. **Intelligence serves organization** — electoral baseline and insights exist to decide where to act, not to impress. The product delivers intelligence, not a fancy spreadsheet: it computes the bulk of the analysis (vigilance across 436 Praças, structural differential diagnosis, a prioritized decision queue with suggested action menus) so staff spend judgment, not arithmetic. Decision metrics are **relative and local** — coverage of per-Praça goals by auditable pledges, capture of the campo's majoritarian ceiling, quantiles/LQ of the candidate's own distribution — never absolute statewide % (a DF seat costs ~80–150k votes; the statewide record is ~5% of valid votes, so absolute scales flatten into noise). Suggestions are grounded in `docs/research/` (approved discovery, 25 data→decision patterns) and are always a menu for a human: the product never auto-decides, and each decision — including the discarded alternative reading — is recorded ex-ante for the post-election backtest. Anti-goals: raw-count KPIs, 0–100% fixed choropleth scales, "available votes" reservoirs from abstention/null tallies, suggestion-as-rule.
-6. **Inspire without kitsch** — socialist and mobilizing in spirit; never shouty campaign brochure aesthetics.
-7. **Feel the action** — every user-initiated action (filter, save, URL-driven refresh, opening a heavy sheet) must give **immediate** feedback on the control or surface that was touched — under ~100ms perceived response. Prefer optimistic UI for the _control value_ (select shows the chosen option at once; button shows pending) while the _result_ (list, map, KPIs, detail payload) shows honest pending (`aria-busy`, live region, opacity/skeleton on the region that changes) until the server/RSC catches up. Applies to `/campanha` first (field pressure) and to public interactive flows the same way. Anti-goals: silent waits where the control stays on the old value until navigation finishes; full-page spinner as the only feedback; fake progress bars; omitting accessible busy/live announcements.
+4. **Auto-save, no Save button** — editable fields should persist automatically whenever it is safe (on select change, blur, or short debounce — pick what fits the control). Do not require an explicit “Salvar” / Save button for single-field or in-context edits. Pair with Feel the action: optimistic control state + honest pending / saved / error feedback on the field itself. Keep an explicit submit only when the write must be atomic across several fields, needs a note/confirmation, or is a legal consent / irreversible flow. Anti-goals: silent auto-save with no status; saving on every keystroke for free-text without debounce; Save buttons on Popovers and quick-edit cells “just in case.”
+5. **Depth and simplicity as peers** — staff workflows can be rich; leadership paths stay light and phone-first without becoming a second-class product.
+6. **Intelligence serves organization** — electoral baseline and insights exist to decide where to act, not to impress. The product delivers intelligence, not a fancy spreadsheet: it computes the bulk of the analysis (vigilance across 436 Praças, structural differential diagnosis, a prioritized decision queue with suggested action menus) so staff spend judgment, not arithmetic. Decision metrics are **relative and local** — coverage of per-Praça goals by auditable pledges, capture of the campo's majoritarian ceiling, quantiles/LQ of the candidate's own distribution — never absolute statewide % (a DF seat costs ~80–150k votes; the statewide record is ~5% of valid votes, so absolute scales flatten into noise). Suggestions are grounded in `docs/research/` (approved discovery, 25 data→decision patterns) and are always a menu for a human: the product never auto-decides, and each decision — including the discarded alternative reading — is recorded ex-ante for the post-election backtest. Anti-goals: raw-count KPIs, 0–100% fixed choropleth scales, "available votes" reservoirs from abstention/null tallies, suggestion-as-rule.
+7. **Inspire without kitsch** — socialist and mobilizing in spirit; never shouty campaign brochure aesthetics.
+8. **Feel the action** — every user-initiated action (filter, save, URL-driven refresh, opening a heavy sheet) must give **immediate** feedback on the control or surface that was touched — under ~100ms perceived response. Prefer optimistic UI for the _control value_ (select shows the chosen option at once; button shows pending) while the _result_ (list, map, KPIs, detail payload) shows honest pending (`aria-busy`, live region, opacity/skeleton on the region that changes) until the server/RSC catches up. Applies to `/campanha` first (field pressure) and to public interactive flows the same way. Anti-goals: silent waits where the control stays on the old value until navigation finishes; full-page spinner as the only feedback; fake progress bars; omitting accessible busy/live announcements.
## Accessibility & Inclusion
diff --git a/docs/plans/cenarios-estimativa-votos.md b/docs/plans/cenarios-estimativa-votos.md
index 69c86342..8a63dca5 100644
--- a/docs/plans/cenarios-estimativa-votos.md
+++ b/docs/plans/cenarios-estimativa-votos.md
@@ -1,7 +1,7 @@
# Cenários de estimativa de votos (pessimista / média / otimista)
-Status: rascunho
-Atualizado em: 2026-07-23
+Status: entregue (2026-07-23)
+Atualizado em: 2026-07-23 _(entrega A10 + polish overview/Cenário; capture-review-debts: no-op roadmap, gatilho flash)_
Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha A, item A10)
Impeccable: B — encaixe em forms/lista/mapa/dashboard já existentes (sem rota nova)
Appetite: ~2 dias eng; migration (pledge + praça) + helpers de agregação + seletor de cenário nas superfícies staff
@@ -56,15 +56,15 @@ E8 (conta da cadeira) ainda não começou e planeja Σ `estimatedVotes ?? declar
- **Backfill na migration: escalar antigo → `central`; pessimista/otimista null.** Preserva o significado do número único já lançado. **Rejeitado:** copiar o mesmo valor nos três (inventa certeza); zerar tudo (apaga trabalho de campo).
- **Ordem quando os três estão preenchidos: pessimista ≤ central ≤ otimista** (mesmo espírito de `voteGoals` / `voteGoals.ts`). **Rejeitado:** sem invariante (quebra leitura da faixa); forçar os três sempre preenchidos na v1 (fria adoção).
- **Fallback por cenário:** `effective[S] = estimated[S] ?? declared`; total Praça `resolve[S] = expected[S] ?? Σ effective[S]`. Cenário ausente no pledge cai no declarado **daquele** pledge; Praça sem `expected[S]` usa só a soma daquele cenário. **Rejeitado:** “se falta pessimista, usar média” (mistura cenários); um único fallback compartilhado entre S.
-- **Default de leitura = `central` (média); seletor de cenário no mapa e na lista/overview** (URL ou estado local no padrão Ano/Escala). **Rejeitado:** default otimista (infla a conta); três mapas lado a lado; só tooltip com faixa sem trocar o fill.
+- **Default de leitura = `central` (média); seletor de cenário no mapa (e lista/overview se couber) em estado local**, no mesmo padrão de Ano/Escala do `PlazaMapPanel`. **Rejeitado:** default otimista (infla a conta); três mapas lado a lado; só tooltip com faixa sem trocar o fill; `?estimate=` na v1 (Ano/Escala não usam URL — só `compare` usa).
- **Access:** inalterado — `canRead/ManageCampaignStaffField` nos grupos; leader redacted. **Rejeitado:** só coordinator escreve otimista.
- **i18n e naming:** `pessimistic` / `central` / `optimistic`, `VoteEstimateScenario`, `resolvePlazaStaffVoteTotalForScenario`, `aggregatePledgesByPlaza` passa a expor totais por cenário; strings “Pessimista” / “Média” / “Otimista”.
## Questões em aberto
-- **Seletor de cenário na URL (`?estimate=central`) ou só estado local no cliente?** **Opções:** A) query param (shareable, SSR) | B) estado local | C) cookie. **Recomendação:** A no mapa/lista (espelha `year`/`scale` e sobrevive a refresh RSC); default omitido = central. _(assumido — validar com produto)_
-- **Partial fill: salvar só média?** **Opções:** A) sim, nullable independente | B) exigir os três. **Recomendação:** A — ordem só quando ≥2 preenchidos e comparáveis; UI sugere preencher a faixa.
-- **KPI do dashboard mostra um número ou a faixa?** **Opções:** A) só média + link | B) `pessimista–otimista` tipográfico | C) três KPIs. **Recomendação:** B (número médio + faixa secundária); C estoura density.
+- **Seletor de cenário na URL (`?estimate=central`) ou só estado local no cliente?** **Opções:** A) query param (shareable, SSR) | B) estado local | C) cookie. **Recomendação:** **B** no mapa (e lista/overview se houver seletor) — `PlazaMapPanel` já guarda **Ano** e **Escala** em `useState` local; só `compare` vai na URL. Espelhar esse padrão evita RSC round-trip e inconsistência com os outros seletores. Default = `central` sem persistência. _(assumido — validar com produto; A só se share de cenário virar pedido explícito)_ **Rejeitado nesta revisão:** A “porque espelha year/scale” — afirmação defasada (year/scale não são URL).
+- **Partial fill: salvar só média?** **Opções:** A) sim, nullable independente | B) exigir os três. **Recomendação:** A — ordem só quando ≥2 preenchidos e comparáveis; UI sugere preencher a faixa. _(fechado)_
+- **KPI do dashboard mostra um número ou a faixa?** **Opções:** A) só média + link | B) `pessimista–otimista` tipográfico | C) três KPIs. **Recomendação:** B (número médio + faixa secundária); C estoura density. _(fechado)_
## Abordagem proposta
@@ -76,7 +76,7 @@ flowchart LR
plaza --> resolve["resolvePlazaStaffVoteTotalForScenario"]
agg --> resolve
resolve --> list["PlazaList + overview"]
- resolve --> map["PlazaMapPanel + ?estimate="]
+ resolve --> map["PlazaMapPanel + estado local"]
resolve --> dash["dashboard / E8"]
forms["PledgeEstimateForm + Plaza*Votes"] --> pledge
forms --> plaza
@@ -85,13 +85,13 @@ flowchart LR
Componentes:
-- **`VotePledge` / `Plaza`** (`src/collections/VotePledge.ts`, `Plaza.ts`): substituir scalars por groups com três `number` min 0; access staff nos groups; hook de audit `estimatedAt`/`estimatedBy` quando qualquer subcampo muda; validação de ordem (reusar padrão de `voteGoals.ts` → `voteEstimate.ts`).
-- **Migration** `pnpm migrate:create add_vote_estimate_scenarios`: criar colunas do group; `UPDATE … SET central = estimated_votes` (e idem `expected_votes`); dropar scalars; sem Consent.
-- **`src/lib/schemas/votePledge.ts` / `plaza.ts`:** schemas Zod dos três; action `setVotePledgeEstimate` / `setPlazaExpectedVotes` passam a aceitar o trio (nullable).
-- **`src/utilities/votePledgeData.ts`:** `PlazaPledgeAggregate` ganha `effectiveByScenario` + `declaredTotal`; `resolvePlazaStaffVoteTotal` vira variante por cenário; `rollupPlazaStaffVotes` default `central`. Depth: **não** criar `VoteEstimateService` — estender o módulo profundo existente.
-- **`plazaMapData.ts` + `PlazaMapPanel`:** bundle 2026 indexado por cenário (ou recompute client-side a partir de três maps); seletor “Cenário” ao lado de Ano/Escala; readout mostra valor do cenário + faixa se completa.
-- **UI:** `PledgeEstimateForm` três inputs; `PlazaStrategyForm` / `PlazaListExpectedVotesControl` trio (Popover pode empilhar 3 campos); `StaffPlazaVotesDisplay` / `PlazaPledgesPanel` / `PlazaList` leem cenário ativo + faixa; `DeclareVotesForm` intacto.
-- **Testes:** int assimetria (leader não vê nenhum dos três); unit ordem + fallback por cenário; int migration backfill → central; mapa/lista usam default central.
+- **`VotePledge` / `Plaza`** (`src/collections/VotePledge.ts`, `Plaza.ts`): substituir scalars por groups com três `number` min 0; access staff nos groups; hook de audit `estimatedAt`/`estimatedBy` quando qualquer subcampo muda; validação de ordem (reusar padrão de `src/utilities/voteGoals.ts` → helper fino `voteEstimate.ts` em `src/utilities/` ou `src/lib/` — só se ≥2 call sites).
+- **Migration** `pnpm migrate:create add_vote_estimate_scenarios`: criar colunas do group (`estimated_votes_pessimistic|central|optimistic`, idem `expected_votes_*` no padrão de `vote_goals_*`); `UPDATE … SET …_central = estimated_votes` (e idem `expected_votes`); dropar scalars + índice antigo em `estimated_votes`; sem Consent.
+- **`src/lib/schemas/votePledge.ts` / `plaza.ts`:** schemas Zod dos três; actions existentes `estimateVotes` / `setPlazaExpectedVotes` (+ form actions em `plazaStaffFormActions` / `pledgeFormActions`) passam a aceitar o trio (nullable).
+- **`src/utilities/votePledgeData.ts`:** `PlazaPledgeAggregate` ganha `effectiveByScenario` + `declaredTotal`; `resolvePlazaStaffVoteTotal` / `rollupPlazaStaffVotes` com cenário (default `central`). Depth: **não** criar `VoteEstimateService` — estender o módulo profundo existente.
+- **`plazaMapData.ts` + `PlazaMapPanel`:** bundle 2026 com valores por cenário (três maps no bundle — leitura local no seletor); seletor “Cenário” ao lado de Ano/Escala (**estado local**, como Ano/Escala); readout mostra valor do cenário + faixa se completa.
+- **UI:** `PledgeEstimateForm` três inputs; `PlazaStrategyForm` / `PlazaListExpectedVotesControl` trio (Popover pode empilhar 3 campos); `StaffPlazaVotesDisplay` / `PlazaPledgesPanel` / `PlazaList` leem cenário ativo (default central) + faixa; `DeclareVotesForm` intacto.
+- **Testes:** int assimetria (leader não vê nenhum dos três) em `campaignVotePledge.int.spec.ts`; unit ordem + fallback por cenário; int/unit backfill semântico → central; mapa/lista usam default central.
- **Depth check:** reusar `plazaStaffFormActions`, `campaignAccess`, shells B9; espelhar invariante de `voteGoals.ts` em helper fino compartilhado só se ≥2 call sites.
## Dependências
@@ -108,6 +108,18 @@ Componentes:
- Alterar `voteGoals` ou fundir labels; previsão estatística; import CSV de faixas.
- Expor estimativas à liderança; auto-preencher faixa a partir do declarado.
+## Já resolvido no simplify/critique (não reabrir)
+
+- Overview: label por cenário ativo (`Média nas Praças filtradas`), endpoints `Pessimista · Otimista`, `VoteEstimateScenarioStrip`, `sem estimativa` sob o hero; sync via `PlazaEstimateScenarioProvider`.
+- Cenário: seletor no overview; no mapa só com Ano=2026; disclaimer inline → `CampaignInfoHint` (`?` Popover); flash + `aria-live` no overview ao trocar.
+- Cleanup `/simplify`: dropar `staffVoteRange` / `staffVoteTotal` mortos nos DTOs overview/dashboard; `formatElectionNumber`; context sem `useCallback` extra.
+
+## Explicitamente fora (capture-review-debts 2026-07-23)
+
+- Simplify amplo do restante A10 (forms/pledges/migration) sem dor concreta — sem `escala-dry-pos-a10`.
+- Trocar `CampaignInfoHint` por `FieldDescription` — affordance errado (hint dismissível vs ajuda estática).
+- Remover `rollup.staffVoteTotal` — ainda usado por testes/API do rollup.
+
## Rabbit holes
- **Fundir com Bom/Regular/Mínimo.** Se alguém “aproveitar o group que já existe”: apaga meta×estimativa. **Mitigação:** labels e identificadores distintos; teste que `voteGoals` não é lido pelo resolver de estimativa.
@@ -120,16 +132,19 @@ Componentes:
- **Nota por cenário** (hoje uma `estimateNote`). Revisitar se assessores pedirem justificar só o pessimista vs. o otimista.
- **Aviso soft vs `voteGoals.minimum` no cenário pessimista.** Revisitar após E8 com totais preenchidos (mesmo gatilho de A9).
- **Delta semanal por cenário.** Revisitar com C12 (versions no group).
+- **Abstração compartilhada de flash/pending client-side** (hoje local em `PlazaListOverview`). Revisitar quando houver o **2º** flash client-side em `/campanha` (não URL/`useTransition`).
## Referências
+- Critiques: [overview](../../.impeccable/critique/2026-07-23T21-29-20Z__src-components-campaign-plazalistoverview-tsx.md), [Cenário](../../.impeccable/critique/2026-07-23T21-52-39Z__src-components-campaign-plazamappanel-tsx-cenario.md)
- `docs/roadmap.md` (Trilha A / A10 → E8)
- `docs/plans/estimativa-votos-praca.md` — A9 (scalar a evoluir)
- `docs/plans/remodelagem-pracas.md` — assimetria declared×estimated
- `docs/plans/conta-da-cadeira.md` — consumidor E8 (atualizar dep)
- `src/collections/VotePledge.ts`, `src/collections/Plaza.ts`
-- `src/utilities/votePledgeData.ts`, `src/utilities/plazaMapData.ts`, `src/lib/voteGoals.ts`
+- `src/utilities/votePledgeData.ts`, `src/utilities/plazaMapData.ts`, `src/utilities/voteGoals.ts`
- `src/components/campaign/PledgeEstimateForm.tsx`, `PlazaListExpectedVotesControl.tsx`, `PlazaMapPanel.tsx`, `DeclareVotesForm.tsx`
- AGENTS.md — staff-only estimates, naming, migrations, overrideAccess
- `PRODUCT.md` / `DESIGN.md` — Field Desk; princípio 5 (métricas locais)
-- Feedback coordenação geral 2026-07-23 (áudio em `docs/general-coordinator-interview/`)
+- Feedback coordenação geral 2026-07-23 (áudio em `docs/general-coordinator-interview/` / `CUSTOMER.md` Interview Snapshot)
+- Revisão auditoria 2026-07-23: path `voteGoals` corrigido; seletor de cenário → estado local (Ano/Escala não são URL); actions reais = `estimateVotes` / `setPlazaExpectedVotes`.
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 6c47303c..508f69ad 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,6 +1,6 @@
# Roadmap — Teqo
-Atualizado em: 2026-07-23 (D3–D5: canal WhatsApp **pessoal** por staff + QR — [plano-mestre](plans/whatsapp-interno-campanha.md); A10 cenários — [plano](plans/cenarios-estimativa-votos.md); export CSV admin entregue)
+Atualizado em: 2026-07-23 (A10 cenários entregue — [plano](plans/cenarios-estimativa-votos.md); D3–D5 WhatsApp; export CSV admin entregue)
Registro canônico dos **próximos** planos e débitos. Histórico de entregas: resumo abaixo + planos em [`docs/plans/`](plans/) + notebook [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc).
@@ -53,7 +53,7 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re
| ID | Fatia | Plano | Entrega essencial | Classe | Appetite | Janela | Depende de |
| --- | ---------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -------- | --------- | ---------------------------------------------- |
-| E8 | Conta da cadeira | [detalhes](plans/conta-da-cadeira.md) | Global `campaignGoals`; potencial por praça (válidos projetados, captura do campo, roll-off, headroom); decomposição meta→praça; cobertura Σpledges÷meta (cenário default = média) | B | ~2d | 2 | **A10**, deploy remodelagem |
+| E8 | Conta da cadeira | [detalhes](plans/conta-da-cadeira.md) | Global `campaignGoals`; potencial por praça (válidos projetados, captura do campo, roll-off, headroom); decomposição meta→praça; cobertura Σpledges÷meta (cenário default = média) | B | ~2d | 2 | deploy remodelagem |
| E9 | Fila de alocação | [detalhes](plans/fila-de-alocacao.md) | Lista de decisão (7 colunas), ordenação por déficit descoberto + risco por frescor, "coluna da vergonha" | B | ~1,5d | 2 | E8 |
| C12 | Registro-fundação | [detalhes](plans/registro-fundacao.md) | Versions em `votePledge`; sinais tipados no `plazaUpdate`; `actionPlan.origin`+custo; collection `allocationDecision` | B | ~2d | 2 | deploy remodelagem (paralelo a E8; suave: A10) |
| E10 | Classificação territorial relativa | [detalhes](plans/classificacao-territorial-relativa.md) | Âncoras relativas (LQ / share da cadeira marginal), multi-eixo; substitui 35/20/10 para DF | B | ~1d | 3 | E8 |
@@ -88,7 +88,7 @@ flowchart TD
R2 --> A9["A9 Estimativa votos ✓"]
A9 --> A9plus["A9+ Loader lista ✓"]
A9plus --> B9["B9 Edição rápida lista ✓"]
- A9 --> A10["A10 Cenários estimativa pessimista/média/otimista"]
+ A9 --> A10["A10 Cenários estimativa ✓"]
R2 --> A10
R2 -.-> B7["B7 Mapa filtrado ✓"]
B7 --> B12["B12 Aproximar mapa ao filtro ✓"]
@@ -142,13 +142,13 @@ flowchart TD
E11i -.decisões acumuladas.-> E15i
```
-Paralelizáveis agora: **A10** (pós-A9; paraleliza com C12 / smoke pós-deploy), fill-ins (O0+, RS+, …). **D3** (baixa/média prioridade; após remodelagem em prod + folga — não bloqueia E8/E9). ~~**A9** / **A9+** / **B9** / **B7** / **B10** / **B11** / **B6** / **B12** / **filtros-auto** / **C8** F4~~ entregues 2026-07-21.
+Paralelizáveis agora: **E8** (pós-A10), fill-ins (O0+, RS+, …). **D3** (baixa/média prioridade; após remodelagem em prod + folga — não bloqueia E8/E9). ~~**A9** / **A9+** / **A10** / **B9** / **B7** / **B10** / **B11** / **B6** / **B12** / **filtros-auto** / **C8** F4~~ entregues 2026-07-21–23.
### Sequência por janela
-**Janela 1 — agora → 05/08 (convenções):** ~~R0 → R1 → R2~~ entregues; ~~**A9** estimativa de votos~~ / ~~**B9** edição rápida na lista~~ / ~~**B7** mapa filtrado~~ / ~~**B10** hover+click-nav~~ / ~~**B11** escala % dos válidos~~ / ~~**B12** aproximar mapa ao footprint filtrado~~ / ~~**filtros-auto** lista de Praças~~ entregues; **deploy da remodelagem** (revisar SQL destrutivo da migração antes do build) + smoke em produção; **A10** cenários pessimista/média/otimista (pode puxar para cá pós-deploy — desbloqueia E8); R6 critique/polish; Onda 0 jurídica em paralelo.
+**Janela 1 — agora → 05/08 (convenções):** ~~R0 → R1 → R2~~ entregues; ~~**A9** estimativa de votos~~ / ~~**A10** cenários pessimista/média/otimista~~ / ~~**B9** edição rápida na lista~~ / ~~**B7** mapa filtrado~~ / ~~**B10** hover+click-nav~~ / ~~**B11** escala % dos válidos~~ / ~~**B12** aproximar mapa ao footprint filtrado~~ / ~~**filtros-auto** lista de Praças~~ entregues; **deploy da remodelagem** (revisar SQL destrutivo da migração antes do build) + smoke em produção; R6 critique/polish; Onda 0 jurídica em paralelo.
-**Janela 2 — 05/08 → 16/08 (pré-propaganda):** C2 dados reais assim que o jurídico liberar; **A10** (se não fechou na Janela 1) → **E8** conta da cadeira → **E9** fila de alocação, com **C12** registro-fundação em paralelo (migrations cedo, longe do congelamento); D2 se sobrar folga.
+**Janela 2 — 05/08 → 16/08 (pré-propaganda):** C2 dados reais assim que o jurídico liberar; **E8** conta da cadeira → **E9** fila de alocação, com **C12** registro-fundação em paralelo (migrations cedo, longe do congelamento); D2 se sobrar folga.
**Janela 3 — 16/08 → set:** A6 dobradinha (pós-TSE 15/08), **E10** classificação relativa → **B13** escala relativa no mapa, **E14** níveis N0–N4, **E11** motor de sugestões v1, **E12** camada TI, **E13** planejador de giros; **B8** polígonos das Praças-zona Salvador/Camaçari (F1 catálogo de bairros shipável antes; F2 dissolve), débitos sobreviventes (abaixo). Se houver folga **e** Little Hire ainda vazar para o ZAP: começar **D3** (fundação do canal) — não compete com E8/E9 na Janela 2.
@@ -156,7 +156,7 @@ Paralelizáveis agora: **A10** (pós-A9; paraleliza com C12 / smoke pós-deploy)
### Cortes seguros / não cortáveis
-**Não cortáveis:** Onda 0 (jurídico/Consent); R1–R2 (sem eles a vertical não reflete a operação real); C2 dados reais; assimetria declarado×estimado (relação de campo); ~~**A9**~~ (total esperado da Praça — entregue 2026-07-21); **A10** (faixa pessimista/média/otimista — semântica operacional pedida pela coordenação; E8 consome); **E8**+**E9**+**C12** (a conta da cadeira, a fila e o registro ex-ante são o mínimo de "inteligência, não planilha" — e C12 é irrecuperável se não registrar durante a campanha).
+**Não cortáveis:** Onda 0 (jurídico/Consent); R1–R2 (sem eles a vertical não reflete a operação real); C2 dados reais; assimetria declarado×estimado (relação de campo); ~~**A9**~~ (total esperado da Praça — entregue 2026-07-21); ~~**A10**~~ (faixa pessimista/média/otimista — entregue 2026-07-23); **E8**+**E9**+**C12** (a conta da cadeira, a fila e o registro ex-ante são o mínimo de "inteligência, não planilha" — e C12 é irrecuperável se não registrar durante a campanha).
**Cortes seguros** (se o prazo apertar, nesta ordem): **D5** inbox→rascunhos (manter registro manual + `wa.me`); **D4** envio bridge (manter `wa.me`); **D3** fundação do canal (atalho `wa.me` continua); **E13** planejador de giros (agenda segue manual com J-A/J-B como guia); **E12** camada TI (rollup manual por lista); **E15** (pós-eleição por definição — cortar = perder o aprendizado 2030); **E11** motor v1 (manter fila E9 sem sugestões); **B13** símbolo proporcional (manter quantis/LQ como escala); **E14** (manter `priority` alta/normal); R4 mapa comparativo (manter tabela comparativa); painel de detalhe por zona no mapa; R3 organizações (manter demandas); resultado de plano com mídia (manter texto); Eleitorado/IBGE na Praça; D2 push (manter sino); A6; **B8** (F2 polígonos; manter F1 bairros na Praça se já entregue — mapa continua agregado no município); débitos/fill-ins. ~~**B9** / **B10** / **B11** / **B6** / **B12**~~ / ~~**export CSV de assinaturas/contatos**~~ (entregues — não cortar).
@@ -196,8 +196,7 @@ Paralelizáveis agora: **A10** (pós-A9; paraleliza com C12 / smoke pós-deploy)
## Próximos — sobreviventes e pós-remodelagem
-- **A10** Cenários de estimativa (pessimista / média / otimista) em pledge + total da Praça + lista/mapa · liderança declara um · desbloqueia E8 · Janela 1–2 · não cortável · [plano](plans/cenarios-estimativa-votos.md)
-- **Programa Inteligência de campanha** (E8–E15, B13, C12) · fatias, gaps de dados e fila canônica no [plano-mestre](plans/inteligencia-campanha.md) · embasamento: [docs/research/](research/) (relatório aprovado 2026-07-21) · **E8 depende de A10**
+- **Programa Inteligência de campanha** (E8–E15, B13, C12) · fatias, gaps de dados e fila canônica no [plano-mestre](plans/inteligencia-campanha.md) · embasamento: [docs/research/](research/) (relatório aprovado 2026-07-21) · **E8** desbloqueado por A10 ✓
- **A6** dobradinha 2026 automática quando o TSE publicar candidaturas · gatilho externo: pós-15/08 · reenquadrar para Praça · alimenta **E13** · [plano](plans/insight-dobradinha-2026.md)
- **B5 F2–F3** cache CLI compartilhado + factory mun/TI (scripts continuam) · [plano](plans/escala-dry-pos-b2.md)
- **B8** Polígonos das Praças-zona (Salvador ZE 1–19 / Camaçari ZE 170–171): ~~F1 catálogo zona→bairros + UI na Praça~~ (entregue 2026-07-21); F2 prep catálogo (~½ dia) + dissolve IBGE/malha → TopoJSON no mapa · Janela 3 · cortável (F2) · [plano](plans/poligonos-pracas-zona.md)
diff --git a/src/app/(campaign)/campanha/(app)/pracas/[slug]/page.tsx b/src/app/(campaign)/campanha/(app)/pracas/[slug]/page.tsx
index 70b928b1..f99e5c9f 100644
--- a/src/app/(campaign)/campanha/(app)/pracas/[slug]/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/pracas/[slug]/page.tsx
@@ -12,10 +12,10 @@ import { PlazaCandidateComparisonTable } from '@/components/campaign/PlazaCandid
import { PlazaLeadershipsPanel } from '@/components/campaign/PlazaLeadershipsPanel'
import { PlazaPledgesPanel } from '@/components/campaign/PlazaPledgesPanel'
import { PlazaStrategyCard } from '@/components/campaign/PlazaStrategyCard'
-import { PlazaZoneNeighborhoodsCard } from '@/components/campaign/PlazaZoneNeighborhoodsCard'
import { PlazaTabNav } from '@/components/campaign/PlazaTabNav'
import { PlazaUpdateFeed } from '@/components/campaign/PlazaUpdateFeed'
import { PlazaUpdateForm } from '@/components/campaign/PlazaUpdateForm'
+import { PlazaZoneNeighborhoodsCard } from '@/components/campaign/PlazaZoneNeighborhoodsCard'
import { RecentVisitTracker } from '@/components/campaign/RecentVisitTracker'
import { Badge } from '@/components/ui/Badge'
import { isCampaignStaff } from '@/utilities/campaignAccess'
@@ -37,7 +37,12 @@ import {
import { formatPlazaGeographyLabel, plazaKindLabels } from '@/utilities/plazaUi'
import { loadPlazaUpdatesFeed, parsePlazaUpdateFeedParams } from '@/utilities/plazaUpdatePageData'
import { loadAdvisorSummaries } from '@/utilities/plazaViewModels'
-import { loadLeaderPledges, loadPlazaPledges, sumStaffPledgeEffectiveTotal } from '@/utilities/votePledgeData'
+import {
+ aggregatePlazaPledgesFromRows,
+ loadLeaderPledges,
+ loadPlazaPledges,
+ toPlazaPledgeCoverageView,
+} from '@/utilities/votePledgeData'
import { declareVotesFormAction, estimateVotesFormAction } from './pledgeFormActions'
import { createPlazaUpdateFormAction } from './updateFormActions'
@@ -159,7 +164,7 @@ const OverviewTab = async ({
if (isStaffView && view.strategy) {
const pledges = await loadPlazaPledges(payload, user, view.id)
- const leadershipVoteTotal = sumStaffPledgeEffectiveTotal(pledges)
+ const pledgeCoverage = toPlazaPledgeCoverageView(aggregatePlazaPledgesFromRows(pledges))
return (