From a7e1a8f2e78f2b799b148d633240ded09dc92aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Sun, 2 Aug 2026 13:38:01 -0300 Subject: [PATCH] Drop internal WhatsApp bridge plans; keep wa.me only. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unofficial linked-device bridges risk banning staff phone numbers; D3–D5 stay wontfix and their plan files are removed. --- .cursor/rules/projects/nucleos-eleitorais.mdc | 4 +- docs/CHANGELOG-AGENTS.md | 2 + .../convite-whatsapp-lista-liderancas.md | 2 +- docs/plans/email-celular-lista-liderancas.md | 2 +- docs/plans/fluxos-acao-primeiro-inicio.md | 2 +- docs/plans/notifications.md | 2 +- docs/plans/whatsapp-canal-fundacao.md | 127 ------------------ docs/plans/whatsapp-envio-liderancas.md | 104 -------------- docs/plans/whatsapp-interno-campanha.md | 92 ------------- docs/plans/whatsapp-sugestao-atualizacoes.md | 113 ---------------- 10 files changed, 8 insertions(+), 442 deletions(-) delete mode 100644 docs/plans/whatsapp-canal-fundacao.md delete mode 100644 docs/plans/whatsapp-envio-liderancas.md delete mode 100644 docs/plans/whatsapp-interno-campanha.md delete mode 100644 docs/plans/whatsapp-sugestao-atualizacoes.md diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index 524f923e..5b4de23d 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -133,7 +133,7 @@ alwaysApply: false - **A10 entregue em código (2026-07-23; deploy pendente com remodelagem):** cenários pessimista/média/otimista em `votePledge.estimatedVotes` e `municipality.expectedVotes` (groups + migration `20260723_124200_add_vote_estimate_scenarios`, backfill scalar→`central`); agregação por cenário em `votePledgeData`/`municipalityMapData`/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):** `MunicipalityEstimateScenarioProvider` 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-municipalitylistoverview-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-municipalitymappanel-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). +- **WhatsApp interno D3–D5 descartado (2026-08-02):** Issues #30–#32 `wontfix`. Bridge não oficial (Baileys/Evolution/WAHA / linked device) arrisca ban do número pessoal do staff (ToS Meta + fingerprint de N números no IP de datacenter). Canal no produto = **só deep link `wa.me`** (convites, share, kit de apoiador). Planos `whatsapp-interno-campanha.md` / `whatsapp-canal-fundacao.md` / `whatsapp-envio-liderancas.md` / `whatsapp-sugestao-atualizacoes.md` removidos. - **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–E16 + B13 + C12** (+ adjacentes A11/E17 desde 2026-07-24; [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 (`municipalitiesByIbgeCode`); `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**. @@ -208,7 +208,7 @@ alwaysApply: false - Em 2026-07-23, **access explícito em toda collection (hardening Fase 0):** o default do Payload ("qualquer autenticado pode CRUD") é tratado como bug; toda collection declara `access` explícito — `users`/`signature`/`subscription`/`consent` só Payload admin (`payloadAdminOnly`), escrita de CMS admin-only com leitura pública preservada. JWTs de `campaignUser` não operam mais essas collections via `/api`. **Rejeitado:** confiar no default do Payload; RBAC só na camada de UI. - Em 2026-07-23, **padrões de engenharia vigentes (hardening Fases 1–6):** zero lint warnings (`--max-warnings=0`); `as never` banido (helpers tipados `hookFilledCreateData` / `stub` nos testes); knip no CI com files/dependencies em nível de erro; dead code se apaga imediatamente após qualquer mudança; convenção `lib/` (dado estático + lógica pura) vs `utilities/` (runtime Payload/Next); caching ladder (React `cache()` → `unstable_cache`+tag → artefato commitado). Regra sempre aplicada: [`.cursor/rules/engineering-standards.mdc`](../engineering-standards.mdc); débitos: [`docs/TECH-DEBT.md`](../../../docs/TECH-DEBT.md). - 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-08-02, **WhatsApp no produto = só `wa.me` (supersede D3–D5 de 2026-07-23):** bridge não oficial / QR / sidecar / número pessoal pareado descartados — risco de ban do chip do assessor/CG. Deep link `wa.me` (humano envia no app dele) permanece para convites, share e kit. **Rejeitado também:** WABA; disparo em massa (TSE art. 33); chatbot eleitoral. Issues #30–#32 `wontfix`. - 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`). diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 3b06b438..223925b1 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**Recently resolved (2026-08-02):** **WhatsApp interno D3–D5 descartado** — Issues #30–#32 `wontfix` (bridge Baileys/Evolution/WAHA / linked device): risco de ban do número pessoal do staff. Canal no produto = só `wa.me`. Planos `whatsapp-interno-campanha.md`, `whatsapp-canal-fundacao.md`, `whatsapp-envio-liderancas.md`, `whatsapp-sugestao-atualizacoes.md` removidos; refs em `notifications.md` / convite·email listas / fluxos-ação / regra `nucleos-eleitorais` atualizadas. + **Recently resolved (2026-08-02):** **Agent pool hard max → 12** — `POOL_HARD_MAX_SLOTS` de 8 → 12 (`scripts/lib/agent-pool-state.mjs`); default continua 5. Skill `agent-pool` alinhada. **Recently resolved (2026-08-01):** **B102** (Issue #129) — crash na busca do bottom drawer (focus/digitação): `TooltipProvider` do layout `(app)` envolvia só `{children}` e o drawer (irmão do scrollport) renderizava hits `priority:alta` via `CampaignHoverTooltip` sem provider. Fix: provider envolve `CampaignAppScrollChrome`; pins unit focus→suggest e type→search. Plano: [`crash-busca-bottom-drawer.md`](docs/plans/crash-busca-bottom-drawer.md). diff --git a/docs/plans/convite-whatsapp-lista-liderancas.md b/docs/plans/convite-whatsapp-lista-liderancas.md index cae7858b..35e52ab0 100644 --- a/docs/plans/convite-whatsapp-lista-liderancas.md +++ b/docs/plans/convite-whatsapp-lista-liderancas.md @@ -88,7 +88,7 @@ Componentes: - Convite de acesso ao app (`kind: 'login'`) na lista — só existe no detalhe hoje (condicionado a `supportStatus === 'engajado'`); o pedido do usuário é só o convite de completar cadastro. Se precisar, é extensão trivial do mesmo componente, mas fora deste item. - Disparo em massa / seleção múltipla de lideranças para convite em lote — vedado por Res. TSE 23.610 art. 33 e fora do desenho do produto (ver `docs/roadmap.md` § Fora de escopo); esta ação é sempre 1:1, uma linha por vez. - Editar/validar o telefone da liderança a partir da lista — isso é o formulário de edição existente (`/campanha/liderancas/[id]/editar` ou equivalente); esta ação só lê `contact.phone`. -- Programa WhatsApp interno D3–D5 (bridge de sessão, inbox) — este item usa o mesmo `wa.me` manual de sempre, não abre canal novo. +- Bridge WhatsApp / sessão pareada (D3–D5 descartados) — este item usa só `wa.me` 1:1. ## Rabbit holes diff --git a/docs/plans/email-celular-lista-liderancas.md b/docs/plans/email-celular-lista-liderancas.md index f4657a48..ae8177ec 100644 --- a/docs/plans/email-celular-lista-liderancas.md +++ b/docs/plans/email-celular-lista-liderancas.md @@ -111,7 +111,7 @@ Depth check: reusa `CampaignTable`, `phone` helpers, padrão visual do B19; extr - Editar contato na ficha interna / action `updateLeadershipContact` — Adiado. - Busca por e-mail/telefone; filtros novos; sort por coluna. - Painel de lideranças no município (`MunicipalityLeadershipsPanel`) / dossiê. -- Programa WhatsApp interno **D3–D5** (bridge) — aqui é só `wa.me` 1:1 como no B19. +- Bridge WhatsApp / sessão pareada (D3–D5 descartados) — aqui é só `wa.me` 1:1 como no B19. - Seletor de colunas (**B17**) — se existir, estas colunas entram como `defaultVisible` quando B17 chegar às lideranças (fora deste item). ## Rabbit holes diff --git a/docs/plans/fluxos-acao-primeiro-inicio.md b/docs/plans/fluxos-acao-primeiro-inicio.md index 6245a1a1..b3e08d18 100644 --- a/docs/plans/fluxos-acao-primeiro-inicio.md +++ b/docs/plans/fluxos-acao-primeiro-inicio.md @@ -245,5 +245,5 @@ Não competir com os botões. - Redesign completo do detalhe / abas - Remover o mapa - Substituir o sistema de listas -- App nativo / WhatsApp bridge (D3) +- App nativo / bridge WhatsApp (D3–D5 descartados — só `wa.me`) - Treinar o CG no IA atual diff --git a/docs/plans/notifications.md b/docs/plans/notifications.md index d98bdcb4..438951f9 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 (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. +Notificações por WhatsApp Business API ficam **fora** deste plano (Meta + Res. TSE 23.610 art. 33). Bridge WhatsApp não oficial (Baileys/Evolution/WAHA / linked device) também ficou **fora** — Issues D3–D5 (#30–#32) `wontfix`: risco de ban do número pessoal do staff. Canal WhatsApp no produto = só deep link `wa.me` (humano envia no app dele). ## Objetivos diff --git a/docs/plans/whatsapp-canal-fundacao.md b/docs/plans/whatsapp-canal-fundacao.md deleted file mode 100644 index b0337de1..00000000 --- a/docs/plans/whatsapp-canal-fundacao.md +++ /dev/null @@ -1,127 +0,0 @@ -# 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 dos municípios 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 município 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 deleted file mode 100644 index 9c051641..00000000 --- a/docs/plans/whatsapp-envio-liderancas.md +++ /dev/null @@ -1,104 +0,0 @@ -# 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 dos municípios 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 do município”. -- **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 deleted file mode 100644 index 7affd72c..00000000 --- a/docs/plans/whatsapp-interno-campanha.md +++ /dev/null @@ -1,92 +0,0 @@ -# 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 (`municipalityUpdate`, demanda, pledge) — alinhado a E11/C12. -- Fail-closed em Consent, access por papel/município, 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 `municipalityUpdate`/`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 (`private/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 deleted file mode 100644 index ff2e6c0d..00000000 --- a/docs/plans/whatsapp-sugestao-atualizacoes.md +++ /dev/null @@ -1,113 +0,0 @@ -# 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 `MunicipalityUpdateFeed` / forms `MunicipalityUpdateForm` · 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** (`municipalityUpdate` / 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 o município/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 do município 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 do município?” -- **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×município. -- **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: `municipalityUpdate` (`MunicipalityUpdateForm` / 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 município (se liderança tem 1 município; 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 (`municipalityUpdate` 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 aos municípios 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-`municipalityUpdate` 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`, `suggestMunicipalityUpdateFromMessage`, `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ó `municipalityUpdate` | update+demanda | update+demanda+voto. **Recomendação:** `municipalityUpdate` + `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["municipalityUpdate / campaignDemand
(+ pledge form)"] - In --> Match --> Draft --> Human --> Dom -``` - -Componentes: - -- **Collection ou table `whatsappInboundDraft`** (status `pending|accepted|discarded`, `suggestedKind`, `municipality`, `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 `MunicipalityUpdateForm` fields onde der (depth check). -- **Suave C12:** se sinais tipados em `municipalityUpdate` 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 município quando liderança tem N municípios.** 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 município”. -- **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 `municipalityUpdate`.** 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/MunicipalityUpdate.ts`, `src/components/campaign/MunicipalityUpdateForm.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