diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index f4924d72..604c8049 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -7,6 +7,8 @@ alwaysApply: false ## Status +- **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**. @@ -66,6 +68,8 @@ alwaysApply: false ## Decisões +- 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). diff --git a/.gitignore b/.gitignore index fd20fefb..127b9a96 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,9 @@ next-env.d.ts # IBGE demographics download cache (pnpm build:demographics) /data/demographics/ +# Transcription intermediates (source audio lives under docs/) +output/**/*.wav + # Playwright node_modules/ /test-results/ diff --git a/docs/IMPROVE-APP-PLAN.md b/docs/IMPROVE-APP-PLAN.md index f675b3bb..f487713b 100644 --- a/docs/IMPROVE-APP-PLAN.md +++ b/docs/IMPROVE-APP-PLAN.md @@ -16,40 +16,44 @@ ## Phase Status -| Phase | Skill | Status | Artifact | Date | -| ----- | ------------------------ | --------------------------------------------------------------------------------------------------------- | ------------------------------------- | ---------- | -| 1 | jobs-to-be-done | done | CUSTOMER.md | 2026-07-19 | -| 2 | ux-heuristics | deferred: após primeiros testes de quarta (~2026-07-22) — auditoria expert sem usuários seria especulação | DESIGN.md, EXPERIMENTS.md | 2026-07-19 | -| 3 | design-everyday-things | pending | DESIGN.md, EXPERIMENTS.md | | -| 4 | refactoring-ui | pending | DESIGN.md, EXPERIMENTS.md | | -| 5 | microinteractions | pending | DESIGN.md, EXPERIMENTS.md | | -| 6 | made-to-stick | pending | POSITIONING.md, EXPERIMENTS.md | | -| 7 | influence-psychology | skipped: sem paywall/upgrade/trial in-app | — | 2026-07-19 | -| 8 | high-perf-browser | pending | DESIGN.md, EXPERIMENTS.md | | -| 9 | steve-jobs-design-review | pending | PRODUCT.md, DESIGN.md, EXPERIMENTS.md | | -| opt | continuous-discovery | done (cadência + roteiro; evidência chega ~2026-07-22) | CUSTOMER.md | 2026-07-19 | -| opt | improve-retention | deferred: após Fase 1 + primeiros testes — risco é Little Hire | PRODUCT.md | | +| Phase | Skill | Status | Artifact | Date | +| ----- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------- | ---------- | +| 1 | jobs-to-be-done | done | CUSTOMER.md | 2026-07-19 | +| 2 | ux-heuristics | pending — destravada 2026-07-23 (evidência da sessão âncora em CUSTOMER.md; Bloco B de uso observado ainda sem registro) | DESIGN.md, EXPERIMENTS.md | 2026-07-23 | +| 3 | design-everyday-things | pending | DESIGN.md, EXPERIMENTS.md | | +| 4 | refactoring-ui | pending | DESIGN.md, EXPERIMENTS.md | | +| 5 | microinteractions | pending | DESIGN.md, EXPERIMENTS.md | | +| 6 | made-to-stick | pending | POSITIONING.md, EXPERIMENTS.md | | +| 7 | influence-psychology | skipped: sem paywall/upgrade/trial in-app | — | 2026-07-19 | +| 8 | high-perf-browser | pending | DESIGN.md, EXPERIMENTS.md | | +| 9 | steve-jobs-design-review | pending | PRODUCT.md, DESIGN.md, EXPERIMENTS.md | | +| opt | continuous-discovery | done (cadência + roteiro; evidência chega ~2026-07-22) | CUSTOMER.md | 2026-07-19 | +| opt | improve-retention | deferred: após Fase 1 + primeiros testes — risco é Little Hire | PRODUCT.md | | Statuses: pending · in-progress · awaiting-evidence · done · deferred: \ · skipped: \ ## Key Decisions -| Date | Phase | Decision | Rationale | -| ---------- | ------ | ------------------------------------------- | ------------------------------------------------------------------------------------------- | -| 2026-07-19 | Intake | Escopo = `/campanha` (não site público) | Job e atrito são de operação de campanha | -| 2026-07-19 | Intake | Fase 7 skipped | Sem superfícies de upsell in-app | -| 2026-07-19 | Intake | continuous-discovery opcional recomendada | Evidência = nenhuma; testes humanos esta semana | -| 2026-07-19 | Intake | improve-retention deferred pós-Fase 1 | Sintoma clássico de abandono por dados defasados; método completo depois do job + evidência | -| 2026-07-19 | Intake | Plano de fases confirmado | Inclui `docs/roadmap.md` como registro vivo de ideias/pendências | -| 2026-07-19 | Intake | Roadmap = backlog de build | Findings deste journey → EXPERIMENTS + itens/planos no roadmap quando forem para build | -| 2026-07-19 | 1 | Job = campanha dep. federal pela Bahia | Abrangência estadual; cargo federal | -| 2026-07-19 | 1 | Pior dimensão = emocional | Confiança no quadro atualizado; risco de ansiedade com dados mortos | -| 2026-07-19 | 1 | Leak = Little Hire | Big Hire coberto por onboarding presencial + seed de dados iniciais | -| 2026-07-19 | 2 | Fase 2 deferred | Esperar testes com coordenador geral + assistente (~quarta) antes da auditoria heurística | -| 2026-07-19 | opt CD | Outcome = Little Hire até 16/08 | Sinal: ≥1 update espontâneo em 7 dias pós-quarta | -| 2026-07-19 | opt CD | Quarta = Mom Test 25 + observe 25 | História do passado + primeira sessão sem pitch | -| 2026-07-19 | opt CD | Cadência presencial semanal + remoto ad hoc | Coordenador geral disponível remotamente quando necessário | -| 2026-07-19 | opt CD | Sala = product + onboarding lead + CG | CG é participante; interviewers = product + onboarding | +| Date | Phase | Decision | Rationale | +| ---------- | ------ | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-07-19 | Intake | Escopo = `/campanha` (não site público) | Job e atrito são de operação de campanha | +| 2026-07-19 | Intake | Fase 7 skipped | Sem superfícies de upsell in-app | +| 2026-07-19 | Intake | continuous-discovery opcional recomendada | Evidência = nenhuma; testes humanos esta semana | +| 2026-07-19 | Intake | improve-retention deferred pós-Fase 1 | Sintoma clássico de abandono por dados defasados; método completo depois do job + evidência | +| 2026-07-19 | Intake | Plano de fases confirmado | Inclui `docs/roadmap.md` como registro vivo de ideias/pendências | +| 2026-07-19 | Intake | Roadmap = backlog de build | Findings deste journey → EXPERIMENTS + itens/planos no roadmap quando forem para build | +| 2026-07-19 | 1 | Job = campanha dep. federal pela Bahia | Abrangência estadual; cargo federal | +| 2026-07-19 | 1 | Pior dimensão = emocional | Confiança no quadro atualizado; risco de ansiedade com dados mortos | +| 2026-07-19 | 1 | Leak = Little Hire | Big Hire coberto por onboarding presencial + seed de dados iniciais | +| 2026-07-19 | 2 | Fase 2 deferred | Esperar testes com coordenador geral + assistente (~quarta) antes da auditoria heurística | +| 2026-07-19 | opt CD | Outcome = Little Hire até 16/08 | Sinal: ≥1 update espontâneo em 7 dias pós-quarta | +| 2026-07-19 | opt CD | Quarta = Mom Test 25 + observe 25 | História do passado + primeira sessão sem pitch | +| 2026-07-19 | opt CD | Cadência presencial semanal + remoto ad hoc | Coordenador geral disponível remotamente quando necessário | +| 2026-07-19 | opt CD | Sala = product + onboarding lead + CG | CG é participante; interviewers = product + onboarding | +| 2026-07-23 | opt CD | Sessão âncora realizada e processada | Mom Test 7/7 no Bloco A; evidência em CUSTOMER.md (Interview Evidence + Snapshot); Bloco B perdido no áudio — usabilidade (O2/O3/O4) segue sem evidência | +| 2026-07-23 | opt CD | OST: O1/O5 confirmadas; O6–O8 novas | Dossiê pré-agenda, registro datado de deltas de voto, ilhas de comunicação — histórias reais (Cairu, Amélia Rodrigues, Santa Inês, Salvador 10×) | +| 2026-07-23 | opt CD | Âncora de prioridade = % do voto do deputado | Derruba % do eleitorado local como critério da mesa; calibra E8/E10/B13 no roadmap | +| 2026-07-23 | 2 | Fase 2 destravada (pending) | Evidência real disponível; auditoria heurística deve mirar os fluxos do delta-do-dia e da fila de prioridade | ## Next Actions @@ -57,8 +61,10 @@ Statuses: pending · in-progress · awaiting-evidence · done · deferred: \ pledge["votePledge.estimatedVotes group"] + mig --> plaza["plaza.expectedVotes group"] + pledge --> agg["votePledgeData por cenário"] + plaza --> resolve["resolvePlazaStaffVoteTotalForScenario"] + agg --> resolve + resolve --> list["PlazaList + overview"] + resolve --> map["PlazaMapPanel + ?estimate="] + resolve --> dash["dashboard / E8"] + forms["PledgeEstimateForm + Plaza*Votes"] --> pledge + forms --> plaza + leader["DeclareVotesForm"] -.só declared.-> pledge +``` + +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. +- **Depth check:** reusar `plazaStaffFormActions`, `campaignAccess`, shells B9; espelhar invariante de `voteGoals.ts` em helper fino compartilhado só se ≥2 call sites. + +## Dependências + +- **Dura:** R2 / A9 (schema e superfícies de estimativa) — código entregue; deploy da remodelagem deve estar aplicado antes de dados reais. +- **Dependentes (atualizar):** **E8** passa a depender de A10 (cobertura usa cenário, default média); **C12** suave (versions capturam o group); E9/B13 via E8. +- **Reusa:** `votePledgeData.ts`, `plazaMapData.ts`, `plazaStaffFormActions.ts`, `voteGoals.ts` (padrão de ordem), `PledgeEstimateForm`, `PlazaMapPanel`. + +## Não escopo + +- Conta da cadeira / cobertura ÷ meta → [E8](conta-da-cadeira.md). +- Versions/trajetória de pledge → [C12](registro-fundacao.md). +- Escala relativa do mapa (quantis/LQ) → [B13](escala-relativa-mapa.md). +- Alterar `voteGoals` ou fundir labels; previsão estatística; import CSV de faixas. +- Expor estimativas à liderança; auto-preencher faixa a partir do declarado. + +## 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. +- **Três choropleths / chart de incerteza.** Explode UX e appetite. **Mitigação:** um seletor, um fill; faixa só no readout/lista. +- **Exigir os três sempre + workflow de “confirmar faixa”.** Volta ao sugerir→confirmar rejeitado na remodelagem. **Mitigação:** nullable + ordem condicional. +- **Service layer / event sourcing da faixa.** **Mitigação:** group + helpers em `votePledgeData`. + +## Adiado com gatilho + +- **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). + +## Referências + +- `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/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/`) diff --git a/docs/plans/conta-da-cadeira.md b/docs/plans/conta-da-cadeira.md index b9de15eb..d7264dfb 100644 --- a/docs/plans/conta-da-cadeira.md +++ b/docs/plans/conta-da-cadeira.md @@ -1,8 +1,8 @@ # E8 — Conta da cadeira (metas derivadas, potencial por praça, cobertura) Status: rascunho -Atualizado em: 2026-07-21 -Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", E8; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md)) +Atualizado em: 2026-07-21 _(deps: A10 registrado 2026-07-23)_ +Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", E8; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md); **depende de A10**) Impeccable: B — cobertura/metas encaixadas no dashboard, na lista de Praças e no detalhe (sem rota nova) Appetite: ~2 dias eng; migration pequena (global `campaignGoals`) + utilities derivadas + encaixes de UI Responsável: — @@ -27,7 +27,7 @@ O relatório de discovery ([§5.1](../research/relatorio-entrevista-persona-camp - Global `campaignGoals` (admin group `Campanha`, staff-only): meta estadual de votos, margem, ano-base, nota. - Utilities derivadas por praça (sem persistir o derivado): válidos projetados do cargo (série 2014/2018/2022), teto do campo (majoritário 1º turno), taxa de captura, share intracampo (via `campoParties.ts`), roll-off DF×majoritária, potencial e meta sugerida (decomposição proporcional ao potencial). -- Cobertura: Σ (`estimatedVotes ?? declaredVotes`) ÷ meta por praça e agregada; delta semanal (exige C12 para série; até lá, delta vs. snapshot em memória do último load é aceitável mostrar como "—"). +- Cobertura: Σ (estimativa do cenário ativo — default **média/`central`** via A10 — `?? declaredVotes`) ÷ meta por praça e agregada; delta semanal (exige C12 para série; até lá, delta vs. snapshot em memória do último load é aceitável mostrar como "—"). - Encaixes: linha de cobertura no dashboard (`campaignDashboardData.ts`), colunas meta/cobertura na lista (`PlazaList`/`PlazaListOverview`), card no detalhe da Praça. - Access: tudo staff-only (`coordinator`/`advisor`); `leader` não vê metas — mesmo padrão de `voteGoals`. - Sanity check por TI: Σ metas das praças do TI vs. participação histórica do TI no voto (aviso, não bloqueio). @@ -67,15 +67,15 @@ Componentes: - **`src/lib/campoParties.ts`**: mapa ano→partidos do campo (2014/2018/2022/2026), com fixture de teste. - **`src/utilities/plazaPotential.ts`**: deriva por praça (reusa `plazaElectionGeography.ts` para células cidade×zona e `plazaElectoralBaseline.ts` para a série); expõe `projectedValidVotes`, `fieldCeiling`, `captureRate`, `intraFieldShare`, `rollOff`, `suggestedGoal`. -- **`src/utilities/goalCoverage.ts`**: cobertura por praça/agregado consumindo `aggregatePledgesByPlaza` (regra `estimatedVotes ?? declaredVotes` de `votePledgeData.ts`) + `voteGoals`/meta sugerida; sanity por TI via `bahiaTerritories.ts`. +- **`src/utilities/goalCoverage.ts`**: cobertura por praça/agregado consumindo `aggregatePledgesByPlaza` (regra por cenário de [A10](cenarios-estimativa-votos.md) / `votePledgeData.ts`, default `central`) + `voteGoals`/meta sugerida; sanity por TI via `bahiaTerritories.ts`. - **Global `CampaignGoals`** (`src/globals/CampaignGoals.ts`): campos `stateGoal`, `margin`, `baseYear`, `note`; access staff-read/coordinator-write; hook `revalidateGlobal`. - **UI:** linha nova em `CampaignMetricStrip` do dashboard; coluna/ordenar por cobertura em `PlazaList` (reusa padrão B9 para editar meta via `plazaStaffFormActions`); card "Conta da cadeira" no detalhe. - **Migration**: `pnpm migrate:create add_campaign_goals_global` (tabela do global). Sem mudança em collections. ## Dependências -- Dura: deploy da remodelagem (schema `plaza`/`votePledge` em produção). Suave: C12 (delta semanal real da cobertura usa trajetória de pledges; até lá, sem histórico). -- Reusa: `plazaElectoralBaseline.ts`, `plazaElectionGeography.ts`, `votePledgeData.ts`, `plazaPageData.ts` (`loadPlazaListPageBundle`), `campaignDashboardData.ts`, `plazaStaffFormActions.ts`, `campaignAccess.ts`. +- Dura: **A10** (cenários pessimista/média/otimista — [plano](cenarios-estimativa-votos.md)); deploy da remodelagem (schema `plaza`/`votePledge` em produção). Suave: C12 (delta semanal real da cobertura usa trajetória de pledges; até lá, sem histórico). +- Reusa: `plazaElectoralBaseline.ts`, `plazaElectionGeography.ts`, `votePledgeData.ts` (agregação por cenário pós-A10), `plazaPageData.ts` (`loadPlazaListPageBundle`), `campaignDashboardData.ts`, `plazaStaffFormActions.ts`, `campaignAccess.ts`. ## Não escopo diff --git a/docs/plans/inteligencia-campanha.md b/docs/plans/inteligencia-campanha.md index 7acdb51a..0c4f8149 100644 --- a/docs/plans/inteligencia-campanha.md +++ b/docs/plans/inteligencia-campanha.md @@ -46,18 +46,18 @@ Regras transversais (de §6.2/§6.4, viram comportamento do produto): **barato-a ## Fatias (funcionalidades faltantes) -| ID | Fatia (plano) | Entrega | OST | Classe | Appetite | Janela | -| --- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------ | -------- | --------- | -| E8 | **[Conta da cadeira](conta-da-cadeira.md)** | Global `campaignGoals`; potencial por praça (válidos projetados, captura do campo, share intracampo via `campoParties.ts`, roll-off, headroom); decomposição meta estadual→praça com sanity por TI; cobertura Σ`estimatedVotes ?? declaredVotes`÷meta no dashboard/lista; denominadores sempre válidos projetados (I-B) | O-A | B | ~2d | 2 | -| E9 | **[Fila de alocação](fila-de-alocacao.md)** | Lista de decisão com as 7 colunas (§5.2 do relatório: votos em jogo, meta/cobertura/delta, classe, LQ/captura, rede+frescor, competição, tendência); ordenação default por déficit descoberto; ordenação de risco (defesa por frescor); "coluna da vergonha" | O-B, O-F | B | ~1,5d | 2 | -| C12 | **[Registro-fundação](registro-fundacao.md)** | Versions em `votePledge`; sinais tipados no `plazaUpdate`; `actionPlan.origin`+`resourceCost`; collection `allocationDecision`; garantia "nunca sobrescrever" | O-E, O-G(base) | B | ~2d | 2 | -| E10 | **[Classificação territorial relativa](classificacao-territorial-relativa.md)** | Recalibra defesa/ataque/indecisa/perdida com âncoras relativas (LQ vs. share estadual próprio; múltiplos do share da cadeira marginal), multi-eixo (dominância+importância+competição+teto do campo); substitui limiares 35/20/10 de `electionInsights.ts` para DF | O-D | B | ~1d | 3 | -| B13 | **[Escala relativa no mapa](escala-relativa-mapa.md)** | Modo de escala novo: quantis do próprio candidato (5 classes) default, LQ e rank na praça; símbolo proporcional por votos em jogo sobre o polígono; % dos válidos mantido como opção; roll-off como métrica adicional | O-C | B | ~2d | 3 | -| E14 | **[Níveis de envolvimento N0–N4](niveis-de-envolvimento.md)** | `engagementLevel` com critérios, promoção/rebaixamento com histerese e sinais de reversão (via `allocationDecision`); vocabulário duplo (nível nunca exposto fora do staff — access) | O-K | B | ~1d | 3 | -| E11 | **[Motor de sugestões v1](motor-de-sugestoes.md)** | Computa subconjunto dos padrões (P1, P2, P3, P5, P6, P7, K-A/K-B; demais por fase 2) por praça; sugestão com menu/estatuto/leitura-alternativa; aceitar/descartar grava `allocationDecision`; triagem 1–5 + histerese; "pauta do silêncio" mensal | O-H | C | ~3d | 3 | -| E12 | **[Camada TI](camada-territorios-identidade.md)** | Rollups por Território de Identidade com salvaguardas MAUP (razão de agregados, mediana+amplitude+praça crítica, Metropolitano decomposto); benchmark intra-TI (T4); gatilhos T1–T3/T5 no motor = fase 2 de E11 | O-I | B | ~1,5d | 3 | -| E13 | **[Planejador de presença/giros](planejador-de-giros.md)** | Elegibilidade (5 condições ✓/—), fase do calendário (construção/consolidação/ativação), compositor de giro (J-C âncora+satélites+semente), visita pedida×justificada (J-B, usa `actionPlan.origin`) | O-J | C | ~1,5d | 3 | -| E15 | **[Backtest pós-eleição](backtest-pos-eleicao.md)** | Pledge (trajetória) vs. voto realizado por zona; calibração de limiares E10/E14; relatório de aprendizado | O-G | A | ~1d | pós-04/10 | +| ID | Fatia (plano) | Entrega | OST | Classe | Appetite | Janela | +| --- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------ | -------- | --------- | +| E8 | **[Conta da cadeira](conta-da-cadeira.md)** | Global `campaignGoals`; potencial por praça (válidos projetados, captura do campo, share intracampo via `campoParties.ts`, roll-off, headroom); decomposição meta estadual→praça com sanity por TI; cobertura Σ(estimativa do cenário — default média via **A10**)÷meta no dashboard/lista; denominadores sempre válidos projetados (I-B) | O-A | B | ~2d | 2 | +| E9 | **[Fila de alocação](fila-de-alocacao.md)** | Lista de decisão com as 7 colunas (§5.2 do relatório: votos em jogo, meta/cobertura/delta, classe, LQ/captura, rede+frescor, competição, tendência); ordenação default por déficit descoberto; ordenação de risco (defesa por frescor); "coluna da vergonha" | O-B, O-F | B | ~1,5d | 2 | +| C12 | **[Registro-fundação](registro-fundacao.md)** | Versions em `votePledge`; sinais tipados no `plazaUpdate`; `actionPlan.origin`+`resourceCost`; collection `allocationDecision`; garantia "nunca sobrescrever" | O-E, O-G(base) | B | ~2d | 2 | +| E10 | **[Classificação territorial relativa](classificacao-territorial-relativa.md)** | Recalibra defesa/ataque/indecisa/perdida com âncoras relativas (LQ vs. share estadual próprio; múltiplos do share da cadeira marginal), multi-eixo (dominância+importância+competição+teto do campo); substitui limiares 35/20/10 de `electionInsights.ts` para DF | O-D | B | ~1d | 3 | +| B13 | **[Escala relativa no mapa](escala-relativa-mapa.md)** | Modo de escala novo: quantis do próprio candidato (5 classes) default, LQ e rank na praça; símbolo proporcional por votos em jogo sobre o polígono; % dos válidos mantido como opção; roll-off como métrica adicional | O-C | B | ~2d | 3 | +| E14 | **[Níveis de envolvimento N0–N4](niveis-de-envolvimento.md)** | `engagementLevel` com critérios, promoção/rebaixamento com histerese e sinais de reversão (via `allocationDecision`); vocabulário duplo (nível nunca exposto fora do staff — access) | O-K | B | ~1d | 3 | +| E11 | **[Motor de sugestões v1](motor-de-sugestoes.md)** | Computa subconjunto dos padrões (P1, P2, P3, P5, P6, P7, K-A/K-B; demais por fase 2) por praça; sugestão com menu/estatuto/leitura-alternativa; aceitar/descartar grava `allocationDecision`; triagem 1–5 + histerese; "pauta do silêncio" mensal | O-H | C | ~3d | 3 | +| E12 | **[Camada TI](camada-territorios-identidade.md)** | Rollups por Território de Identidade com salvaguardas MAUP (razão de agregados, mediana+amplitude+praça crítica, Metropolitano decomposto); benchmark intra-TI (T4); gatilhos T1–T3/T5 no motor = fase 2 de E11 | O-I | B | ~1,5d | 3 | +| E13 | **[Planejador de presença/giros](planejador-de-giros.md)** | Elegibilidade (5 condições ✓/—), fase do calendário (construção/consolidação/ativação), compositor de giro (J-C âncora+satélites+semente), visita pedida×justificada (J-B, usa `actionPlan.origin`) | O-J | C | ~1,5d | 3 | +| E15 | **[Backtest pós-eleição](backtest-pos-eleicao.md)** | Pledge (trajetória) vs. voto realizado por zona; calibração de limiares E10/E14; relatório de aprendizado | O-G | A | ~1d | pós-04/10 | ## Decisões travadas @@ -103,11 +103,11 @@ flowchart TD E11 -.decisões acumuladas.-> E15 ``` -Reuso obrigatório (depth check): `plazaElectoralBaseline.ts`/`plazaElectionGeography.ts` (células cidade×zona), `votePledgeData.ts` (`estimatedVotes ?? declaredVotes`), `aggregatePledgesByPlaza`/`loadPlazaListPageBundle` (A9+), `BahiaMap`+`bahiaMapStyle.ts` (B6/B11/B12), `bahiaTerritories.ts` (TI), `electionInsights.ts` (substituição E10), `CampaignPageShell`/`CampaignMetricStrip`, `campaignAccess.ts`, `withPayloadTransaction`. +Reuso obrigatório (depth check): `plazaElectoralBaseline.ts`/`plazaElectionGeography.ts` (células cidade×zona), `votePledgeData.ts` (agregação por cenário pós-**A10** — [cenarios-estimativa-votos.md](cenarios-estimativa-votos.md); default `central`), `aggregatePledgesByPlaza`/`loadPlazaListPageBundle` (A9+), `BahiaMap`+`bahiaMapStyle.ts` (B6/B11/B12), `bahiaTerritories.ts` (TI), `electionInsights.ts` (substituição E10), `CampaignPageShell`/`CampaignMetricStrip`, `campaignAccess.ts`, `withPayloadTransaction`. ## Dependências -- Duras: deploy da remodelagem (schema `plaza`/`votePledge` em produção); E8→E9/E10/E12/E13; C12→E11/E15. Suaves: A6 dobradinha (melhora J-B/E13); dados reais da Onda 0 (o motor roda com dados fictícios no smoke, mas só vale com rede real). +- Duras: deploy da remodelagem (schema `plaza`/`votePledge` em produção); **A10→E8** (faixa pessimista/média/otimista antes da cobertura); E8→E9/E10/E12/E13; C12→E11/E15. Suaves: A6 dobradinha (melhora J-B/E13); dados reais da Onda 0 (o motor roda com dados fictícios no smoke, mas só vale com rede real); A10→C12 (versions capturam o group). - Nenhuma dependência de site público/admin. ## Não escopo diff --git a/docs/plans/notifications.md b/docs/plans/notifications.md index 54c4fc26..5e960463 100644 --- a/docs/plans/notifications.md +++ b/docs/plans/notifications.md @@ -24,7 +24,7 @@ Coordenadores e lideranças precisam ser avisados de eventos da campanha (convit - **Push web** via Push API + Service Worker + VAPID (entrega em segundo plano, inclusive com o app fechado). - **Sino de notificações in-app** dentro do `/campanha` (central de avisos lidos/não lidos). -Notificações por WhatsApp Business API ficam **fora** deste plano — já existem como item separado no roadmap (linha 57), exigem Meta + templates e serão reavaliadas quando o volume justificar. +Notificações por WhatsApp Business API ficam **fora** deste plano (Meta + Res. TSE 23.610 art. 33). Canal WhatsApp **1:1 interno** staff↔lideranças (bridge não oficial) é o programa **D3–D5** — [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md) — complementar a push/sino, não substituto. ## Objetivos diff --git a/docs/plans/whatsapp-canal-fundacao.md b/docs/plans/whatsapp-canal-fundacao.md new file mode 100644 index 00000000..ec6ad4be --- /dev/null +++ b/docs/plans/whatsapp-canal-fundacao.md @@ -0,0 +1,127 @@ +# D3 — Fundação do canal WhatsApp interno + +Status: rascunho +Atualizado em: 2026-07-23 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha D, item D3; programa [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md)) +Impeccable: B — pareamento QR self-service por staff (sem inbox/compose de produto) +Appetite: ~3 dias eng; sidecar multi-sessão + migration + Consent + UI QR/status; **sem** envio/inbox de produto +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (princípios 1 soberania, 2 clareza, 4 depth/simplicity) / `DESIGN.md` (register `product`) · tema `data-theme='campaign'` · shells `CampaignPageShell` · superfície de perfil existente (`/campanha/perfil` se houver). + +Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (fluxo QR + estados; sem redesign de CRM). + +Brief compacto: + +- **Persona / contexto:** Assessor ou Coordenador Geral no celular/notebook quer **ligar o próprio Zap** uma vez e seguir falando com lideranças “como sempre” — só que o Teqo passa a enxergar o fio. +- **Job principal:** parear a conta WhatsApp **pessoal** do `campaignUser` via QR no app e manter a sessão viva no sidecar. +- **Estratégia de cor:** Restrained; sessão caída = `destructive`/`cadence-overdue`; aviso ToS em muted, não modal de terror. +- **Edit where you see:** pairing self-service no **perfil** do staff (ação rara); não no CRM de lideranças. +- **Anti-goals:** número institucional único; admin pareando o chip de outra pessoa; chat UI; inbox; sync de histórico completo; esconder risco ToS/ban. + +## Dados → decisão → apresentação + +- **Vou apresentar dados?** Sim, superfície mínima neste item (status **da minha** sessão). +- **Decisões desbloqueadas:** + - Assessor/CG: “meu WhatsApp está conectado ao Teqo? preciso escanear de novo?” + - (Aggregate para D4/D5: sessão do ator healthy → habilitar compose/inbox daquela pessoa.) +- **Forma escolhida:** **número + contexto** (estado `conectado | aguardando QR | erro` + telefone mascarado da conta pareada + `lastSeenAt`). **Rejeitado:** dashboard de N sessões para o time inteiro na v1 (CG pode ter lista read-only “quem conectou” — Adiado); chart de uptime; KPI de conversas. +- **Profile:** categórico; 1 sessão por `campaignUser` staff. +- **Anti-goals de dado:** sem vanity de volume de msgs nesta fatia. + +## Contexto + +O programa exige que a liderança continue vendo o **Zap pessoal** do assessor/CG — não um robô institucional. Isso implica **N sessões** (uma por staff), pareadas no app (QR), não um chip compartilhado. + +Socket WhatsApp não roda em Vercel serverless → sidecar multi-sessão. Sem fundação (sessão↔ `campaignUser`, adapter, log, Consent), D4/D5 não têm onde plugar. + +## Objetivos + +- Sidecar Node longo com **um** adapter não oficial, **N sessões** indexadas por `campaignUser.id` (só roles `coordinator` | `advisor`). +- Fluxo de pareamento no `/campanha`: `startPairing` → sidecar gera QR → UI mostra QR (refresh se expirar) → evento `paired` → status `connected` + telefone detectado (normalizado). +- App Next.js ↔ sidecar via API autenticada (HMAC); o Next **não** abre socket WA. +- Collections: `whatsappSession` (**1:1 com `campaignUser`**, unique) + `whatsappMessage` (log com `session` / `campaignUser`, direção, phones, texto, timestamps, match opcional `leadership`). +- Consent **`campanha-whatsapp-canal`** (fail-closed) + aceite explícito no UI de pairing (risco ToS/conta pessoal). Texto no lote jurídico. +- UI: status + QR + desconectar **só da própria sessão**; `leader` não pareia. +- Guardrails: allowlist inbound = phones de lideranças engajadas no CRM (e, para advisor, preferir as das Praças acessíveis — pode afrouxar para “todas lideranças engajadas” se match for só por phone); rate limit por sessão; secrets em env. + +## Decisões travadas + +- **Sessão = `campaignUser` staff, número pessoal.** UNIQUE `(campaignUser)`. **Rejeitado:** sessão singleton institucional; parear número digitado sem QR; um CG operar o chip de um assessor. +- **Login/pareamento = QR no app (estilo WhatsApp Web).** **Rejeitado:** só deep-link `wa.me`; pairing por SMS inventado; exigir app Companion oficial da Meta como único caminho (se o adapter oferecer código de pareamento sem QR como fallback técnico, ok — UX primária = QR). +- **Sidecar multi-sessão fora do Vercel.** Credenciais por sessão em volume do worker. **Rejeitado:** Baileys dentro de Server Action; Cloud API / WABA. +- **Um adapter na v1.** Baileys _ou_ Evolution self-hosted — um só. **Rejeitado:** multi-provider. +- **Log mínimo pós-`pairedAt`; sem histórico prévio; sem mídia binária na v1.** **Rejeitado:** dump completo do chip; gravar chats com não-CRM. +- **Consent `campanha-whatsapp-canal` + copy de risco pessoal.** **Rejeitado:** hardcode de ID; silêncio sobre ban/ToS. +- **i18n/naming:** `whatsappSession`, `whatsappMessage`, `startWhatsappPairing`, `whatsappBridge`; UI pt-BR (“Conectar meu WhatsApp”, “Escaneie o QR com o celular”). + +## Questões em aberto + +- **Onde hospedar o sidecar?** **Opções:** Fly.io / Railway / VPS. **Recomendação:** Fly/Railway com volume; runbook na implementação. _(ops)_ +- **CG vê quem do time está conectado?** **Opções:** não na v1 | lista read-only de status. **Recomendação:** não na v1 (só a própria sessão); lista de saúde do time = Adiado com gatilho no programa. + +## Abordagem proposta + +```mermaid +flowchart LR + Staff["Assessor / CG
/campanha/perfil"] + QR["QR pairing
própria sessão"] + Next["Next.js
HMAC + queue"] + Worker["Sidecar
N sessões WA"] + Sess["whatsappSession
1:1 campaignUser"] + Log["whatsappMessage"] + CRM["leadership via phone"] + + Staff --> QR --> Next + Next <--> Worker + Worker --> Sess + Worker --> Log + Log --> CRM +``` + +Componentes: + +- **`whatsappBridge`** (`src/utilities/whatsappBridge.ts`, `server-only`): `startPairing(userId)`, `getSessionStatus(userId)`, `disconnect(userId)`, (D4) `enqueueSend(userId, …)`; timeout curto. +- **Sidecar** (`workers/whatsapp-bridge/`): map `campaignUserId → adapter session`; emite QR/status/inbound; allowlist CRM. +- **`whatsappSession`**: `user` → `campaignUser` (unique, required), `status`, `pairedPhone`, `pairedAt`, `lastSeenAt`, `lastError`. +- **`whatsappMessage`**: `session`/`user`, `direction`, `peerPhone`, `leadership` (nullable), `body`, `providerMessageId`, timestamps. +- **UI** `WhatsappPairingPanel` no perfil: QR image/data-URL ou SVG, polling/SSE de status, botão desconectar, aviso Consent/ToS. +- **Actions** `startWhatsappPairing` / `disconnectWhatsappSession` / `getWhatsappSessionStatus` — actor só na própria sessão (`user.id`). +- **Migration:** `pnpm migrate:create add_whatsapp_channel` — session + message; sem backfill. +- **Reuse:** `normalizeBrazilianPhone`, `campaignConsent`, `withPayloadTransaction`, `overrideAccess: false`. + +## Dependências + +- **Dura:** remodelagem deployada (`campaignUser` roles + `leadership`/`Contact`). +- **Suave:** Onda 0 (texto Consent); telefones reais nas lideranças. +- **Não depende de D2.** + +## Não escopo + +- Compose/envio → [D4](whatsapp-envio-liderancas.md). +- Inbox / rascunhos → [D5](whatsapp-sugestao-atualizacoes.md). +- Número institucional / WABA / massa → fora. +- Mídia no log; grupos; histórico pré-pairing → Adiado. + +## Rabbit holes + +- **N sockets × memória no worker.** Mitigação: lazy connect on pairing; disconnect idle com re-QR; monitorar RSS antes de escalar horizontalmente (sticky sessions por user se shardear — Adiado). +- **Staff desconecta no celular e o Teqo não sabe.** Mitigação: health ping + status `erro` + CTA re-parear no compose D4. +- **Parear conta errada (pessoal demais / família).** Mitigação: mostrar telefone detectado pós-QR + “Desconectar”; copy de que deve ser o número que as lideranças já usam. + +## Adiado com gatilho + +- **Retention/expurgo do log.** Revisitar quando: volume ou pedido jurídico. +- **Lista CG “quem conectou”.** Revisitar quando: onboarding do time travar sem visão de cobertura. +- **Advisor-only allowlist por Praça no inbound.** Revisitar se falso positivo de match cruzado aparecer. + +## Referências + +- [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md) +- `docs/roadmap.md` — Trilha D +- `src/utilities/phone.ts`, `campaignConsent.ts`, `campaignAccess.ts` +- `docs/plans/notifications.md` — D2 +- `PRODUCT.md` / `DESIGN.md` +- AGENTS.md — Consent, migrations, campaign auth diff --git a/docs/plans/whatsapp-envio-liderancas.md b/docs/plans/whatsapp-envio-liderancas.md new file mode 100644 index 00000000..ec1c7073 --- /dev/null +++ b/docs/plans/whatsapp-envio-liderancas.md @@ -0,0 +1,104 @@ +# D4 — Envio WhatsApp 1:1 para lideranças + +Status: rascunho +Atualizado em: 2026-07-23 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha D, item D4; programa [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md)) +Impeccable: B — compose encaixado no CRM de liderança / detalhe (sem rota de “marketing blast”) +Appetite: ~1–2 dias eng; actions + UI compose + templates curtos; reusa sessão pessoal D3 +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (princípios 2, 3 Edit where you see, 4) / `DESIGN.md` (register `product`) · tema `campaign` · precedente `LeadershipInviteButtons` / share `wa.me`. + +Na implementação: craft compacto → critique → polish. + +Brief compacto: + +- **Persona / contexto:** Assessor na estrada cobra atualização; CG manda um “como está Orolândia?” — a liderança deve ver a mensagem **no chat pessoal de sempre** com aquela pessoa. +- **Job principal:** enviar **uma** mensagem 1:1 pela **sessão WhatsApp do próprio ator** para **uma** liderança do escopo. +- **Estratégia de cor:** Restrained; ícone WA ok; primário Signal Red. +- **Edit where you see:** sim — Sheet “Mensagem” no detalhe `/campanha/liderancas/[id]`. +- **Anti-goals:** multi-destinatário; enviar pela sessão de outro staff; número institucional; templates de propaganda; matar `wa.me` de convites. + +## Dados → decisão → apresentação + +Dados: N/A — superfície de ação (compose). Feedback de entrega = Feel the action (§7), não KPI. + +## Contexto + +Com [D3](whatsapp-canal-fundacao.md), cada staff tem (ou não) a própria sessão pareada. O valor de produto é: **parece Zap pessoal** — a liderança não “fala com a campanha”, fala com o assessor/CG que já conhece. + +## Objetivos + +- Action `sendLeadershipWhatsappMessage({ leadershipId, body })`: access OK → resolve phone → exige **`whatsappSession` do `req.user` em `connected`** + Consent → `enqueueSend(actorUserId, …)` → log outbound com `user` = ator. +- UI compose: textarea + templates curtos editáveis; se sessão down → CTA “Conectar meu WhatsApp” (perfil D3) **e** fallback `wa.me` (“Abrir no seu WhatsApp”). +- Rate limit por ator/hora e por destinatário; **hard reject** N>1 destinatários. +- Guardrails: só `coordinator`/`advisor`; advisor só lideranças das Praças acessíveis; nunca `supporter`; nunca enviar usando sessão de terceiro. + +## Decisões travadas + +- **Remetente = sessão do ator logado.** A liderança recebe do número pessoal pareado daquela pessoa. **Rejeitado:** “enviar como campanha”; CG disparar pelo chip de um assessor; escolher remetente na UI. +- **Um destinatário por envio.** **Rejeitado:** lista/BCC; “todas as lideranças da Praça”. +- **Escopo = access da liderança.** **Rejeitado:** telefone livre digitado. +- **`wa.me` = fallback + convites.** **Rejeitado:** migrar convites para o bridge nesta fatia. +- **i18n/naming:** `sendLeadershipWhatsappMessage`, `LeadershipWhatsappCompose`; copy pt-BR (“Enviar pelo meu WhatsApp”). + +## Questões em aberto + +- **Templates: hardcoded vs collection?** **Opções:** 3–5 strings em código | collection. **Recomendação:** constantes na v1. +- **Thread no detalhe?** **Opções:** só compose | compose + últimas N do par **nesta sessão**. **Recomendação:** últimas 10 msgs do par na sessão do ator (não misturar threads de outro assessor). _(assumido)_ + +## Abordagem proposta + +```mermaid +flowchart LR + UI["LeadershipWhatsappCompose"] + Action["sendLeadershipWhatsappMessage"] + Sess["whatsappSession do ator"] + Bridge["enqueueSend(actorId, …)"] + Log["whatsappMessage outbound"] + Lead["leadership phone"] + + UI --> Action + Action --> Sess + Action --> Lead + Action --> Bridge --> Log +``` + +Componentes: + +- **`LeadershipWhatsappCompose`**: Sheet; estados connected / needs-pairing / sending / failed; deep-link perfil se unpaired. +- **Action** em `actions/whatsapp.ts`: `overrideAccess: false`; nunca aceita `sessionUserId` do client (só `req.user.id`). +- **Reuse D3:** bridge, session, Consent, phone normalize. +- **Migration:** nenhuma se D3 já tem `user` no log. + +## Dependências + +- **Dura:** **D3** (sessão pessoal + log + Consent). +- **Suave:** telefones reais nas lideranças. + +## Não escopo + +- Inbox / rascunhos → [D5](whatsapp-sugestao-atualizacoes.md). +- Apoiadores / grupos / auto-resposta → fora. +- Push “mensagem enviada” → D2. + +## Rabbit holes + +- **Templates ricos.** Mitigação: `{nome}`, `{praca}` no máximo. +- **Fila com retry infinito.** Mitigação: retry curto + `failed` visível. + +## Adiado com gatilho + +- Bulk UI. Revisitar só com ok jurídico explícito. +- CG enviar “em cópia” vendo thread do assessor. Revisitar com o item de inbox agregado do programa (acesso cruzado). + +## Referências + +- [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md), [whatsapp-canal-fundacao.md](whatsapp-canal-fundacao.md) +- `src/components/campaign/LeadershipInviteButtons.tsx` +- `src/app/(campaign)/campanha/(app)/liderancas/[id]/page.tsx` +- `src/utilities/campaignAccess.ts`, `phone.ts` +- `PRODUCT.md` / `DESIGN.md` +- AGENTS.md — access, transactions diff --git a/docs/plans/whatsapp-interno-campanha.md b/docs/plans/whatsapp-interno-campanha.md new file mode 100644 index 00000000..78ac2806 --- /dev/null +++ b/docs/plans/whatsapp-interno-campanha.md @@ -0,0 +1,92 @@ +# WhatsApp interno da campanha (programa D3–D5) + +Status: rascunho +Atualizado em: 2026-07-23 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha D, programa D3–D5) +Impeccable: N/A — plano-mestre; classes por fatia (D3 A/B, D4 B, D5 C) +Appetite: programa ~5–8 dias eng fatiados; **não** implementar como monólito +Responsável: — + +## Dados → decisão → apresentação + +Dados: N/A neste plano-mestre — cada fatia declara a sua. O programa existe para **fechar o Little Hire** (confiança no quadro atualizado): o canal real (ZAP) deixa de competir com o Field Desk e passa a alimentá-lo. + +## Contexto + +Feedback de produto (2026-07-23) + evidência de discovery/entrevista: lideranças e staff dificilmente sentirão segurança registrando só no `/campanha` enquanto o trabalho real continua no WhatsApp. O coordenador geral descreve o ZAP como o campo (“tudo após o ZAP”; mensagem > ligação). O [`IMPROVE-APP-PLAN.md`](../IMPROVE-APP-PLAN.md) nomeia o leak: assessores param de atualizar → WhatsApp/planilha → ferramenta defasada → abandono. + +Hoje o produto só usa WhatsApp como **atalho `wa.me`** (convites, share, kit de apoiador): o humano envia no app dele; o Teqo não vê a conversa nem grava o sinal. **D2** (push + sino) cobre avisos _dentro_ do app — complementar, não substituto do canal de campo. + +**Fronteira legal/produto já travada no roadmap:** WhatsApp Business API e disparo em massa continuam **fora de escopo** (Meta veda campanhas políticas; Res. TSE 23.610 art. 33). Este programa é **comunicação 1:1 interna** staff ↔ lideranças já cadastradas (`Contact`/`leadership`) — não propaganda a terceiros nem blast. + +**Decisão de produto (2026-07-23):** o envio deve parecer o canal **pessoal** do assessor/coordenador (é o que a liderança espera), não um número institucional da campanha. Pareamento no próprio `/campanha` (QR, estilo WhatsApp Web). + +## Objetivos do programa + +- Trazer o canal ZAP para _dentro_ do fluxo de poder do Field Desk sem virar o sistema de registro (princípio 1 de `PRODUCT.md`: atalho pragmático; verdade no Teqo). +- Cada `coordinator`/`advisor` vincula **o próprio WhatsApp pessoal** e fala com lideranças **como se fosse o chip dele** — a liderança recebe no chat pessoal de sempre. +- Fatiar em três entregas shipáveis (D3 → D4 → D5), cada uma com appetite próprio. +- Humano no loop em qualquer escrita de domínio (`plazaUpdate`, demanda, pledge) — alinhado a E11/C12. +- Fail-closed em Consent, access por papel/Praça, e auditoria mínima das mensagens processadas. + +## Fatias (ordem de execução) + +| ID | Plano | Entrega essencial | Classe | Appetite | Depende de | +| --- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------ | -------- | ----------------------------------- | +| D3 | [whatsapp-canal-fundacao.md](whatsapp-canal-fundacao.md) | Bridge multi-sessão + **QR por staff** + log + Consent; **zero** envio em massa | B | ~3d | remodelagem em prod (suave: Onda 0) | +| D4 | [whatsapp-envio-liderancas.md](whatsapp-envio-liderancas.md) | Compose/envio 1:1 pela **sessão do ator** (CRM → liderança; parece chat pessoal) | B | ~1–2d | **D3** | +| D5 | [whatsapp-sugestao-atualizacoes.md](whatsapp-sugestao-atualizacoes.md) | Inbox inbound **na sessão de quem recebeu** → rascunhos com aceite humano | C | ~2–3d | **D3** (suave: D4, C12) | + +```mermaid +flowchart TD + D3["D3 Fundação
QR por campaignUser"] + D4["D4 Envio 1:1
sessão do ator"] + D5["D5 Inbox → rascunhos
humano confirma"] + D3 --> D4 + D3 --> D5 + D4 -.thread/contexto.-> D5 +``` + +## Decisões travadas (programa) + +- **Canal interno 1:1, framework não oficial permitido.** Destinatários = lideranças já no CRM; never blast a apoiadores/eleitores. **Rejeitado:** WhatsApp Business API; disparo em massa / listas (TSE art. 33); bot que responde sozinho a terceiros. +- **Número pessoal por staff (`campaignUser`), não linha institucional.** Cada assessor/coordenador geral pareia o próprio WhatsApp; a liderança vê o remetente que já conhece. **Rejeitado:** chip único da campanha (quebra a relação pessoal — pedido explícito de produto 2026-07-23); um único CG enviando “em nome de” todos os assessores. +- **Pareamento no app via QR (estilo WhatsApp Web).** Staff abre `/campanha`, inicia pareamento, escaneia com o celular; sessão fica no sidecar. **Rejeitado:** só `wa.me` forever; pedir senha/OTP inventado; forçar app nativo Meta. +- **Teqo permanece system of record.** Mensagens são evidência/auditoria; commits de domínio só após ação humana (D5). **Rejeitado:** auto-criar `plazaUpdate`/`votePledge` sem aceite. +- **Sidecar obrigatório (multi-sessão).** N sockets (um por staff vinculado); não vive em Vercel serverless. **Rejeitado:** “só mais uma API route” no Next. +- **Não substitui D2 nem `wa.me`.** D2 = push in-app; `wa.me` = fallback se a sessão do ator estiver down. **Rejeitado:** remover convites `wa.me` neste programa. +- **Risco ToS/ban é pessoal e explícito.** Consent + copy no pairing avisam que o vínculo usa cliente não oficial e pode afetar a conta WhatsApp da pessoa. **Rejeitado:** esconder o risco; usar conta do deputado/candidato como default. + +## Questões em aberto (programa) + +- **Onde vive o QR no IA?** **Opções:** `/campanha/perfil` | `/campanha/configuracoes/whatsapp` | Sheet no compose D4 (“conectar para enviar”). **Recomendação:** `/campanha/perfil` (self-service, cada um liga o seu) + estado “WhatsApp conectado/desconectado” no compose D4 com deep-link para parear. _(shape D3)_ +- **LLM para classificar inbound em D5?** **Opções:** regras/heurística | LLM com revisão humana | só triagem manual. **Recomendação:** D5 v1 = heurística + templates + edição humana; LLM só com gatilho. + +## Não escopo (programa) + +- Business API / templates Meta / disparo a base nominal → continua fora de escopo. +- Chatbot eleitoral público / atendimento a cidadão → fora. +- Número institucional / “WhatsApp da campanha” compartilhado → fora (conflita com a decisão de canal pessoal). +- Substituir onboarding presencial / seed (Big Hire) → IMPROVE-APP-PLAN. + +## Rabbit holes + +- **Evolution API / Baileys / multi-device / grupos WA.** Mitigação: D3 escolhe **um** adapter; multi-**sessão** (N staff) sim; multi-**número por pessoa** e grupos = Adiado. +- **“Sincronizar todo o histórico do chip.”** Mitigação: só mensagens após pairing + filtro por telefones do CRM (e, no inbox, só o que chegou naquela sessão). +- **Espionar conversas pessoais do staff com não-lideranças.** Mitigação: allowlist estrita de phones do CRM; não indexar chats fora da allowlist. + +## Adiado com gatilho + +- Sessão secundária / segundo chip por pessoa. Revisitar quando: pedido explícito de um assessor com dois números de trabalho. +- LLM de classificação. Revisitar quando: D5 v1 com ≥50 rascunhos/semana e taxa de descarte >40% por “categoria errada”. +- Coordenador ver inbox agregado de todos os assessores. Revisitar quando: CG pedir visão transversal **e** jurídico ok com acesso cruzado a threads pessoais. + +## Referências + +- `docs/roadmap.md` — Trilha D; Fora de escopo (Business API / massa) +- Entrevista CG 2026-07-23 (`output/transcribe/general-coordinator-interview-20260723/`) — “ZAP é o campo” +- `docs/IMPROVE-APP-PLAN.md` — Little Hire / leak WhatsApp +- `PRODUCT.md` §1 soberania + atalho pragmático; §5 humano no loop +- `docs/plans/notifications.md` (D2) — complementar +- `src/utilities/phone.ts` — `buildWhatsAppUrl` (fallback `wa.me`) +- AGENTS.md — Contact, Consent por chave, campaign access, Vercel diff --git a/docs/plans/whatsapp-sugestao-atualizacoes.md b/docs/plans/whatsapp-sugestao-atualizacoes.md new file mode 100644 index 00000000..a631683c --- /dev/null +++ b/docs/plans/whatsapp-sugestao-atualizacoes.md @@ -0,0 +1,113 @@ +# D5 — Sugestões de atualização a partir do WhatsApp inbound + +Status: rascunho +Atualizado em: 2026-07-23 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha D, item D5; programa [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md)) +Impeccable: C — superfície nova (inbox + revisão de rascunhos); sem design-ref +Appetite: ~2–3 dias eng; inbox staff + draft suggestions + aceite → domínio; **sem** auto-commit +Responsável: — + +## Design (Impeccable) + +Âncoras: `PRODUCT.md` (princípios 1, 2, 5 humano no loop, 7) / `DESIGN.md` (register `product`) · tema `campaign` · shells `CampaignPageShell`, feeds `PlazaUpdateFeed` / forms `PlazaUpdateForm` · paralelo E11 (menu + aceite/descarte). + +Na implementação: shape → craft → critique → polish (classe C). + +Brief compacto: + +- **Persona / contexto:** Assessor que já conversou no ZAP com a liderança e precisa **não redigitar** o reporte no Teqo; Coordenador que quer o quadro atualizado sem caçar print no grupo. +- **Job principal:** transformar mensagem inbound em **rascunho editável** (`plazaUpdate` / demanda / nota de voto) e só gravar no domínio após aceite humano. +- **Estratégia de cor:** Restrained; fila por frescor, não por “AI score”. +- **Edit where you see:** sim — aceitar/editar/descartar no card do rascunho; deep-link para a Praça/liderança. +- **Anti-goals:** bot que grava sozinho; LLM obrigatório na v1; inbox estilo WhatsApp Web clone; “caça” a mensagens de não-lideranças. + +## Dados → decisão → apresentação + +- **Vou apresentar dados?** Sim, superfície neste item (fila de rascunhos + origem). +- **Decisões desbloqueadas:** + - Assessor: “esta mensagem vira atualização da Praça X, demanda, ou só arquivo?” + - Assessor/CG: “aceito o texto sugerido ou edito antes de gravar?” + - Staff: “descarto (ruído / off-topic) para não poluir o frescor da Praça?” +- **Forma escolhida:** **lista/fila** de itens (mensagem → tipo sugerido → preview) + detalhe com editar/aceitar/descartar. **Rejeitado:** dashboard de % auto-classificados; chart de volume WA; mapa de mensagens. +- **Profile:** itens categóricos + texto; dezenas/dia típico (não milhares); granularidade liderança×praça. +- **Anti-goals de dado:** sem vanity “msgs processadas”; sem auto-atualizar `lastUpdateAt` sem aceite (frescor mentiroso). + +## Contexto + +O Little Hire falha quando o reporte vive só no ZAP ([IMPROVE-APP-PLAN](../IMPROVE-APP-PLAN.md); entrevista CG: canal real = mensagem). D3 grava inbound no log; D4 cobre outbound. Falta o caminho **ZAP → registro** com humano no loop — o mesmo estatuto de E11 (“sugestão nunca decide”). + +Domínio alvo já existe: `plazaUpdate` (`PlazaUpdateForm` / feed), `campaignDemand`, `votePledge.declaredVotes` (liderança) / estimativas staff (assimetria). + +## Objetivos + +- Inbox staff (`/campanha/whatsapp` ou seção no dashboard): só mensagens inbound da **própria sessão** do ator (o Zap pessoal dele); match a `leadership` por phone; unmatched→triagem. +- Gerador de **draft** (heurística v1): classifica intent grosso (`atualizacao | demanda | voto | outro`) + sugere Praça (se liderança tem 1 Praça; se N, pede escolha) + texto pré-preenchido editável. +- Aceite cria o documento de domínio na transação (`withPayloadTransaction`), liga `whatsappMessage` → entidade criada (auditoria), respeita access/locks existentes (`plazaUpdate` locks). +- Descarte registra motivo curto (opcional na v1) e marca mensagem como `ignored` — não some do log. +- Guardrails: **nunca** auto-create; inbox = sessão do ator (não ver thread do colega na v1); advisor ainda filtrável às Praças acessíveis; `leader` não usa inbox; sem processar phones fora do CRM. + +## Decisões travadas + +- **Humano no loop obrigatório.** Draft ≠ commit. **Rejeitado:** auto-`plazaUpdate` por NLP; webhook que escreve domínio direto. +- **Inbox = sessão pessoal do ator.** Alinhado a D3/D4 (canal pessoal). **Rejeitado:** inbox único da “campanha”; CG ler todas as threads dos assessores na v1 (Adiado no programa). +- **Heurística v1, não LLM.** Palavras-chave / botões de tipo manual + textarea. **Rejeitado:** LLM na v1 (custo, indeterminismo, appetite); ver Adiado. +- **Teqo é o registro; WA é evidência.** `lastUpdateAt` e versões (C12) só mudam no aceite. **Rejeitado:** usar timestamp da msg WA como frescor oficial sem commit. +- **i18n/naming:** `whatsappInboundDraft`, `suggestPlazaUpdateFromMessage`, `acceptWhatsappDraft`; rotas/params em inglês; UI pt-BR (“Sugestões do WhatsApp”). + +## Questões em aberto + +- **Rota dedicada vs. painel?** **Opções:** `/campanha/whatsapp` | aba no dashboard | Sheet na liderança. **Recomendação:** rota `/campanha/whatsapp` (inbox) + entrada “Criar a partir do WhatsApp” no detalhe da liderança se houver msgs pendentes. _(shape fecha layout)_ +- **Quais tipos de domínio na v1?** **Opções:** só `plazaUpdate` | update+demanda | update+demanda+voto. **Recomendação:** `plazaUpdate` + `campaignDemand` na v1; voto (declared/estimated) só se o texto for inequívoco **e** o aceite abrir o form de pledge existente — senão Adiado. _(assumido — validar)_ + +## Abordagem proposta + +```mermaid +flowchart LR + In["whatsappMessage inbound
(D3 allowlist)"] + Match["match Contact → leadership"] + Draft["whatsappInboundDraft
heurística + edição"] + Human["Aceitar / Editar / Descartar"] + Dom["plazaUpdate / campaignDemand
(+ pledge form)"] + In --> Match --> Draft --> Human --> Dom +``` + +Componentes: + +- **Collection ou table `whatsappInboundDraft`** (status `pending|accepted|discarded`, `suggestedKind`, `plaza`, `body`, rel `whatsappMessage`): migration própria desta fatia **ou** campos no `whatsappMessage` se depth preferir menos collections — **recomendação:** collection draft (ciclo de vida claro; não polui log imutável). +- **`suggestDraftFromMessage`** puro + action `acceptWhatsappDraft` / `discardWhatsappDraft` reusando creates existentes de update/demanda (não duplicar invariantes). +- **UI** `WhatsappInboxPage` + `WhatsappDraftReviewCard`; reusa `PlazaUpdateForm` fields onde der (depth check). +- **Suave C12:** se sinais tipados em `plazaUpdate` existirem, o aceite pode preencher `kind`/sinal — senão texto livre como hoje. +- **Migration:** `pnpm migrate:create add_whatsapp_inbound_draft`. + +## Dependências + +- **Dura:** **D3** (inbound log + match phone + Consent). +- **Suave:** **D4** (thread/contexto outbound); **C12** (sinais tipados); remodelagem/demandas já em prod. + +## Não escopo + +- Auto-resposta / chatbot → fora. +- Classificação LLM → Adiado com gatilho (programa). +- Envio em massa / apoiadores → fora. +- Clone de WhatsApp Web (sync de mídia, status, reactions) → fora. + +## Rabbit holes + +- **Resolver Praça quando liderança tem N Praças.** Mitigação: obrigar select no draft; sem “adivinhar” por NLP geográfico. +- **Mesclar com motor E11.** Mitigação: filas separadas (E11 = território/meta; D5 = inbox de mensagens); no máximo link “abrir Praça”. +- **Reprocessar histórico em massa.** Mitigação: só msgs após feature flag / `draftsEnabledAt`. + +## Adiado com gatilho + +- **LLM classifier.** Revisitar quando: ≥50 drafts/semana e descarte >40% por tipo errado (critério do programa). +- **Sugerir `declaredVotes` / cenários A10.** Revisitar quando: assessores pedirem explicitamente e houver padrão estável de msgs numéricas. +- **Mídia → anexo em `plazaUpdate`.** Revisitar quando: D3 passar a persistir mídia. + +## Referências + +- [whatsapp-interno-campanha.md](whatsapp-interno-campanha.md), [whatsapp-canal-fundacao.md](whatsapp-canal-fundacao.md), [whatsapp-envio-liderancas.md](whatsapp-envio-liderancas.md) +- `src/collections/PlazaUpdate.ts`, `src/components/campaign/PlazaUpdateForm.tsx` +- `docs/plans/motor-de-sugestoes.md` (E11) — precedente humano no loop +- `docs/IMPROVE-APP-PLAN.md` — Little Hire +- `PRODUCT.md` / `DESIGN.md` +- AGENTS.md — transactions, access, Consent diff --git a/docs/roadmap.md b/docs/roadmap.md index 2a64ff2f..6c47303c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap — Teqo -Atualizado em: 2026-07-23 (Admin Payload: export CSV de assinaturas e contatos entregue — [plano](plans/exportar-csv-assinaturas.md)) +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) 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). @@ -21,13 +21,13 @@ Registro canônico dos **próximos** planos e débitos. Histórico de entregas: - Um único app Next.js: site `(frontend)`, admin `(payload)`, `/campanha` `(campaign)`. Sem Rust separado. Vercel por enquanto. - Doações só via CTA QueroApoiar (`apoiar.me/jorgesolla`). Pessoa = `Contact` + joins — nunca cadastro paralelo. - **Praça = unidade operacional pré-definida** (2026-07-20): município, exceto Salvador (19 Praças-zona) e Camaçari (2 Praças-zona); zonas compartilhadas cortadas na divisa municipal. Ninguém cria/edita geografia no app. Substitui o Núcleo Eleitoral — plano [remodelagem-pracas.md](plans/remodelagem-pracas.md). -- Roles: `coordinator` ("Coordenador Geral"), `advisor` ("Assessor"), `leader` ("Liderança"); assessor vê só as Praças que administra. Sem disparo em massa (Res. TSE 23.610 art. 33 / Meta). +- Roles: `coordinator` ("Coordenador Geral"), `advisor` ("Assessor"), `leader` ("Liderança"); assessor vê só as Praças que administra. Sem disparo em massa (Res. TSE 23.610 art. 33 / Meta). Comunicação **1:1 interna** staff↔lideranças via bridge WhatsApp não oficial = programa **D3–D5** (não é Business API nem blast). ## Onda 0 — caminho crítico para dados reais Textos provisórios de Consent + `/privacidade` auto-provisionados ([onda-0.md](plans/onda-0.md)); **hold de PII real** até o lote jurídico final. Inalterada pela remodelagem (mesmas chaves `Consent`). -1. **Lote jurídico único** _(externo)_ — textos finais (substituem provisórios) + base LGPD art. 11: `lideranca-autopreenchimento`, `apoiador-cadastro`, `apoiador-intencao-voto`, `campanha-notificacoes-push`, Aviso de Privacidade, avaliação RIPD. +1. **Lote jurídico único** _(externo)_ — textos finais (substituem provisórios) + base LGPD art. 11: `lideranca-autopreenchimento`, `apoiador-cadastro`, `apoiador-intencao-voto`, `campanha-notificacoes-push`, **`campanha-whatsapp-canal`** (D3 — armazenamento/processamento do canal interno staff↔lideranças; se o lote já tiver fechado sem esta chave, entra no próximo passe com a assessoria — não abrir rodada jurídica só por ela), Aviso de Privacidade, avaliação RIPD. 2. **Smoke pós-deploy** — `NEXT_PUBLIC_SITE_URL` HTTPS, login `/campanha`, Praça de teste; checklist no AGENTS.md. 3. **Ativação com dados reais** assim que (1) liberar — lideranças/apoiadores reais e import em massa. 4. **Onboarding do time** — usuários `coordinator`/`advisor`, primeiras Praças assumidas, treino de campo. @@ -51,18 +51,18 @@ Feedback da coordenação (2026-07-20) invalidou o modelo de Núcleo: a campanha O discovery literatura→persona→entrevista ([relatório aprovado](research/relatorio-entrevista-persona-campanha.md); compêndio com ~67 fontes em [docs/research/](research/)) fixou o kernel: a disputa de DF é conta de quociente fragmentada; o gargalo é converter lealdade de campo em voto nominal praça a praça via rede; % estadual absoluto é anti-métrica. O produto deve entregar **inteligência, não planilha chique**: metas derivadas, leitura relativa, fila de decisão priorizada, sugestões dado→decisão com humano no loop, registro ex-ante auditável. Plano-mestre: [inteligencia-campanha.md](plans/inteligencia-campanha.md) (incl. gaps de dados G1–G10 e desenho canônico da fila). -| 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 | 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) | -| 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 | -| B13 | Escala relativa no mapa | [detalhes](plans/escala-relativa-mapa.md) | Quantis do candidato (default) + LQ + rank; símbolo proporcional por votos em jogo; % válidos mantido | B | ~2d | 3 | E8, E10 | -| E14 | Níveis de envolvimento N0–N4 | [detalhes](plans/niveis-de-envolvimento.md) | `engagementLevel` com histerese e sinais de reversão; staff-only (vocabulário duplo) | B | ~1d | 3 | C12 | -| E11 | Motor de sugestões v1 | [detalhes](plans/motor-de-sugestoes.md) | 8 padrões (P1/P2/P3/P5/P6/P7/K-A/K-B), menu com estatuto, aceitar/descartar → `allocationDecision`, triagem 1–5 | C | ~3d | 3 | E8, E9, E10, C12 | -| E12 | Camada TI | [detalhes](plans/camada-territorios-identidade.md) | Rollups com salvaguardas MAUP, benchmark intra-TI (gatilhos T no motor = fase 2 de E11) | B | ~1,5d | 3 | E8 | -| E13 | Planejador de presença/giros | [detalhes](plans/planejador-de-giros.md) | Elegibilidade (5 condições ✓/—), fases construção/consolidação/ativação, compositor de giro, visita pedida×justificada | C | ~1,5d | 3 | E8, C12 | -| E15 | Backtest pós-eleição | [detalhes](plans/backtest-pos-eleicao.md) | Pledge (trajetória) vs. resultado por zona; calibração de limiares | A | ~1d | pós-04/10 | C12, TSE 2026 | +| 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 | +| 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 | +| B13 | Escala relativa no mapa | [detalhes](plans/escala-relativa-mapa.md) | Quantis do candidato (default) + LQ + rank; símbolo proporcional por votos em jogo; % válidos mantido | B | ~2d | 3 | E8, E10 | +| E14 | Níveis de envolvimento N0–N4 | [detalhes](plans/niveis-de-envolvimento.md) | `engagementLevel` com histerese e sinais de reversão; staff-only (vocabulário duplo) | B | ~1d | 3 | C12 | +| E11 | Motor de sugestões v1 | [detalhes](plans/motor-de-sugestoes.md) | 8 padrões (P1/P2/P3/P5/P6/P7/K-A/K-B), menu com estatuto, aceitar/descartar → `allocationDecision`, triagem 1–5 | C | ~3d | 3 | E8, E9, E10, C12 | +| E12 | Camada TI | [detalhes](plans/camada-territorios-identidade.md) | Rollups com salvaguardas MAUP, benchmark intra-TI (gatilhos T no motor = fase 2 de E11) | B | ~1,5d | 3 | E8 | +| E13 | Planejador de presença/giros | [detalhes](plans/planejador-de-giros.md) | Elegibilidade (5 condições ✓/—), fases construção/consolidação/ativação, compositor de giro, visita pedida×justificada | C | ~1,5d | 3 | E8, C12 | +| E15 | Backtest pós-eleição | [detalhes](plans/backtest-pos-eleicao.md) | Pledge (trajetória) vs. resultado por zona; calibração de limiares | A | ~1d | pós-04/10 | C12, TSE 2026 | ```mermaid flowchart TD @@ -88,6 +88,8 @@ 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"] + R2 --> A10 R2 -.-> B7["B7 Mapa filtrado ✓"] B7 --> B12["B12 Aproximar mapa
ao filtro ✓"] R2 -.-> B8["B8 Polígonos Praças-zona
(SSA/CMS)"] @@ -95,10 +97,17 @@ flowchart TD R2 -.-> B11["B11 Escala % válidos
no mapa ✓"] A9 -.métrica 2026.-> B10 A9 -.numerador 2026.-> B11 + A10 -.métrica 2026 por cenário.-> B10 B10 -.hover densos.-> B6["B6 setStyle incremental ✓"] B11 -.troca escala.-> B6 JUR -.chave push.-> D2 C2prod --> C5["C5 GOTV (validar)"] + R2 --> D3["D3 WA canal
fundação"] + JUR -.chave WA canal.-> D3 + D3 --> D4["D4 WA envio 1:1"] + D3 --> D5["D5 WA → rascunhos"] + D4 -.thread.-> D5 + C2prod -.telefones reais.-> D3 subgraph Intel["Inteligência de campanha — discovery 2026-07-21"] E8i["E8 Conta da cadeira"] @@ -114,7 +123,9 @@ flowchart TD end R4 --> E8i + A10 --> E8i R5 --> C12i + A10 -.versions no group.-> C12i E8i --> E9i --> E11i C12i --> E11i C12i --> E14i @@ -131,23 +142,23 @@ flowchart TD E11i -.decisões acumuladas.-> E15i ``` -Paralelizáveis agora: fill-ins (O0+, RS+, …). ~~**A9** / **A9+** / **B9** / **B7** / **B10** / **B11** / **B6** / **B12** / **filtros-auto** / **C8** F4~~ entregues 2026-07-21. +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. ### 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; R6 critique/polish; Onda 0 jurídica em paralelo. +**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 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 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 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). +**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. -**Janela 4 — set → 04/10:** C5 GOTV _(validar)_, congelamento ~20/09 (só bugfix/dados). **E15** backtest pós-eleição (após 04/10). +**Janela 4 — set → 04/10:** C5 GOTV _(validar)_, congelamento ~20/09 (só bugfix/dados). **D4** envio 1:1 e **D5** inbox→rascunhos só se D3 estiver estável **antes** do congelamento (senão Adiar pós-04/10). **E15** backtest pós-eleição (após 04/10). ### 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); **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 — 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). -**Cortes seguros** (se o prazo apertar, nesta ordem): **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). +**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). ## Já entregue (resumo) @@ -185,12 +196,17 @@ Paralelizáveis agora: fill-ins (O0+, RS+, …). ~~**A9** / **A9+** / **B9** / * ## Próximos — sobreviventes e pós-remodelagem -- **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) +- **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** - **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) - **C5** operação dia D / GOTV _(validar com produto)_ · design [`Dia-D-GOTV`](design-refs/latest/Dia-D-GOTV.png) · depende de C2 dados reais - **D2** push + sino in-app · soft: chave `campanha-notificacoes-push` (Onda 0) · [plano](plans/notifications.md) +- **Programa WhatsApp interno (D3–D5)** · baixa/média prioridade · 1:1 staff↔lideranças via bridge **não oficial** · **número pessoal** de cada assessor/CG (QR no app, parece chat pessoal) · não Business API / não massa / não chip institucional · fecha Little Hire · plano-mestre [whatsapp-interno-campanha.md](plans/whatsapp-interno-campanha.md) + - **D3** fundação multi-sessão (QR por `campaignUser` + log + Consent `campanha-whatsapp-canal`) · [plano](plans/whatsapp-canal-fundacao.md) + - **D4** envio 1:1 pela sessão do ator · depende de D3 · [plano](plans/whatsapp-envio-liderancas.md) + - **D5** inbox da própria sessão → rascunhos (`plazaUpdate`/demanda) com humano no loop · depende de D3 · [plano](plans/whatsapp-sugestao-atualizacoes.md) - **R6** critique/polish visual da vertical remodelada (ciclo /impeccable completo por superfície; smoke visual coordenador feito em 2026-07-21) · gatilho: antes de 16/08 · (opcional: mover filtros acima do mapa) - **Fill-ins:** O0+ ([plano](plans/escala-dry-pos-onda0.md)) · RS+ auth read leve + shells de senha ([plano](plans/escala-dry-pos-reset-senha-perfil.md)) · higiene PascalCase @@ -229,7 +245,8 @@ Paralelizáveis agora: fill-ins (O0+, RS+, …). ~~**A9** / **A9+** / **B9** / * - Serviço Rust separado; self-host/Coolify enquanto Vercel atender; doações in-app - Geocodificação de seções eleitorais / Praça = seção (polígonos **aproximados** das 21 Praças-zona SSA/CMS = **B8**, sem seções) - PWA do site/`/admin`; PostGIS sem query espacial real -- WhatsApp Business API / disparo em massa; previsão estatística de votos neste ciclo +- WhatsApp Business API / disparo em massa / blast a apoiadores ou eleitores (Res. TSE 23.610 art. 33; Meta veda WABA político). **Exceto** o programa **D3–D5**: bridge não oficial **1:1 interno** staff↔lideranças já no CRM (ver [whatsapp-interno-campanha.md](plans/whatsapp-interno-campanha.md)) — risco ToS/operacional explícito nos planos; não é substituto de WABA +- Previsão estatística de votos neste ciclo - Import automático de planilhas de projeção (decisão E4 mantida: dados estratégicos via UI) ## Fontes