diff --git a/.cursor/rules/campanha-action-feedback.mdc b/.cursor/rules/campanha-action-feedback.mdc index a9bcd893..b195c6df 100644 --- a/.cursor/rules/campanha-action-feedback.mdc +++ b/.cursor/rules/campanha-action-feedback.mdc @@ -6,7 +6,7 @@ alwaysApply: false # Feel the action -Canonical principle: `PRODUCT.md` § Design Principles — **Feel the action**. Precedents: `ActionPlanFilters` / `PlazaFilters` / `SupporterFilters` (`useTransition` + `data-pending` + `aria-busy` + live region); form submits with `useActionState` pending + `Spinner`. +Canonical principle: `PRODUCT.md` § Design Principles — **Feel the action**. Precedents: `ActionPlanFilters` / `MunicipalityFilters` / `SupporterFilters` (`useTransition` + `data-pending` + `aria-busy` + live region); shared list pending via `CampaignListPendingBoundary` + `useCampaignListPending` (`CampaignListPending.tsx`, hardening 2026-07-23); form submits with `useActionState` pending + `Spinner`. When adding or changing an interactive control that triggers navigation, revalidation, or a server action: diff --git a/.cursor/rules/campanha-edit-where-you-see.mdc b/.cursor/rules/campanha-edit-where-you-see.mdc index e33b68af..bda62e75 100644 --- a/.cursor/rules/campanha-edit-where-you-see.mdc +++ b/.cursor/rules/campanha-edit-where-you-see.mdc @@ -1,12 +1,12 @@ --- description: /campanha staff UIs — edit mutable data in context (not read-only by default) -globs: src/app/(campaign)/**/*,src/components/campaign/**/*,src/utilities/campaign*/**,src/utilities/plaza*/**,src/utilities/votePledge* +globs: src/app/(campaign)/**/*,src/components/campaign/**/*,src/utilities/campaign*/**,src/utilities/municipality*/**,src/utilities/votePledge* alwaysApply: false --- # Edit where you see (`/campanha`) -Canonical principles: `PRODUCT.md` § Design Principles — **Edit where you see** + **Auto-save, no Save button**. Precedente: B9 (`PlazaList*Control` + `plazaStaffFormActions`). +Canonical principles: `PRODUCT.md` § Design Principles — **Edit where you see** + **Auto-save, no Save button**. Precedente: B9 (`MunicipalityList*Control` + `municipalityStaffFormActions`). When adding or changing a **staff** (coordinator / advisor) view that displays mutable campaign data: diff --git a/.cursor/rules/projects/nucleos-eleitorais.mdc b/.cursor/rules/projects/nucleos-eleitorais.mdc index 1ec389e8..0a8da487 100644 --- a/.cursor/rules/projects/nucleos-eleitorais.mdc +++ b/.cursor/rules/projects/nucleos-eleitorais.mdc @@ -1,5 +1,5 @@ --- -description: Estado do projeto Núcleos Eleitorais +description: Estado do projeto da vertical /campanha (Núcleos → Praças → Municípios) alwaysApply: false --- @@ -7,6 +7,8 @@ alwaysApply: false ## Status +- **DEPLOY EM PRODUÇÃO REALIZADO (2026-07-23).** A cadeia destrutiva da remodelagem (`20260721_020109_remodel_municipalities` → `20260723_200000_remodel_municipalities` + `20260723_202000_reconcile_municipality_remodel`) foi aplicada em produção pelos builds Vercel de 2026-07-23 (deploys production READY dos commits `a7b8f1b`/`c7a1b74`/`59868a9`). **Este item supersede toda menção "deploy pendente" nas entradas abaixo.** Pendências operacionais: smoke pós-deploy + onboarding do time (Onda 0 §2/§4 em [`docs/roadmap.md`](../../../docs/roadmap.md)); hold de PII real segue no lote jurídico. +- **Hardening de engenharia entregue e em produção (2026-07-23, Fases 0–6):** (0) access lockdown — `users`/`signature`/`subscription`/`consent` admin-only, escrita de CMS (`post`/`tag`/`media`/`petition`) admin-only, gates de rota do leader em `demandas*`/`planos*`, `createLeaderSupporter` com escopo correto, `candidate` tratado como staff irrestrito em todas as superfícies; (1) tooling gate — knip no CI (`.github/workflows/ci.yml`, mirror GitHub), `pnpm lint --max-warnings=0`, ban `as never` (helpers `hookFilledCreateData`/`stub`), tsconfig consolidado + `noUnusedLocals`, ~12 módulos mortos deletados; (2) caching — artefato TSE commitado (`pnpm build:election-aggregates` → `src/lib/electionAggregates/`, loader `bahiaElectionAggregates.ts`), `unstable_cache` com tag `election-tse` nos loaders de eleições, `loadMunicipalityScope` (React `cache()`) dedupe dashboard+mapa+lista; (3) pending honesto — bug de revalidação do toggle de tarefa, `CampaignListPendingBoundary` esmaecendo resultados, transição no compare do mapa, mapa do dashboard + abas do detalhe em Suspense; (4) state scoping — hover do mapa isolado em `MunicipalityMapSelection`, provider de cenário estreitado, `LeaderContactForm` extraído como ilha cliente; (5) estrutura — `campaignAccess` dividido em `src/utilities/access/*` (re-export preservado), `supporterImport.ts` separado, `runStaffEntityMutation` + `runCampaignRedirectFormAction` dedupando twins e 4 ladders de formActions, rename `PLAZA_*`→`MUNICIPALITY_*` concluído; (6) docs + regra `.cursor/rules/engineering-standards.mdc`. Tracker: [`docs/IMPROVE-CODE-QUALITY-PLAN.md`](../../../docs/IMPROVE-CODE-QUALITY-PLAN.md) · ledger: [`docs/TECH-DEBT.md`](../../../docs/TECH-DEBT.md) · mapa de testes: [`docs/TESTING.md`](../../../docs/TESTING.md). - **Remodelagem Municípios entregue em código (2026-07-23; deploy pendente — migrações `20260723_200000_remodel_municipalities` + `20260723_202000_reconcile_municipality_remodel` são destrutivas para a vertical campanha).** Decisões pós-reunião 2026-07-23: rename Praça→Município (`municipality`, `/campanha/municipios`); **435** entradas (Camaçari única; Salvador 19 zonas); role `candidate` (visão irrestrita); demandas staff-only; leader lockdown (ferramenta de contatos de apoiadores); dobradinhas (`stateDeputy`); mapa no Início (dashboard). Plano-mestre: [`remodelagem-municipios.md`](../../../docs/plans/remodelagem-municipios.md) (substitui `remodelagem-pracas.md`). - **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`. @@ -71,6 +73,8 @@ alwaysApply: false ## Decisões +- 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-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. @@ -142,5 +146,5 @@ alwaysApply: false - Em 2026-07-17 o roadmap foi repriorizado orientado ao calendário eleitoral (Res. TSE 23.760/2026: propaganda a partir de 16/08, 1º turno 04/10), com ordem de execução, trilhas paralelas (A dados eleitorais/território, B superfícies de coordenação, C operação de campo, D plataforma) e grafo de dependências. Decisões de sequenciamento: lançar o MVP antes da migração multi-município; multi-município antes de zonas-por-municipio/eventos/demandas; baseline TSE independente de zonas-por-municipio (dependência rebaixada a suave); WhatsApp Business API movido a fora de escopo (vedação Meta + Res. 23.610 art. 33). - Backlog consolidado do produto (admin RBAC, mapa, eventos, demandas, apoiadores, listas globais, site, white-label): [`docs/roadmap.md`](../../../docs/roadmap.md). - **B7 entregue (2026-07-21):** mapa em `/campanha/municipios` usa `buildMunicipalityListWhere` (lista/overview/mapa alinhados). Plano: [`docs/plans/mapa-pracas-filtrado.md`](../../../docs/plans/mapa-pracas-filtrado.md). Débito perf triplicado → **A9+**. -- **A9 / B6 / B9 / B7 / B10 / B11 / B8 F1 / filtros-auto entregues (2026-07-21; deploy pendente):** ver bullets no topo do Status. Próximos abertos Janela 1: **R6**, deploy+smoke remodelagem, **B12**; Janela 3: **B8 F2** (polígonos no mapa). +- **A9 / B6 / B9 / B7 / B10 / B11 / B8 F1 / filtros-auto entregues (2026-07-21; em produção desde 2026-07-23):** ver bullets no topo do Status. Próximos abertos: ver [`docs/roadmap.md`](../../../docs/roadmap.md) (compile 2026-07-24) — Janela 1: smoke pós-deploy + **R6** (+ **E8** em paralelo); Janela 3: **B8 F2** (polígonos das zonas de Salvador). - **B8 registrado (2026-07-21); F1 entregue (2026-07-21):** polígonos das Praças-zona Salvador/Camaçari. F1: catálogo `municipalityZoneNeighborhoods` + card na Praça. F2 pendente: dissolve malha IBGE → TopoJSON no mapa. Plano: [`docs/plans/poligonos-pracas-zona.md`](../../../docs/plans/poligonos-pracas-zona.md). diff --git a/AGENTS.md b/AGENTS.md index 8dcf3687..0a0673c0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,8 +8,8 @@ Teqo is the digital platform for deputado federal Jorge Solla (PT-BA): public si **Locked decisions — do not relitigate without a new concrete reason:** -- Single Next.js app, three route groups: `(frontend)` public site, `(payload)` admin, `(campaign)` internal campaign tool. No separate Rust service or frontend app for `/campanha`; it is modeled with Payload collections. The campaign roles are `coordinator` (label "Coordenador Geral"), `advisor` ("Assessor"), and `leader` ("Liderança"), all on the separate auth collection `campaignUser` (see "Campaign auth" below). An advisor sees and manages ONLY the Praças they administer. -- **2026-07-20 remodel (in progress): "Praça" replaces "Núcleo Eleitoral" as the operational unit.** Praças are PREDEFINED (seeded, geography read-only): one per Bahia municipality, except Salvador (19 zone Praças, ZE 1–19) and Camaçari (2 zone Praças, ZE 170/171); shared/leaky TSE zones are cut at the municipal boundary. Code identifier: `municipality`. Leaderships are one record per person (`contact` UNIQUE) linked to N municipalities and N organizations; vote pledges are per leadership×municipality with `declaredVotes` (leader-visible) vs `estimatedVotes` (staff-only, hidden from the leader). Campaign production data was reset by the remodel migration (public-site data untouched). Master plan: `docs/plans/remodelagem-pracas.md`. Sections below describing "nucleus" surfaces are being rewritten as the remodel lands. +- Single Next.js app, three route groups: `(frontend)` public site, `(payload)` admin, `(campaign)` internal campaign tool. No separate Rust service or frontend app for `/campanha`; it is modeled with Payload collections. The campaign roles are `coordinator` (label "Coordenador Geral"), `advisor` ("Assessor"), `leader` ("Liderança"), and `candidate` ("Candidato", unrestricted visibility), all on the separate auth collection `campaignUser` (see "Campaign auth" below). An advisor sees and manages ONLY the municipalities they administer; a leader is locked down to the supporter contact tool. +- **2026-07-23 remodel (DEPLOYED to production 2026-07-23): "Município" is the operational unit** (it replaced "Praça", which replaced "Núcleo Eleitoral"). Municípios are PREDEFINED (seeded, geography read-only): 435 rows — one per Bahia municipality (416), except Salvador (19 zone Municípios, ZE 1–19); Camaçari is a single whole municipality (ZE 170/171 aggregated); shared/leaky TSE zones are cut at the municipal boundary. Code identifier: `municipality`. Leaderships are one record per person (`contact` UNIQUE) linked to N municipalities and N organizations; vote pledges are per leadership×municipality with `declaredVotes` (staff-entered, leader-visible) vs `estimatedVotes` (three scenarios, staff-only, hidden from the leader). Campaign production data was reset by the remodel migrations (public-site data untouched). Master plan: `docs/plans/remodelagem-municipios.md` (supersedes `remodelagem-pracas.md`). Details: "Campaign Municípios model" below. - Hosting stays on Vercel for now. No self-host/Coolify migration in progress. - Donations are NOT handled in this app — they run through QueroApoiar (`apoiar.me/jorgesolla`, TSE-homologated). The site only needs a "Doar" link/CTA out to that URL. @@ -32,9 +32,9 @@ Teqo is the digital platform for deputado federal Jorge Solla (PT-BA): public si - `Consent.text` was reconciled from `varchar` to `jsonb` via `20260715_163500_consent_text_to_jsonb` (matching the `richText` field). Any future field-type change is a migration, not a manual DB edit. - To copy production content locally, use `pnpm db:pull` (`scripts/db-pull.mjs`): it only reads prod, only writes local, and excludes supporter PII (`contact`/`signature`/`subscription` row data). - To (re)populate news content, `pnpm db:seed:posts` fetches articles live from jorgesolla.com.br into the LOCAL db (same non-local `DATABASE_URL` guard as `pnpm dev`). It writes from a CLI process outside the deployed runtime, so after seeding — or after ANY direct-DB change — you must bust the production `posts` cache via `POST /api/revalidate`. See "Posts & Tags" below. -- To (re)populate the TSE 2022 election baseline, `pnpm db:seed:tse` downloads TSE open data and imports the Bahia scope into the LOCAL db (same guard; no revalidate needed — campaign pages are dynamic). See "Election baseline data (TSE 2022)" below. +- To (re)populate the TSE 2022 election baseline, `pnpm db:seed:tse` downloads TSE open data and imports the Bahia scope into the LOCAL db (same guard). Since the 2026-07-23 hardening, elections-tab reads are cached under the `election-tse` tag — after re-seeding a database the deployed app reads, bust it via `POST /api/revalidate?tag=election-tse`. See "Election baseline data (TSE 2022)" below. -## Known Gaps (as of 2026-07-18 — resolve before relying on this file for onboarding new devs) +## Known Gaps (as of 2026-07-24 — resolve before relying on this file for onboarding new devs) Backlog consolidado (bloqueadores, site, campanha, white-label): [`docs/roadmap.md`](docs/roadmap.md). @@ -108,7 +108,7 @@ curl -X POST "https:///api/revalidate?tag=global_privacy-policy" \ The internal `/campanha` area is gated by its own authentication, deliberately kept **isolated from the Payload admin (`/admin`)** so a campaign session and an admin session can coexist in the same browser. -- **`campaignUser` collection** (`src/collections/CampaignUser.ts`) — a separate Payload auth collection with `name`, optional contact `phone`, and required `role`: `geral`, `coordenador`, or `lideranca` (default and least privilege). Staff may sign in by email; leadership accounts use the normalized 11-digit phone as `username`. It is not `users`: `admin.user` remains `users`, so campaign users cannot log into `/admin`. +- **`campaignUser` collection** (`src/collections/CampaignUser.ts`) — a separate Payload auth collection with `name`, optional contact `phone`, and required `role`: `coordinator` ("Coordenador Geral"), `advisor` ("Assessor"), `candidate` ("Candidato"), or `leader` ("Liderança" — default and least privilege). Staff may sign in by email; leadership accounts use the normalized 11-digit phone as `username`. It is not `users`: `admin.user` remains `users`, so campaign users cannot log into `/admin`. - **Isolated session cookie `campaign-token`.** The flow sets its own httpOnly cookie named `campaign-token` (constant `CAMPAIGN_TOKEN_COOKIE` in `src/utilities/campaignAuth.ts`), scoped to `path: '/campanha'`, `sameSite: 'lax'`, and `secure` only in production. It is deliberately NOT the default `payload-token` cookie — using a distinct name + path is what lets a campaign session and a Payload admin session live side by side without clobbering each other. - **Session verification — `getCampaignUser()`** (`src/utilities/campaignAuth.ts`). Reads the `campaign-token` cookie, calls `payload.auth({ headers })`, accepts only `user.collection === 'campaignUser'`, then reloads that document by ID. The fresh read makes role downgrades effective immediately instead of trusting a stale JWT role. - **Login/logout server actions** (`src/app/(campaign)/campanha/actions/auth.ts`). `loginCampaign` validates input with `campaignLoginSchema` (`src/lib/schemas/campaign-login.ts`), calls `payload.login({ collection: 'campaignUser' })`, and — because the Local API returns a token WITHOUT setting a cookie — sets the `campaign-token` cookie itself (using the collection's `auth.tokenExpiration` as `maxAge`) before redirecting to `/campanha`. `logoutCampaign` clears the cookie (`maxAge: 0`) and redirects to `/campanha/login`. @@ -117,7 +117,7 @@ The internal `/campanha` area is gated by its own authentication, deliberately k ## Campaign Municípios model (2026-07-23 remodel) -`/campanha` is organized around `municipality` ("Município") — the campaign's PREDEFINED operational territory. Master plan: `docs/plans/remodelagem-municipios.md` (supersedes `docs/plans/remodelagem-pracas.md`). +`/campanha` is organized around `municipality` ("Município") — the campaign's PREDEFINED operational territory. Deployed to production on 2026-07-23 (destructive remodel migrations applied via the Vercel build). Master plan: `docs/plans/remodelagem-municipios.md` (supersedes `docs/plans/remodelagem-pracas.md`). - **Município definition:** 435 seeded rows — one per Bahia municipality (416), except Salvador (19 zone municipalities, ZE 1–19); Camaçari is a single whole municipality (ZE 170/171 aggregated). Canonical catalog: `src/lib/municipalityCatalog.ts` (`tests/fixtures/municipality-catalog.snapshot.json`). Seeded by migration `20260723_200000_remodel_municipalities` (+ reconcile `20260723_202000_reconcile_municipality_remodel`). Geography is admin-only; staff edit strategy fields and the coordinator assigns `advisors`. - **Collections (admin group `Campanha`):** `municipality`, `leadership`, `votePledge`, `organization`, `stateDeputy` (dobradinhas), `campaignDemand`, `municipalityUpdate`, `campaignInvite`, `supporter`, `actionPlan`. @@ -144,9 +144,9 @@ Nominal supporter registry backing the campaign's base of declared supporters (r Mobilization events / agenda backing the campaign's field operations (roadmap item C3; implemented and merged to `main` on 2026-07-18). One collection in admin group `Campanha`: -- **`actionPlan`** (`src/collections/ActionPlan.ts`) — a calendarizable action/event (caminhada, comício, etc.) anchored on ONE `municipality`, with task checklist, update feed and result record. Fields: `title`, canonical immutable `slug`, `kind` / `status` enums in pt-BR, `startAt` (required beyond `rascunho`), `municipality` (required) + free-text `locality`, `organizations` (hasMany — supporting orgs), `advisors` (staff responsible; ex-`coordinators`), `deputyPresent` (checkbox "Deputado presente"), `responsible` → `Contact`, `leadership` optional, `tasks`/`updates` arrays (derived fields server-side), and the action result: `resultSummary` + `resultMedia` (upload → `media`) + derived `resultRecordedBy/At` — queryable history for future activities in the same Praça. No `Consent` (internal staff data). +- **`actionPlan`** (`src/collections/ActionPlan.ts`) — a calendarizable action/event (caminhada, comício, etc.) anchored on ONE `municipality`, with task checklist, update feed and result record. Fields: `title`, canonical immutable `slug`, `kind` / `status` enums in pt-BR, `startAt` (required beyond `rascunho`), `municipality` (required) + free-text `locality`, `organizations` (hasMany — supporting orgs), `advisors` (staff responsible; ex-`coordinators`), `deputyPresent` (checkbox "Deputado presente"), `responsible` → `Contact`, `leadership` optional, `tasks`/`updates` arrays (derived fields server-side), and the action result: `resultSummary` + `resultMedia` (upload → `media`) + derived `resultRecordedBy/At` — queryable history for future activities in the same município. No `Consent` (internal staff data). - **Access.** `coordinator` everything; `advisor` where listed in `advisors` OR the plan's `municipality` is administered (may create, auto-included); `leader` has no access (lockdown). -- **UI** `/campanha/planos` — list with tabs Próximos (default) / Todos / Realizados / Rascunhos, filters (kind, municipality), cards with kind/status badge + date/time + Praça + "Resp:" + task progress + "Deputado presente"; detail with tabs Visão geral / Tarefas / Atualizações; result form appears for staff when the plan is `realizado`. +- **UI** `/campanha/planos` — list with tabs Próximos (default) / Todos / Realizados / Rascunhos, filters (kind, municipality), cards with kind/status badge + date/time + município + "Resp:" + task progress + "Deputado presente"; detail with tabs Visão geral / Tarefas / Atualizações; result form appears for staff when the plan is `realizado`. ## Campaign map geometries (B2) @@ -160,7 +160,7 @@ Static Bahia map foundation for future Leaflet surfaces (roadmap B2; UI is B3). ## Election baseline data (TSE 2022) -Public TSE 2022 results back the electoral baseline (roadmap items A3 + A4). Three collections in admin group `Dados Eleitorais`: `electionTally` (per city×zone totals: eligible voters, turnout, valid/blank/null votes), `electionCandidateVote` (nominal votes per candidate×city×zone), and `electionCandidate` (candidate registry with a cross-year `identityKey` hash — no CPF or voter ID is ever persisted). Read access requires an authenticated `campaignUser` or `users` account (`canReadElectionData`); writes are Payload-admin-only, and the import runs via CLI with `overrideAccess: true`. Since the Praça remodel, the consumers are `municipalityElectoralBaseline.ts` (per-municipality series/turnout), `municipalityCandidateComparison.ts` (multi-candidate table) and `municipalityMapData.ts` (map, incl. the diverging compare mode); ticket roles live in `BASELINE_TICKET_2022`. Seeds cover 2014/2018/2022 (E2 extended the original 2022-only A3 scope). +Public TSE 2022 results back the electoral baseline (roadmap items A3 + A4). Three collections in admin group `Dados Eleitorais`: `electionTally` (per city×zone totals: eligible voters, turnout, valid/blank/null votes), `electionCandidateVote` (nominal votes per candidate×city×zone), and `electionCandidate` (candidate registry with a cross-year `identityKey` hash — no CPF or voter ID is ever persisted). Read access requires an authenticated `campaignUser` or `users` account (`canReadElectionData`); writes are Payload-admin-only, and the import runs via CLI with `overrideAccess: true`. Since the Município remodel, the consumers are `municipalityElectoralBaseline.ts` (per-municipality series/turnout), `municipalityCandidateComparison.ts` (multi-candidate table) and `municipalityMapData.ts` (map, incl. the diverging compare mode); ticket roles live in `BASELINE_TICKET_2022`. Seeds cover 2014/2018/2022 (E2 extended the original 2022-only A3 scope). Two caching layers sit on top (2026-07-23 hardening): the immutable Solla/valid-votes series per municipality ships as a committed artifact — `pnpm build:election-aggregates` → `src/lib/electionAggregates/bahia-federal-baseline.json`, loader `src/lib/bahiaElectionAggregates.ts`, snapshot-tested against `municipalityCatalog` — so the map/dashboard never query these collections for historical years; and the elections-tab loaders (`municipalityElectoralBaseline`, `municipalityCandidateComparison`, `electionCandidateOptions`) are wrapped in `unstable_cache` under the `election-tse` tag (bust via `POST /api/revalidate?tag=election-tse` after re-seeding). `pnpm db:seed:tse` (`scripts/seed-tse-results.mjs`) downloads the TSE open-data zips (provenance URLs + SHA-256 in the script header), parses the Bahia scope, and imports via `payload.db.drizzle` in a transaction — idempotent by replace-per-scope `(year, office, turn)`, only `voteType: 'nominal'` rows in v1. It refuses a non-local `DATABASE_URL` (same guard family as the other seeds; override with `ALLOW_REMOTE_DB=true`) and caches downloads under `data/tse/` (gitignored). No revalidate step is needed: campaign pages are dynamic with auth, not ISR-cached. @@ -172,7 +172,7 @@ Public TSE 2022 results back the electoral baseline (roadmap items A3 + A4). Thr 4. Scan every edited runtime source file with Aikido and resolve new findings before handoff. Do not expand hardening work into unrelated pre-existing findings without an explicit scope change. 5. Review Vercel environment variables and migration SQL before deploy; `pnpm build` applies pending migrations to production automatically. 6. **Mandatory production blocker:** do not insert real leadership/support data or enable invites until electoral counsel documents the LGPD art. 11 basis, approves the specific versioned consent text, and an admin creates it with exactly `Consent.key = 'lideranca-autopreenchimento'`. The app intentionally fails closed when this key is absent. -7. **Praça remodel deploy note:** migration `20260721_020109_remodel_municipalities` is DESTRUCTIVE for the campaign vertical (drops nucleus-era tables, wipes campaign rows, converts `campaignUser.role` values, seeds 436 municipalities). Review its SQL before the first production build that includes it; public-site data is untouched. +7. **Remodel deploy (historical — applied to production 2026-07-23):** the destructive remodel chain (`20260721_020109_remodel_municipalities` → `20260723_200000_remodel_municipalities` + `20260723_202000_reconcile_municipality_remodel`) already ran in production via the Vercel build: campaign vertical reset, 435 municípios seeded, public-site data untouched. Treat any future destructive migration the same way — review its SQL before the first production build that includes it. ## Core Principles @@ -191,7 +191,7 @@ Public TSE 2022 results back the electoral baseline (roadmap items A3 + A4). Thr ## Project Structure -Generic Payload layout (aspirational — `access/` and `hooks/` as separate dirs don't exist yet in this repo; access control and hooks are currently written inline inside each collection/global file): +Actual layout (campaign access control lives in `src/utilities/access/*` domain modules with `src/utilities/campaignAccess.ts` as the re-export surface; hooks are still written inline inside each collection/global file): ``` src/ @@ -202,8 +202,8 @@ src/ ├── collections/ # Users, CampaignUser, CampaignInvite, Municipality, Leadership, VotePledge, MunicipalityUpdate, Organization, StateDeputy, CampaignDemand, Supporter, SupporterImportBatch, ActionPlan, ElectionTally, ElectionCandidateVote, ElectionCandidate, Media, Petition, Signature, Consent, Contact, Subscription, Post, Tag ├── globals/ # Global configs (SiteSettings, HomePage, Metadata) ├── components/ # Custom React components (campaign/*, ui from shadcn) -├── lib/ # cities.ts, bahiaTerritories.ts, bahiaTseZones.ts, bahiaMunicipalityCodes.ts, bahiaGeometries.ts, geometries/*.topo.json, territorySuggestions.ts, formData.ts, zod schemas (lib/schemas/*) -├── utilities/ # campaign*, nucleus*, posts, documents, globals, locks, etc. +├── lib/ # cities.ts, bahiaTerritories.ts, bahiaTseZones.ts, bahiaMunicipalityCodes.ts, bahiaGeometries.ts, geometries/*.topo.json, municipalityCatalog.ts, electionAggregates/ (committed TSE artifact) + bahiaElectionAggregates.ts, formData.ts, zod schemas (lib/schemas/*) +├── utilities/ # campaign*, access/* (per-domain RBAC; campaignAccess.ts re-exports), municipality*, posts, documents, globals, locks, etc. └── payload.config.ts # Main config ``` diff --git a/README.md b/README.md index e232d8d0..a7878c82 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Teqo then becomes a configurable base product for other politicians in Brazil: 6. Start the dev server: `pnpm dev` 7. Open: `http://localhost:3000` -Useful scripts: `pnpm db:start` / `pnpm db:stop` (local Postgres), `pnpm db:pull` (refresh local content from prod), `pnpm db:seed:posts` (import news posts/tags fresh from the live jorgesolla.com.br site into the local db; idempotent by slug, refuses a non-local database), `pnpm db:seed:tse` (import TSE 2022 Bahia election results into the local db; idempotent per year/office/turn scope, refuses a non-local database). +Useful scripts: `pnpm db:start` / `pnpm db:stop` (local Postgres), `pnpm db:pull` (refresh local content from prod), `pnpm db:seed:posts` (import news posts/tags fresh from the live jorgesolla.com.br site into the local db; idempotent by slug, refuses a non-local database), `pnpm db:seed:tse` (import TSE 2014/2018/2022 Bahia election results into the local db; idempotent per year/office/turn scope, refuses a non-local database), `pnpm build:election-aggregates` (regenerate the committed TSE aggregate artifact in `src/lib/electionAggregates/` from locally seeded data). ### Content & cache revalidation @@ -94,6 +94,8 @@ Then run: - `pnpm test:e2e` — Playwright (requires the test DB schema and a free port) - `pnpm test:all` — unit + integration + E2E +Quality gates (also enforced by CI on the GitHub mirror): `pnpm lint` (zero warnings — `--max-warnings=0`), `pnpm typecheck`, and `pnpm exec knip` (dead files/dependencies fail; delete what your change orphaned). Standards: `.cursor/rules/engineering-standards.mdc`. + ## Tech Stack - Payload CMS diff --git a/docs/TECH-DEBT.md b/docs/TECH-DEBT.md index 2d72224e..981e6749 100644 --- a/docs/TECH-DEBT.md +++ b/docs/TECH-DEBT.md @@ -10,31 +10,32 @@ Ledger of known engineering debt. Items large enough to be scheduled work become | knip loader noise: `Error loading src/payload.config.ts` (server-only guard) | knip run output | tooling noise | none (exit 0, static graph resolves) | S | P3 | open | | | Missing-consent window fixture releases its exclusive lease during the observation window — readers can interleave (rare flake). Full fix: session-level advisory lock spanning delete→op→restore + updated fault-path pins | `tests/helpers/testDatabaseLease.ts` (`withMissingInviteConsentFixture`) | test-infra race | low (ms-wide window; shared leases added 2026-07-23 narrow it) | M | P2 | open | | | `users` collection has no roles (all admins omnipotent) | `src/collections/Users.ts` | RBAC | medium (small trusted admin set) | M (schema migration) | P1 before widening `/admin` | open | AGENTS Known Gaps #1 | -| Any campaign user can enumerate staff names/roles (`canReadCampaignUsers`) | `src/utilities/campaignAccess.ts` | info disclosure | low | S | P2 | open | | +| Any campaign user can enumerate staff names/roles (`canReadCampaignUsers`) | `src/utilities/access/campaignUsers.ts` | info disclosure | low | S | P2 | open | | | `removeSupporterData` deletes via `overrideAccess: true` while `canDeleteSupporter` is admin-only | `src/app/(campaign)/campanha/actions/supporter.ts` | access asymmetry (likely intentional LGPD path) | low | S | P3 | open — document or align | | | Legacy public consent flows hardcode document IDs (`consent: 2`) | `src/app/(frontend)/actions/submitWhatsapp.ts` | correctness | medium | S | P1 | open | AGENTS Known Gaps #2 | | E2E suite thin (5 specs; most campaign CRUD int-only) | `tests/e2e/` | coverage | medium | L | P2 | open | | | CI runs on GitHub mirror only (no Codeberg/Woodpecker pipeline) | `.github/workflows/ci.yml` | process | low | M | P3 | open | | | E2E campaign journeys latency-flake on loaded local machines (5s expect / 30s test budgets vs cold dev compiles; suite green with CI retries) | `tests/e2e/campaignMunicipalities.e2e.spec.ts` | test-infra | low | S | P3 | open — consider per-step budgets or a warmed dist dir | | | Post listing cache holds all published posts, filters in memory | `src/utilities/posts.ts` | scale (fine at ~tens of posts) | low | M | P3 | open — watch corpus growth | | -| Petition `generateStaticParams` uses uncached `getDocuments` without `select`/`depth: 0` | `src/utilities/documents.ts` consumers | over-fetch (build-time) | low | S | P2 | planned (Phase 2) | | +| Petition `generateStaticParams` uses uncached `getDocuments` without `select`/`depth: 0` | `src/utilities/documents.ts` consumers | over-fetch (build-time) | low | S | P2 | closed 2026-07-23 (Phase 2: `getPetitionIds` with `depth: 0` + empty `select`) | | | Compare mode (`?compare=`) still queries per candidate uncached cross-request | `src/utilities/municipalityMapData.ts` | perf | low | M | P3 | open — React `cache()` dedups in-request; add `unstable_cache` if profiling warrants | | | Map bundle serializes all years × scenarios (~100KB raw) | `municipalityMapContract.ts` consumers | payload size | low — gzip crushes repeating keys to ~15-25KB on the wire; instant year/scenario switching is a core analysis flow worth the bytes | M | P3 | deferred 2026-07-23 with rationale (revisit only if RSC payload profiling says otherwise) | | | Props→state `useEffect` mirrors in list controls/comboboxes | `MunicipalityListExpectedVotesControl`, `AsyncSearchCombobox`, `StrictCombobox` | pattern | none — reviewed 2026-07-23: guarded-equality sync is the correct "server echo vs local draft" reconciliation (A10 pattern), not derived state | — | — | closed (accepted as correct) | | +| User-visible "Praça" copy survived the M1 rename (map panel title "Mapa das Praças", admin field descriptions, provisional Onda 0 consent texts, `SupportStatusBadge` summary, fallback/error strings, code comments) | `rg -n 'Praça' src` (~15 hits) | copy drift | low (cosmetic; consent texts are provisional Onda 0 copies) | S | P2 | open — found 2026-07-24 docs sync | | ## Smell Inventory -| Smell | Location | Refactoring | Status | -| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------- | ------------------- | -| God module: RBAC for every collection in one file (964 lines, ~80 exports) | `src/utilities/campaignAccess.ts` | split per domain under `src/utilities/access/` | planned (Phase 5) | -| God module: ~71 exports of electoral math | `src/lib/electionInsights.ts` | split by concern when next touched | ledgered | -| Multi-feature action bag: CRUD + CSV import pipeline + LGPD in one file (722 lines) | `src/app/(campaign)/campanha/actions/supporter.ts` | split CRUD vs import | planned (Phase 5) | -| Clone twins: create/update/staff-assert/unique-conflict skeleton | `actions/organization.ts` ↔ `actions/stateDeputy.ts` | extract shared factory | planned (Phase 5) | -| 13 near-identical `formActions.ts` try/catch→mapError→redirect ladders | `src/app/(campaign)/campanha/(app)/*/formActions.ts` | shared form-action wrapper | planned (Phase 5) | -| Flat 80+-file `src/utilities/` junk drawer; `lib/` vs `utilities/` boundary undocumented | `src/utilities/`, `src/lib/` | document convention + move misfiled modules | planned (Phase 5/6) | -| Praça→Município rename leftovers in identifiers (`PLAZA_MAP_YEARS`, `ACCESSIBLE_PLAZA_IDS_CONTEXT_KEY`, …) | map/list utilities | finish rename | planned (Phase 5) | -| Props→state `useEffect` mirrors | `MunicipalityListExpectedVotesControl`, comboboxes | key-remount or fully controlled | planned (Phase 4) | -| Detail-page RSCs mixing loading + tab UI + forms (300+ lines) | `municipios/[slug]/page.tsx`, `planos/[slug]/page.tsx` | extract tab loaders/sections | ledgered | +| Smell | Location | Refactoring | Status | +| ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| God module: RBAC for every collection in one file (964 lines, ~80 exports) | `src/utilities/campaignAccess.ts` | split per domain under `src/utilities/access/` | done 2026-07-23 (14 domain modules; `campaignAccess.ts` kept as re-export surface) | +| God module: ~71 exports of electoral math | `src/lib/electionInsights.ts` | split by concern when next touched | ledgered | +| Multi-feature action bag: CRUD + CSV import pipeline + LGPD in one file (722 lines) | `src/app/(campaign)/campanha/actions/supporter.ts` | split CRUD vs import | done 2026-07-23 (`supporterImport.ts`) | +| Clone twins: create/update/staff-assert/unique-conflict skeleton | `actions/organization.ts` ↔ `actions/stateDeputy.ts` | extract shared factory | done 2026-07-23 (`runStaffEntityMutation` in `campaignEntityActions.ts`) | +| 13 near-identical `formActions.ts` try/catch→mapError→redirect ladders | `src/app/(campaign)/campanha/(app)/*/formActions.ts` | shared form-action wrapper | partially done 2026-07-23 — 4 `*/nova` ladders on `runCampaignRedirectFormAction`; remainder tracked as C8 F4 ([plan](plans/escala-dry-pos-c6.md)) | +| Flat 80+-file `src/utilities/` junk drawer; `lib/` vs `utilities/` boundary undocumented | `src/utilities/`, `src/lib/` | document convention + move misfiled modules | done 2026-07-23 (convention in `.cursor/rules/engineering-standards.mdc`; `RefreshRouteOnSave` moved to `components/`) | +| Praça→Município rename leftovers in identifiers (`PLAZA_MAP_YEARS`, `ACCESSIBLE_PLAZA_IDS_CONTEXT_KEY`, …) | map/list utilities | finish rename | done 2026-07-23 | +| Props→state `useEffect` mirrors | `MunicipalityListExpectedVotesControl`, comboboxes | key-remount or fully controlled | closed 2026-07-23 (accepted as correct — see Debt Ledger row) | +| Detail-page RSCs mixing loading + tab UI + forms (300+ lines) | `municipios/[slug]/page.tsx`, `planos/[slug]/page.tsx` | extract tab loaders/sections | ledgered (tabs stream via Suspense since Phase 3; extraction still open) | ## Sprout / Wrap Register diff --git a/docs/TESTING.md b/docs/TESTING.md index b036969b..85d30e49 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -14,15 +14,15 @@ CI (`.github/workflows/ci.yml`) runs lint (zero warnings), typecheck, knip, unit Modules the 2026-07 hardening phases touch, and where their behavior is pinned: -| Module | Pinned behaviors | Test files | Gaps | -| ------ | ---------------- | ---------- | ---- | -| `campaignAccess.ts` (RBAC) | per-role municipality/leadership/pledge/supporter/demand/plan scoping; leader lockdown; staff field asymmetry | `campaignMunicipalityAccess`, `campaignVotePledge`, `campaignSupporter`, `campaignUserPhone`, `campaignActionPlan`, `campaignDemandWorkflow`, `collectionAccessLockdown` (int) | no dedicated per-export suite; covered through collection behavior | -| Collection access lockdown | campaign JWT denied on `users`/PII/CMS writes; anonymous read of public content; admin unaffected | `collectionAccessLockdown.int.spec.ts` | REST-transport variant untested (Local API with `overrideAccess: false` is equivalent) | -| `municipalityMapData.ts` | bundle scoping by URL filters + advisor access; `validVotesByYear` incl. 2026 reuse of 2022; null on empty scope | `municipalityMapData.int.spec.ts` | zone breakdown shape | -| `votePledgeData.ts` | `estimated[scenario] ?? declared` aggregation; role scoping; leader denial | `campaignVotePledge.int.spec.ts` | | -| Supporter import (token/bulk) | preview→confirm flow, dedup by phone, consent fail-closed | `campaignSupporter.int.spec.ts` | `supporterImportBulk`/`supporterImportToken` internals only via flow | -| Leader supporter create | linked-municipality scope; out-of-scope rejection; staff rejection | `collectionAccessLockdown.int.spec.ts` | | -| Posts visibility/caching | `isPostVisible` fail-closed; cached listing | (indirect via routes) | no dedicated `posts` spec — backlog | +| Module | Pinned behaviors | Test files | Gaps | +| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| RBAC — `src/utilities/access/*` (split 2026-07-23; `campaignAccess.ts` is the re-export surface) | per-role municipality/leadership/pledge/supporter/demand/plan scoping; leader lockdown; staff field asymmetry | `campaignMunicipalityAccess`, `campaignVotePledge`, `campaignSupporter`, `campaignUserPhone`, `campaignActionPlan`, `campaignDemandWorkflow`, `collectionAccessLockdown` (int) | no dedicated per-export suite; covered through collection behavior | +| Collection access lockdown | campaign JWT denied on `users`/PII/CMS writes; anonymous read of public content; admin unaffected | `collectionAccessLockdown.int.spec.ts` | REST-transport variant untested (Local API with `overrideAccess: false` is equivalent) | +| `municipalityMapData.ts` | bundle scoping by URL filters + advisor access; `validVotesByYear` incl. 2026 reuse of 2022; null on empty scope | `municipalityMapData.int.spec.ts` | zone breakdown shape | +| `votePledgeData.ts` | `estimated[scenario] ?? declared` aggregation; role scoping; leader denial | `campaignVotePledge.int.spec.ts` | | +| Supporter import (token/bulk) | preview→confirm flow, dedup by phone, consent fail-closed | `campaignSupporter.int.spec.ts` | `supporterImportBulk`/`supporterImportToken` internals only via flow | +| Leader supporter create | linked-municipality scope; out-of-scope rejection; staff rejection | `collectionAccessLockdown.int.spec.ts` | | +| Posts visibility/caching | `isPostVisible` fail-closed; cached listing | (indirect via routes) | no dedicated `posts` spec — backlog | ## Characterization Backlog diff --git a/docs/plans/aproximar-mapa-pracas.md b/docs/plans/aproximar-mapa-pracas.md index 145aa4f7..410d054d 100644 --- a/docs/plans/aproximar-mapa-pracas.md +++ b/docs/plans/aproximar-mapa-pracas.md @@ -1,6 +1,8 @@ # Aproximar o Mapa das Praças à região filtrada -Status: entregue 2026-07-21 (branch; deploy pendente com remodelagem) +> **Nota (2026-07-24):** entregue na era Praças e **em produção desde 2026-07-23**. A remodelagem M1 renomeou os identificadores `plaza*` → `municipality*` e as rotas `pracas/` → `municipios/`; os nomes neste plano são os da época. Nomes atuais: ver [remodelagem-municipios.md](remodelagem-municipios.md). + +Status: entregue 2026-07-21 (em produção desde 2026-07-23) Atualizado em: 2026-07-21 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B12) Impeccable: B — encaixe em `PlazaMapPanel` / `BahiaMap` (sem rota nova) diff --git a/docs/plans/backtest-pos-eleicao.md b/docs/plans/backtest-pos-eleicao.md index d0846a11..177f322e 100644 --- a/docs/plans/backtest-pos-eleicao.md +++ b/docs/plans/backtest-pos-eleicao.md @@ -1,7 +1,7 @@ # E15 — Backtest pós-eleição (pledge vs. resultado; calibração de limiares) Status: rascunho -Atualizado em: 2026-07-21 +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening) Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", E15; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md)) Impeccable: A — N/A (sem superfície UI de produto neste ciclo; saída é relatório/dado) Appetite: ~1 dia eng; janela pós-04/10 (sem pressa, sem migration própria) @@ -15,11 +15,11 @@ Responsável: — - Import do resultado 2026 (extensão do seed TSE existente para `year=2026`, mesmo formato `electionTally`/`electionCandidateVote`). - **Análises do backtest** (script/utility, saída em markdown versionado em `docs/research/` como apêndice do ciclo): - 1. Pledge → voto: por praça, trajetória de comprometido (fotos em 15/08, 15/09, véspera) vs. nominal realizado; fator de conversão global e por assessor/liderança-fonte (com o cuidado anti-punição de K-C/G4 — leitura agregada, não ranking individual público). - 2. Limiares: quais cortes de LQ/captura teriam separado ex-ante as praças onde o candidato cresceu (calibra E10/E14 para 2030). - 3. Decisões: `allocationDecision` aceitas/descartadas vs. desfecho da praça — o motor acertou onde? a leitura alternativa descartada era a certa? + 1. Pledge → voto: por município, trajetória de comprometido (fotos em 15/08, 15/09, véspera) vs. nominal realizado; fator de conversão global e por assessor/liderança-fonte (com o cuidado anti-punição de K-C/G4 — leitura agregada, não ranking individual público). + 2. Limiares: quais cortes de LQ/captura teriam separado ex-ante os municípios onde o candidato cresceu (calibra E10/E14 para 2030). + 3. Decisões: `allocationDecision` aceitas/descartadas vs. desfecho do município — o motor acertou onde? a leitura alternativa descartada era a certa? 4. Sinais: sinal registrado (invasão/esfriamento) previu queda? lead time médio. - 5. Roll-off: praças I-A trabalhadas vs. não trabalhadas — o diferencial se moveu? + 5. Roll-off: municípios I-A trabalhados vs. não trabalhados — o diferencial se moveu? - Tudo com o estatuto honesto: n=1 eleição, sem contrafactual — é calibração, não prova causal. ## Decisões travadas diff --git a/docs/plans/cadastro-nominal-apoiadores.md b/docs/plans/cadastro-nominal-apoiadores.md index a4b26acd..edc531bb 100644 --- a/docs/plans/cadastro-nominal-apoiadores.md +++ b/docs/plans/cadastro-nominal-apoiadores.md @@ -1,6 +1,6 @@ # Cadastro nominal de apoiadores em massa -Status: engenharia pronta e mesclada em `main` (pendente deploy + lote jurídico) +Status: engenharia pronta, mesclada e em produção (deploy 2026-07-23); dados reais de apoiadores em **hold até o lote jurídico** (Onda 0 — chaves `apoiador-cadastro` / `apoiador-intencao-voto`) Atualizado em: 2026-07-18 Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Campanha → Próximos ciclos") Responsável: — diff --git a/docs/plans/camada-territorios-identidade.md b/docs/plans/camada-territorios-identidade.md index 6597b9ce..8941616a 100644 --- a/docs/plans/camada-territorios-identidade.md +++ b/docs/plans/camada-territorios-identidade.md @@ -1,33 +1,33 @@ # E12 — Camada Territórios de Identidade (rollups regionais com salvaguardas MAUP) Status: rascunho -Atualizado em: 2026-07-21 +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening) Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", E12; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md)) -Impeccable: B — agrupamento/rollup nas superfícies existentes (lista/overview, detalhe da Praça, mapa TI opcional); sem rota nova -Appetite: ~1,5 dia eng; sem migration (mapeamento praça→TI é estático) +Impeccable: B — agrupamento/rollup nas superfícies existentes (lista/overview, detalhe do município, mapa TI opcional); sem rota nova +Appetite: ~1,5 dia eng; sem migration (mapeamento município→TI é estático) Responsável: — ## Design (Impeccable) -Âncoras: `PRODUCT.md` (princípio 5) / `DESIGN.md` (register `product`) · `PlazaListOverview`, geometria `bahia-identity-territories.topo.json` (B2). +Âncoras: `PRODUCT.md` (princípio 5) / `DESIGN.md` (register `product`) · `MunicipalityListOverview`, geometria `bahia-identity-territories.topo.json` (B2). Na implementação: craft compacto → critique → polish. - **Persona / contexto:** coordenador dividindo carteiras de assessores e preparando giro; "TI é a língua que o governo e o campo já falam". -- **Job principal:** ler o território em 27 unidades sem que a média regional esconda a praça crítica. +- **Job principal:** ler o território em 27 unidades sem que a média regional esconda o município crítico. - **Estratégia de cor:** Restrained; rollup nunca aparece sem a decomposição a um clique. -- **Edit where you see:** não neste item (rollup é leitura; carteiras de assessor continuam por praça). -- **Anti-goals:** média regional decidindo alocação de praça (anti-goal 9); TI Metropolitano como uma linha entre 27; segundo agrupamento além de TI (Imediatas ficam adiadas). +- **Edit where you see:** não neste item (rollup é leitura; carteiras de assessor continuam por município). +- **Anti-goals:** média regional decidindo alocação de município (anti-goal 9); TI Metropolitano como uma linha entre 27; segundo agrupamento além de TI (Imediatas ficam adiadas). ## Contexto -Relatório §6.5: TI é a camada default de coordenação (decisões sobre gente e logística regional), com salvaguardas MAUP obrigatórias — razão dos agregados (nunca média das razões), agregado acompanhado de mediana+amplitude+praça crítica nomeada, Metropolitano sempre decomposto, malha congelada por ciclo, "praça manda no voto, TI manda na logística". Análises que só estabilizam no TI: balanço de portfólio, gap regional de captura, leitura da majoritária por TI, benchmark intra-TI (T4), sanity de metas. O repo já tem `bahiaTerritories.ts` (município→TI oficial) e a geometria dos 27 TIs (B2); `plaza.region` já existe no catálogo. +Relatório §6.5: TI é a camada default de coordenação (decisões sobre gente e logística regional), com salvaguardas MAUP obrigatórias — razão dos agregados (nunca média das razões), agregado acompanhado de mediana+amplitude+município crítico nomeado, Metropolitano sempre decomposto, malha congelada por ciclo, "praça manda no voto, TI manda na logística". Análises que só estabilizam no TI: balanço de portfólio, gap regional de captura, leitura da majoritária por TI, benchmark intra-TI (T4), sanity de metas. O repo já tem `bahiaTerritories.ts` (município→TI oficial) e a geometria dos 27 TIs (B2); `municipality.region` já existe no catálogo. ## Objetivos -- **Rollup por TI** sobre o bundle existente: votos em jogo, meta/cobertura (Σ), captura regional (Σ nominais ÷ Σ teto — razão dos agregados), nº de praças por classe/nível, praça crítica (pior déficit/frescor) nomeada, mediana e amplitude da captura. -- **Agrupamento na lista:** modo "por Território" na lista de Praças (grupos colapsáveis com o rollup no header do grupo) — mesma URL/filtros. -- **Benchmark intra-TI (T4):** no detalhe da Praça, comparação com a mediana do TI ("captura 2,1× a mediana do seu território") + praça-farol nomeada. +- **Rollup por TI** sobre o bundle existente: votos em jogo, meta/cobertura (Σ), captura regional (Σ nominais ÷ Σ teto — razão dos agregados), nº de municípios por classe/nível, município crítico (pior déficit/frescor) nomeado, mediana e amplitude da captura. +- **Agrupamento na lista:** modo "por Território" na lista de municípios (grupos colapsáveis com o rollup no header do grupo) — mesma URL/filtros. +- **Benchmark intra-TI (T4):** no detalhe do município, comparação com a mediana do TI ("captura 2,1× a mediana do seu território") + município-farol nomeado. - **Metropolitano decomposto:** o grupo "Metropolitano de Salvador" sempre expande zonas SSA + municípios RMS; nunca agrega num número único sem breakdown visível. - **Malha congelada:** constante de versão da composição TI usada no ciclo (já é estática em `bahiaTerritories.ts` — documentar como frozen 2026). - Opcional barato (se couber no appetite): modo TI no mapa usando a geometria B2 (fills por rollup, mesmas escalas B13). @@ -36,7 +36,7 @@ Relatório §6.5: TI é a camada default de coordenação (decisões sobre gente - **TI como única camada regional do produto neste ciclo** (default operacional — relatório H4). **Rejeitado:** Regiões Imediatas IBGE como camada paralela (teste de sensibilidade fica manual/ad-hoc — G10 adiado); cluster empírico como camada operacional (instável e ilegível; vira exercício de auditoria única fora do produto). - **Razão dos agregados, nunca média das razões** — regra de implementação com teste unit dedicado (divergência entre as duas contas exposta como alarme de heterogeneidade). **Rejeitado:** médias simples (a média que mente — §6.5). -- **i18n e naming:** `territoryRollup`, `computeTerritoryRollups`, `criticalPlaza`, `medianCapture`; labels pt-BR ("Território", "Praça crítica"). +- **i18n e naming:** `territoryRollup`, `computeTerritoryRollups`, `criticalMunicipality`, `medianCapture`; labels pt-BR ("Território", "Município crítico"). ## Questões em aberto @@ -47,10 +47,10 @@ Relatório §6.5: TI é a camada default de coordenação (decisões sobre gente ```mermaid flowchart LR - Bundle["loadPlazaListPageBundle"] + Bundle["loadMunicipalityListPageBundle"] Terr["bahiaTerritories.ts
(município→TI, frozen 2026)"] Roll["computeTerritoryRollups
(Σ, mediana, amplitude, crítica)"] - List["PlazaList agrupada + headers de grupo"] + List["MunicipalityList agrupada + headers de grupo"] Detail["benchmark intra-TI no detalhe"] Map["(opcional) fills TI no BahiaMap"] Bundle --> Roll @@ -63,24 +63,24 @@ flowchart LR Componentes: - **`src/utilities/territoryRollup.ts`**: rollup puro sobre as linhas do bundle + derivados E8; salvaguardas como invariantes testadas (razão de agregados, Metropolitano flagged). -- **`PlazaList.tsx`**: modo de agrupamento (param `?group=territorio`) com headers de grupo; `PlazaListOverview` ganha corte por TI quando agrupado. -- **Detalhe da Praça:** linha de benchmark no card de baseline (reusa `PlazaBaselineCard`). +- **`MunicipalityList.tsx`**: modo de agrupamento (param `?group=territorio`) com headers de grupo; `MunicipalityListOverview` ganha corte por TI quando agrupado. +- **Detalhe do município:** linha de benchmark no card de baseline (reusa `MunicipalityBaselineCard`). - **Mapa (opcional):** `bahiaTerritoryGeometries.ts`/`getTerritoryFeature` já existem; modo TI colore os 27 polígonos pelo rollup. - **Sem migration.** ## Dependências -- Dura: **E8** (captura/meta/cobertura por praça para agregar). Suaves: E9 (agrupamento compõe com as colunas da fila), E14 (nível no rollup), B13 (escalas do mapa TI). +- Dura: **E8** (captura/meta/cobertura por município para agregar). Suaves: E9 (agrupamento compõe com as colunas da fila), E14 (nível no rollup), B13 (escalas do mapa TI). - Reusa: `bahiaTerritories.ts`, `bahiaTerritoryGeometries.ts`/`bahiaGeometries.ts` (B2), bundle A9+. ## Não escopo -- Gatilhos T1–T5 como sugestões (E11 fase 2); malha Regiões Imediatas (G10 — adiado no plano-mestre); carteiras de assessor como entidade (continua `plaza.advisors`); qualquer edição em nível TI. +- Gatilhos T1–T5 como sugestões (E11 fase 2); malha Regiões Imediatas (G10 — adiado no plano-mestre); carteiras de assessor como entidade (continua `municipality.advisors`); qualquer edição em nível TI. ## Rabbit holes - **Rollup virar segunda página de analytics.** É um agrupamento da lista + números no header — não um dashboard regional novo. -- **Zonas de Salvador no rollup do Metropolitano.** As 19 Praças-zona têm `region` = Metropolitano; agregar por soma funciona, mas o breakdown obrigatório precisa listar zonas E municípios RMS separados — não "resolver" juntando tudo. +- **Zonas de Salvador no rollup do Metropolitano.** As 19 Municípios-zona têm `region` = Metropolitano; agregar por soma funciona, mas o breakdown obrigatório precisa listar zonas E municípios RMS separados — não "resolver" juntando tudo. - **Comparação entre TIs virar ranking público.** Mesmo risco de gaming do benchmark (T4-contraindicação): benchmark é para aprender mecanismo, não régua punitiva — copy da UI reflete isso. ## Adiado com gatilho @@ -93,5 +93,5 @@ Componentes: - `docs/roadmap.md` (Inteligência de campanha, E12) · [plano-mestre](inteligencia-campanha.md) - `docs/research/relatorio-entrevista-persona-campanha.md` §6.5 (veredito TI, análises, T1–T5, salvaguardas MAUP) - `src/lib/bahiaTerritories.ts`, `src/lib/bahiaTerritoryGeometries.ts`, `src/lib/geometries/bahia-identity-territories.topo.json` (B2) -- `src/components/campaign/PlazaList.tsx`, `PlazaListOverview.tsx`, `PlazaBaselineCard.tsx`, `src/utilities/plazaPageData.ts` +- `src/components/campaign/MunicipalityList.tsx`, `MunicipalityListOverview.tsx`, `MunicipalityBaselineCard.tsx`, `src/utilities/municipalityPageData.ts` - AGENTS.md — B2 as-built, naming diff --git a/docs/plans/cenarios-estimativa-votos.md b/docs/plans/cenarios-estimativa-votos.md index 8a63dca5..beea3167 100644 --- a/docs/plans/cenarios-estimativa-votos.md +++ b/docs/plans/cenarios-estimativa-votos.md @@ -1,11 +1,13 @@ # Cenários de estimativa de votos (pessimista / média / otimista) -Status: entregue (2026-07-23) -Atualizado em: 2026-07-23 _(entrega A10 + polish overview/Cenário; capture-review-debts: no-op roadmap, gatilho flash)_ -Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha A, item A10) -Impeccable: B — encaixe em forms/lista/mapa/dashboard já existentes (sem rota nova) -Appetite: ~2 dias eng; migration (pledge + praça) + helpers de agregação + seletor de cenário nas superfícies staff -Responsável: — +Status: entregue (2026-07-23; em produção desde 2026-07-23) +Atualizado em: 2026-07-24 _(entrega A10 + polish overview/Cenário; capture-review-debts: no-op roadmap, gatilho flash)_ + +> **Nota 2026-07-24:** identificadores `plaza*` citados abaixo foram renomeados para `municipality*` na remodelagem M1 (ex.: `PlazaListOverview` → `MunicipalityListOverview`, `PlazaMapPanel` → `MunicipalityMapPanel`, `PlazaList*Control` → `MunicipalityList*Control`); os nomes no corpo são os da época da entrega. +> Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha A, item A10) +> Impeccable: B — encaixe em forms/lista/mapa/dashboard já existentes (sem rota nova) +> Appetite: ~2 dias eng; migration (pledge + praça) + helpers de agregação + seletor de cenário nas superfícies staff +> Responsável: — ## Design (Impeccable) @@ -132,7 +134,7 @@ Componentes: - **Nota por cenário** (hoje uma `estimateNote`). Revisitar se assessores pedirem justificar só o pessimista vs. o otimista. - **Aviso soft vs `voteGoals.minimum` no cenário pessimista.** Revisitar após E8 com totais preenchidos (mesmo gatilho de A9). - **Delta semanal por cenário.** Revisitar com C12 (versions no group). -- **Abstração compartilhada de flash/pending client-side** (hoje local em `PlazaListOverview`). Revisitar quando houver o **2º** flash client-side em `/campanha` (não URL/`useTransition`). +- **Abstração compartilhada de flash/pending client-side** (hoje local em `MunicipalityListOverview`). Revisitar quando houver o **2º** flash client-side em `/campanha` (não URL/`useTransition` — o `CampaignListPendingBoundary` do hardening cobre pending de navegação, não este flash de eco do servidor). ## Referências diff --git a/docs/plans/classificacao-territorial-relativa.md b/docs/plans/classificacao-territorial-relativa.md index 1544bddb..767c033d 100644 --- a/docs/plans/classificacao-territorial-relativa.md +++ b/docs/plans/classificacao-territorial-relativa.md @@ -1,9 +1,9 @@ # E10 — Classificação territorial relativa (defesa/ataque para DF) Status: rascunho -Atualizado em: 2026-07-21 +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening) Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", E10; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md)) -Impeccable: B — muda o conteúdo de cards/badges existentes (insights da Praça, overview da lista); sem rota nova +Impeccable: B — muda o conteúdo de cards/badges existentes (insights do município, overview da lista); sem rota nova Appetite: ~1 dia eng; sem migration (derivação em leitura, limiares versionados em lib) Responsável: — @@ -13,7 +13,7 @@ Responsável: — Na implementação: craft compacto → critique → polish. -- **Persona / contexto:** assessor lendo a Praça; coordenador filtrando a lista por classe. +- **Persona / contexto:** assessor lendo o município; coordenador filtrando a lista por classe. - **Job principal:** a classe transforma número em verbo (defender/atacar/consolidar/minimizar) sem mentir na aritmética de DF. - **Estratégia de cor:** manter o mapeamento de badges já usado (defesa verde, ataque contorno vermelho, indecisa âmbar, perdida cinza — precedente A5-2). - **Edit where you see:** não — classe é sugestão automática; discordância do coordenador vira nível/nota (E14), não override do cálculo. @@ -21,15 +21,15 @@ Na implementação: craft compacto → critique → polish. ## Contexto -Os limiares herdados de A5-2 (`TERRITORIAL_DEFESA_MIN = 0.35` etc. em `src/lib/electionInsights.ts`) foram calibrados para leitura majoritária. O relatório (D4) mostra a degeneração em DF: share local típico 1–5% → o estado inteiro vira "perdida". Prescrição: **âncoras relativas** — múltiplos do share estadual do próprio candidato (LQ ≥ 2–3 = defesa; ~1 = padrão; < 0,5 = fraqueza) e/ou múltiplos do share da cadeira marginal — e classificação **multi-eixo**: dominância + importância da praça na própria votação (concentração) + competição local + teto do campo. Cortes exatos são calibráveis por backtest (E15); aqui entra o método com valores iniciais versionados. +Os limiares herdados de A5-2 (`TERRITORIAL_DEFESA_MIN = 0.35` etc. em `src/lib/electionInsights.ts`) foram calibrados para leitura majoritária. O relatório (D4) mostra a degeneração em DF: share local típico 1–5% → o estado inteiro vira "perdida". Prescrição: **âncoras relativas** — múltiplos do share estadual do próprio candidato (LQ ≥ 2–3 = defesa; ~1 = padrão; < 0,5 = fraqueza) e/ou múltiplos do share da cadeira marginal — e classificação **multi-eixo**: dominância + importância do município na própria votação (concentração) + competição local + teto do campo. Cortes exatos são calibráveis por backtest (E15); aqui entra o método com valores iniciais versionados. ## Objetivos -- Novo classificador em `electionInsights.ts` (ou módulo irmão `dfTerritorialClass.ts`): entrada = dominância, LQ, share da praça na votação própria, captura vs. teto do campo, NEC/desequilíbrio local; saída = classe + fatores ("por quê"). +- Novo classificador em `electionInsights.ts` (ou módulo irmão `dfTerritorialClass.ts`): entrada = dominância, LQ, share do município na votação própria, captura vs. teto do campo, NEC/desequilíbrio local; saída = classe + fatores ("por quê"). - Limiares versionados como constantes nomeadas com comentário de estatuto (ilustrativos, calibráveis por E15) — mesmos 4 rótulos operacionais existentes. -- Substituição nos consumidores: card de insights da Praça, distribuição no overview da lista, filtro por classe (se existente) e cor de classe onde o mapa a usar (B13). -- "Por quê" exposto: a UI mostra os 2 fatores dominantes ("LQ 2,8 · praça é 6% da sua votação"), não só o rótulo — antídoto do excesso de confiança (relatório §6.4). -- Testes unit com fixtures das 4 classes + casos de borda (praça sem baseline, válidos 0 — reusar branches `semBaseline` existentes). +- Substituição nos consumidores: card de insights do município, distribuição no overview da lista, filtro por classe (se existente) e cor de classe onde o mapa a usar (B13). +- "Por quê" exposto: a UI mostra os 2 fatores dominantes ("LQ 2,8 · município é 6% da sua votação"), não só o rótulo — antídoto do excesso de confiança (relatório §6.4). +- Testes unit com fixtures das 4 classes + casos de borda (município sem baseline, válidos 0 — reusar branches `semBaseline` existentes). ## Decisões travadas @@ -39,17 +39,17 @@ Os limiares herdados de A5-2 (`TERRITORIAL_DEFESA_MIN = 0.35` etc. em `src/lib/e ## Questões em aberto -- **Manter o classificador antigo para leituras majoritárias?** Opções: substituir tudo | manter ambos com seletor de contexto. **Recomendação:** substituir nos consumidores de Praça (o produto é DF); manter as constantes antigas exportadas até B13 estabilizar, depois remover. -- **NEC/desequilíbrio entram na v1 do classificador?** **Recomendação:** sim como fator qualificador de "ataque" (disputa aberta), com fallback neutro quando o cálculo por praça não estiver disponível — não bloquear a entrega pelos índices. +- **Manter o classificador antigo para leituras majoritárias?** Opções: substituir tudo | manter ambos com seletor de contexto. **Recomendação:** substituir nos consumidores de município (o produto é DF); manter as constantes antigas exportadas até B13 estabilizar, depois remover. +- **NEC/desequilíbrio entram na v1 do classificador?** **Recomendação:** sim como fator qualificador de "ataque" (disputa aberta), com fallback neutro quando o cálculo por município não estiver disponível — não bloquear a entrega pelos índices. ## Abordagem proposta ```mermaid flowchart LR - Base["plazaElectoralBaseline
(série + válidos)"] - Pot["plazaPotential (E8)
captura/LQ/teto"] + Base["municipalityElectoralBaseline
(série + válidos)"] + Pot["municipalityPotential (E8)
captura/LQ/teto"] Class["computeDfTerritorialClass
(âncoras relativas, multi-eixo)"] - UI["insights da Praça + overview + filtro"] + UI["insights do município + overview + filtro"] Base --> Class Pot --> Class Class --> UI @@ -58,13 +58,13 @@ flowchart LR Componentes: - **`src/lib/electionInsights.ts`** (ou `dfTerritorialClass.ts` ao lado): classificador puro + âncoras versionadas; manter helpers de badge/label existentes (`territorialClassBadgeVariant`, `territorialClassLabel`). -- **Consumidores:** componente de insights da Praça (stack atual), `PlazaListOverview.tsx` (distribuição), `plazaPageData.ts` (cálculo no load, sem N+1 — reusa agregados do bundle). +- **Consumidores:** componente de insights do município (stack atual), `MunicipalityListOverview.tsx` (distribuição), `municipalityPageData.ts` (cálculo no load, sem N+1 — reusa agregados do bundle). - **Sem migration, sem collection, sem server action.** ## Dependências - Dura: **E8** (LQ/captura/teto derivados). Suave: E15 (calibração futura dos cortes); B13 (consome as classes). -- Reusa: `plazaElectoralBaseline.ts`, `plazaCandidateComparison.ts` (share de outros candidatos para competição), helpers de badge existentes. +- Reusa: `municipalityElectoralBaseline.ts`, `municipalityCandidateComparison.ts` (share de outros candidatos para competição), helpers de badge existentes. ## Não escopo @@ -73,7 +73,7 @@ Componentes: ## Rabbit holes - **Calibração prematura.** Discutir "LQ 2 ou 3?" sem backtest é bikeshedding — valores iniciais documentados como ilustrativos; E15 decide. -- **Índices de competição por praça exigirem query nova pesada.** Se NEC/desequilíbrio por praça não saírem baratos do que `plazaCandidateComparison.ts` já carrega, degradar para fator neutro — não criar agregado SQL novo neste item. +- **Índices de competição por município exigirem query nova pesada.** Se NEC/desequilíbrio por município não saírem baratos do que `municipalityCandidateComparison.ts` já carrega, degradar para fator neutro — não criar agregado SQL novo neste item. ## Adiado com gatilho @@ -84,5 +84,5 @@ Componentes: - `docs/roadmap.md` (Inteligência de campanha, E10) · [plano-mestre](inteligencia-campanha.md) - `docs/research/relatorio-entrevista-persona-campanha.md` D4 (degeneração + âncoras), §5 nota mapa/métricas - `docs/plans/insight-classificacao-territorial.md` (as-built A5-2 — limiares antigos, UI, testes) -- `src/lib/electionInsights.ts`, `src/utilities/plazaElectoralBaseline.ts`, `src/utilities/plazaCandidateComparison.ts`, `src/components/campaign/PlazaListOverview.tsx` +- `src/lib/electionInsights.ts`, `src/utilities/municipalityElectoralBaseline.ts`, `src/utilities/municipalityCandidateComparison.ts`, `src/components/campaign/MunicipalityListOverview.tsx` - AGENTS.md — limiares versionados, naming diff --git a/docs/plans/conta-da-cadeira.md b/docs/plans/conta-da-cadeira.md index d7264dfb..1fce20f4 100644 --- a/docs/plans/conta-da-cadeira.md +++ b/docs/plans/conta-da-cadeira.md @@ -1,9 +1,9 @@ -# E8 — Conta da cadeira (metas derivadas, potencial por praça, cobertura) +# E8 — Conta da cadeira (metas derivadas, potencial por município, cobertura) Status: rascunho -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) +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening; **A10 entregue e remodelagem em produção — dependências duras satisfeitas**) +Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", E8; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md); A10 ✓) +Impeccable: B — cobertura/metas encaixadas no dashboard, na lista de municípios e no detalhe (sem rota nova) Appetite: ~2 dias eng; migration pequena (global `campaignGoals`) + utilities derivadas + encaixes de UI Responsável: — @@ -13,31 +13,31 @@ Responsável: — Na implementação (`implement-roadmap-item`): craft compacto → critique → polish. -- **Persona / contexto:** coordenador-geral na reunião semanal; assessor conferindo "quanto falta" nas suas Praças. -- **Job principal:** responder "a conta fecha?" em uma linha — meta, comprometido, delta da semana, por praça e no agregado. +- **Persona / contexto:** coordenador-geral na reunião semanal; assessor conferindo "quanto falta" nas seus municípios. +- **Job principal:** responder "a conta fecha?" em uma linha — meta, comprometido, delta da semana, por município e no agregado. - **Estratégia de cor:** Restrained; delta negativo usa o vermelho de campanha com parcimônia (não hero-metric). -- **Edit where you see:** sim — meta da praça editável em contexto (Popover reusando `plazaStaffFormActions`), como B9. +- **Edit where you see:** sim — meta do município editável em contexto (Popover reusando `municipalityStaffFormActions`), como B9. - **Anti-goals:** KPI de % estadual absoluto; contagem bruta de cadastros; gauge/velocímetro SaaS. ## Contexto -O relatório de discovery ([§5.1](../research/relatorio-entrevista-persona-campanha.md)) fixou a OMTM da campanha até 16/08: **cobertura da meta por compromissos auditáveis** (Σ pledges ÷ meta, com delta semanal como número da reunião), decomposta por praça. Hoje `plaza.voteGoals` (Bom/Regular/Mínimo) e `plaza.expectedVotes` existem (E1/A9), mas: não há meta estadual nem decomposição de cima para baixo; não há noção de potencial derivado (válidos projetados, captura do campo, roll-off); denominadores baseados em `aptos` inflariam praças de abstenção estrutural (relatório §6.6/I-B); e não existe "campo" definido por ano para share intracampo. Os dados TSE necessários já estão em `electionTally` (válidos/brancos/nulos/comparecimento por cargo×ano×cidade×zona) e `electionCandidateVote`. +O relatório de discovery ([§5.1](../research/relatorio-entrevista-persona-campanha.md)) fixou a OMTM da campanha até 16/08: **cobertura da meta por compromissos auditáveis** (Σ pledges ÷ meta, com delta semanal como número da reunião), decomposta por município. Hoje `municipality.voteGoals` (Bom/Regular/Mínimo) e `municipality.expectedVotes` existem (E1/A9), mas: não há meta estadual nem decomposição de cima para baixo; não há noção de potencial derivado (válidos projetados, captura do campo, roll-off); denominadores baseados em `aptos` inflariam municípios de abstenção estrutural (relatório §6.6/I-B); e não existe "campo" definido por ano para share intracampo. Os dados TSE necessários já estão em `electionTally` (válidos/brancos/nulos/comparecimento por cargo×ano×cidade×zona) e `electionCandidateVote`; desde o hardening 2026-07-23, a série de válidos federais T1 + votos Solla por município já existe pré-agregada no artefato commitado `src/lib/bahiaElectionAggregates.ts` — preferir o artefato para válidos projetados; teto do campo, share intracampo e roll-off continuam derivando das collections (envolver em `unstable_cache` com a tag `election-tse`, padrão dos loaders de eleições). ## Objetivos - 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: Σ (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. +- Utilities derivadas por município (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: Σ (estimativa do cenário ativo — default **média/`central`** via A10 — `?? declaredVotes`) ÷ meta por município 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 (`MunicipalityList`/`MunicipalityListOverview`), card no detalhe do município. - 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). +- Sanity check por TI: Σ metas dos municípios do TI vs. participação histórica do TI no voto (aviso, não bloqueio). ## Decisões travadas -- **Denominadores usam válidos projetados do cargo, nunca `aptos`** (relatório §6.6; abstenção estrutural >35% em praças pequenas infla potencial). **Rejeitado:** `aptos` (viés pró-praça-vazia); válidos da majoritária (cargo errado); eleitorado IBGE (não é cadastro eleitoral). -- **Meta estadual mora num global editável, decomposição é derivada e a meta por praça continua editável** (`voteGoals` manual vence a sugerida quando preenchida). Mantém julgamento humano no loop e evita reescrever E1. **Rejeitado:** meta por praça 100% automática (K-C: "o erro é a conta" precisa de dono humano); collection de metas versionadas (C12 cobre auditoria via decisões). +- **Denominadores usam válidos projetados do cargo, nunca `aptos`** (relatório §6.6; abstenção estrutural >35% em municípios pequenos infla potencial). **Rejeitado:** `aptos` (viés pró-município-vazio); válidos da majoritária (cargo errado); eleitorado IBGE (não é cadastro eleitoral). +- **Meta estadual mora num global editável, decomposição é derivada e a meta por município continua editável** (`voteGoals` manual vence a sugerida quando preenchida). Mantém julgamento humano no loop e evita reescrever E1. **Rejeitado:** meta por município 100% automática (K-C: "o erro é a conta" precisa de dono humano); collection de metas versionadas (C12 cobre auditoria via decisões). - **"Campo" por ano é lib estática curada** `src/lib/campoParties.ts` (ano → partidos/números). **Rejeitado:** collection administrável (curadoria política rara, não CRUD); inferir por coligação do TSE (federações ≠ campo político real). -- **i18n e naming:** `campaignGoals`, `computePlazaPotential`, `computeGoalCoverage`, `projectedValidVotes`, `captureRate`, `intraFieldShare`, `rollOff`; labels pt-BR. +- **i18n e naming:** `campaignGoals`, `computeMunicipalityPotential`, `computeGoalCoverage`, `projectedValidVotes`, `captureRate`, `intraFieldShare`, `rollOff`; labels pt-BR. ## Questões em aberto @@ -51,10 +51,10 @@ flowchart LR Tally["electionTally + electionCandidateVote
(série 2014/2018/2022)"] Campo["campoParties.ts"] Goals["global campaignGoals"] - Pot["computePlazaPotential
(válidos proj., teto, captura, roll-off)"] + Pot["computeMunicipalityPotential
(válidos proj., teto, captura, roll-off)"] Cov["computeGoalCoverage
(Σ pledges ÷ meta)"] - Pledges["aggregatePledgesByPlaza
(votePledgeData.ts)"] - UI["dashboard + PlazaList/Overview + detalhe"] + Pledges["aggregatePledgesByMunicipality
(votePledgeData.ts)"] + UI["dashboard + MunicipalityList/Overview + detalhe"] Tally --> Pot Campo --> Pot Goals --> Cov @@ -66,16 +66,16 @@ flowchart LR 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 por cenário de [A10](cenarios-estimativa-votos.md) / `votePledgeData.ts`, default `central`) + `voteGoals`/meta sugerida; sanity por TI via `bahiaTerritories.ts`. +- **`src/utilities/municipalityPotential.ts`**: deriva por município (reusa `municipalityElectionGeography.ts` para células cidade×zona e `municipalityElectoralBaseline.ts` para a série); expõe `projectedValidVotes`, `fieldCeiling`, `captureRate`, `intraFieldShare`, `rollOff`, `suggestedGoal`. +- **`src/utilities/goalCoverage.ts`**: cobertura por município/agregado consumindo `aggregatePledgesByMunicipality` (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. +- **UI:** linha nova em `CampaignMetricStrip` do dashboard; coluna/ordenar por cobertura em `MunicipalityList` (reusa padrão B9 para editar meta via `municipalityStaffFormActions`); 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: **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`. +- Duras: nenhuma pendente — **A10** entregue ([plano](cenarios-estimativa-votos.md)) e remodelagem em produção desde 2026-07-23 (schema `municipality`/`votePledge` vivo). Suave: C12 (delta semanal real da cobertura usa trajetória de pledges; até lá, sem histórico). +- Reusa: `municipalityElectoralBaseline.ts`, `municipalityElectionGeography.ts`, `votePledgeData.ts` (agregação por cenário pós-A10), `municipalityPageData.ts` (`loadMunicipalityListPageBundle`), `campaignDashboardData.ts`, `municipalityStaffFormActions.ts`, `campaignAccess.ts`. ## Não escopo @@ -96,6 +96,6 @@ Componentes: - `docs/roadmap.md` (seção Inteligência de campanha) · [plano-mestre](inteligencia-campanha.md) (gaps G5/G6) - `docs/research/relatorio-entrevista-persona-campanha.md` §5.1 (OMTM), §6.6 (denominadores/roll-off), FU2 (numerador/denominador) -- `src/collections/Plaza.ts` (voteGoals/expectedVotes/access), `src/collections/ElectionTally.ts` (campos por cargo) -- `src/utilities/plazaElectoralBaseline.ts`, `src/utilities/plazaElectionGeography.ts`, `src/utilities/votePledgeData.ts`, `src/utilities/campaignDashboardData.ts` +- `src/collections/Municipality.ts` (voteGoals/expectedVotes/access), `src/collections/ElectionTally.ts` (campos por cargo) +- `src/utilities/municipalityElectoralBaseline.ts`, `src/utilities/municipalityElectionGeography.ts`, `src/utilities/votePledgeData.ts`, `src/utilities/campaignDashboardData.ts` - AGENTS.md — migrations, access staff-only, naming, transações diff --git a/docs/plans/edicao-rapida-lista-pracas.md b/docs/plans/edicao-rapida-lista-pracas.md index 2d783bef..a2899704 100644 --- a/docs/plans/edicao-rapida-lista-pracas.md +++ b/docs/plans/edicao-rapida-lista-pracas.md @@ -1,6 +1,8 @@ # Edição rápida na lista de Praças -Status: entregue 2026-07-21 (branch; deploy pendente com remodelagem) +> **Nota (2026-07-24):** entregue na era Praças e **em produção desde 2026-07-23**. A remodelagem M1 renomeou os identificadores `plaza*` → `municipality*` e as rotas `pracas/` → `municipios/`; os nomes neste plano são os da época. Nomes atuais: ver [remodelagem-municipios.md](remodelagem-municipios.md). + +Status: entregue 2026-07-21 (em produção desde 2026-07-23) Atualizado em: 2026-07-21 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B9 — entregue 2026-07-21) Impeccable: B — encaixe em `PlazaList` / cards mobile em `/campanha/pracas` (sem rota nova) diff --git a/docs/plans/escala-dry-pos-a9.md b/docs/plans/escala-dry-pos-a9.md index bd6514a3..ede4967b 100644 --- a/docs/plans/escala-dry-pos-a9.md +++ b/docs/plans/escala-dry-pos-a9.md @@ -1,6 +1,8 @@ # Escala e DRY pós-A9 (loader da lista de Praças) -Status: entregue 2026-07-21 (F1+F2; aguardando merge/deploy com remodelagem) +> **Nota (2026-07-24):** entregue na era Praças e **em produção desde 2026-07-23**. A remodelagem M1 renomeou os identificadores `plaza*` → `municipality*` e as rotas `pracas/` → `municipios/`; os nomes neste plano são os da época. Nomes atuais: ver [remodelagem-municipios.md](remodelagem-municipios.md). + +Status: entregue 2026-07-21 (F1+F2; em produção desde 2026-07-23) Atualizado em: 2026-07-21 (implementação + `/simplify` + rebase em `main` com B11; `capture-review-debts` pós-implementação) Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha A, fill-in **A9+** pós-A9) Impeccable: A — N/A (sem superfície UI; otimização de loader) diff --git a/docs/plans/escala-dry-pos-b3.md b/docs/plans/escala-dry-pos-b3.md index a284e74c..875bb4d6 100644 --- a/docs/plans/escala-dry-pos-b3.md +++ b/docs/plans/escala-dry-pos-b3.md @@ -1,6 +1,8 @@ # Escala e DRY pós-B3 (Leaflet / coroplético) -Status: entregue em código (2026-07-21) +> **Nota (2026-07-24):** entregue na era Praças e **em produção desde 2026-07-23**. A remodelagem M1 renomeou os identificadores `plaza*` → `municipality*` e as rotas `pracas/` → `municipios/`; os nomes neste plano são os da época. Nomes atuais: ver [remodelagem-municipios.md](remodelagem-municipios.md). + +Status: entregue (2026-07-21; em produção desde 2026-07-23) Atualizado em: 2026-07-21 (B6 implementado + triage `capture-review-debts` pós-`/simplify`: gatilhos absorvidos; sem item novo no roadmap) Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B6) Impeccable: A — N/A (sem superfície UI nova; otimização do renderer Leaflet existente) diff --git a/docs/plans/escala-dry-pos-c6.md b/docs/plans/escala-dry-pos-c6.md index 2140c185..f0562876 100644 --- a/docs/plans/escala-dry-pos-c6.md +++ b/docs/plans/escala-dry-pos-c6.md @@ -1,10 +1,12 @@ # Escala e DRY pós-C6 (apoiadores / import / listas) -Status: implementado (pendente merge em `main`) -Atualizado em: 2026-07-21 (`capture-review-debts` pós-B9 `/simplify`) -Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha C, item C8) +Status: F4 (DRY de forms) **entregue** — twin praça 2026-07-21 + hardening 2026-07-23; F1–F2 abertos com gatilho (volume real de import/base nominal pós-Onda 0); F3 parcialmente absorvido +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening) +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins abertos, item C8) Responsável: — +> **Revisão 2026-07-24:** a remodelagem M1 renomeou `plaza*`/`nucleus*` → `municipality*` (paths `pracas/` → `municipios/`), e o hardening 2026-07-23 (Fase 5) completou a **Fase 4**: os 4 ladders `*/nova` migraram para `runCampaignRedirectFormAction`, os demais `formActions` já usam `mapCampaignFormActionError` + `campaignFormFields` (única exceção: `planos/[slug]/lifecycleFormActions.ts`, que só retorna safe-messages fixas — sem ladder para migrar), e `CampaignInviteForm` já importa `fieldError`/`errorProps` de `campaignFormFields`. O split de access também entregou parte da F3: `getAdvisorMunicipalityIds` (`src/utilities/access/municipalities.ts`) é a variante sem `PayloadRequest`, com cache request-scoped por contexto, e `supporterListOverviewAggregate` recebe os IDs pré-carregados. Restam F1 (perf do import) e o restante de F2/F3 — verificar contra o código no PR. + ## Contexto O C6 ([escala-dry-pos-c2.md](escala-dry-pos-c2.md)) entregou a escala do cadastro nominal de apoiadores: import bulk via drizzle na txn Payload, KPI agregado em SQL, token HMAC de import single-use, e shells compartilhados (`campaignListUrl`, `CampaignListPagination`, `campaignFormFields`, `mapCampaignFormActionError`). A passagem `/simplify` de 2026-07-19 aplicou limpezas pontuais (identity maps, dead branches, `relationshipId`, base64url nativo, `rowCount ?? 0`, reuso de `canManageCampaignUsers`) mas **deixou de fora** débitos que os revisores (quality / reuse / performance) marcaram como importantes e maiores que cleanup. Este plano é o registro canônico desses follow-ups — precedente direto de B5 (pós-B2) e C7 (pós-C3). @@ -18,11 +20,11 @@ Sem este item, a importação funciona em volume baixo, mas: (a) o gargalo de im - Leituras de existência (contacts/supporters existentes) no bulk usam drizzle direto na mesma txn, sem o scaffolding do Local API. - A página de lista de apoiadores não faz dois COUNTs filtrados por navegação; o `total` do overview vem do `totalDocs` da lista, ou o overview é cacheado por `(user, filterState)` com TTL curto. - Busca por `q` em nome/cidade usa índice `pg_trgm` GIN (ou restringe a prefix/telefone) — deixa de ser seq scan por página. -- O lookup de núcleos acessíveis do coordenador é memoizado por request (lista + overview compartilham uma query). +- ~~O lookup de municípios acessíveis é memoizado por request~~ — absorvido pelo cache por contexto de `getAccessibleMunicipalityIds` (hardening 2026-07-23); conferir compartilhamento lista+overview no PR. - `chunk` / `requireTable` / `INSERT_CHUNK_SIZE` / scaffold de tipos drizzle vivem num helper compartilhado, reusado por `supporterImportBulk.ts` e `electionResultsImport.ts` (import TSE). - O scaffold de tipos drizzle não apaga a tipagem de colunas: assertion runtime dos nomes de coluna em `payload.db.tables.supporter` ao init **ou** `PostgresTransactionDatabase` expõe um `insert` tipado. -- `getAccessibleNucleusIds` ganha uma variante sem `PayloadRequest` (ou `supporterListOverviewAggregate` reusa a existente), eliminando o lookup duplicado de escopo coordenador. -- Todos os `formActions.ts` de `/campanha` restantes migram para `mapCampaignFormActionError` + `campaignFormFields` (hoje duplicados em ~6 arquivos). +- ~~Variante de `getAccessibleNucleusIds` sem `PayloadRequest`~~ — entregue como `getAdvisorMunicipalityIds` (`src/utilities/access/municipalities.ts`). +- ~~Todos os `formActions.ts` restantes migram para os helpers compartilhados~~ — entregue (F4 completa; ver Fase 4). - Guardrails: `overrideAccess: false` com `user`; escritas multi-step continuam em `withPayloadTransaction`; locks advisory continuam xact-level; sem novo `Consent`; sem mudança de comportamento visível. ## Decisões travadas @@ -32,7 +34,7 @@ Sem este item, a importação funciona em volume baixo, mas: (a) o gargalo de im - **Cortável se a base nominal permanecer pequena.** Fases 1–2 (perf de import/lista) só rendem com base real em volume; se a base ficar pequena até 16/08, adiar para Janela 3. Fases 3–4 (DRY) são baratas e reduzem drift — manter se houver folga. - **Sem migration de schema em Fases 1–3.** Fase 2 pode exigir uma migration pequena (`pg_trgm` GIN em `contact.name`/`contact.city`) — única migration do item. - **`postgresTransactionLocks.ts` é pré-existente e compartilhado** com o path single-create (`upsertContactByPhone`, `enforceUniqueContactPhone`). A Fase 1 troca o loop de locks por uma query única sem mudar o contrato `acquireContactPhoneLocks(payload, req, phones[])` — callers não mudam. -- **i18n e naming** (AGENTS.md): identificadores em inglês (`acquireContactPhoneLocks` mantido, `drizzleBulk.ts`, `getAccessibleNucleusIds` variant, etc.), strings visíveis em pt-BR. +- **i18n e naming** (AGENTS.md): identificadores em inglês (`acquireContactPhoneLocks` mantido, `drizzleBulk.ts`, `getAdvisorMunicipalityIds`, etc.), strings visíveis em pt-BR. ## Questões em aberto @@ -47,8 +49,8 @@ Sem este item, a importação funciona em volume baixo, mas: (a) o gargalo de im flowchart TD F1["Fase 1 — Perf import
locks em 1 RT · .returning · drizzle reads"] F2["Fase 2 — Perf lista
1 COUNT · trgm · memo accessible nuclei"] - F3["Fase 3 — DRY helpers
drizzleBulk · getAccessibleNucleusIds variant · DrizzleTx assertion"] - F4["Fase 4 — DRY forms
migrar formActions restantes"] + F3["Fase 3 — DRY helpers
drizzleBulk · getAdvisorMunicipalityIds ✓ · DrizzleTx assertion"] + F4["Fase 4 — DRY forms ✓
(entregue 2026-07-21 + 2026-07-23)"] C6["C6 apoiadores ✓"] --> F1 F1 --> F2 @@ -60,39 +62,36 @@ flowchart TD - **`acquireContactPhoneLocks`** (`src/utilities/contactPhoneInvariant.ts` / `postgresTransactionLocks.ts`): trocar o loop `for (const key of sortedKeys) await database.execute(...)` por uma query única `SELECT pg_advisory_xact_lock(hashtextextended(k, 0)) FROM unnest(ARRAY[...]) AS k` (ordenar para evitar deadlock). Contrato `acquireContactPhoneLocks(payload, req, phones[])` inalterado — callers (bulk, single-create, hook) não mudam. - **`bulkInsertSupporterImport`** (`src/utilities/supporterImportBulk.ts`): `tx.insert(contactTable).values(...).returning({ id: true, phone: true })` em vez do re-`payload.find` por chunk para recuperar IDs (elimina ~10 round-trips no cap de 5k). Widen o tipo `DrizzleTx` local para tipar `.returning()`. -- Substituir os dois `payload.find` de existência (contacts por phone, supporters por contact_id) por `SELECT ... FROM contact WHERE phone = ANY($)` / `SELECT contact_id FROM supporter WHERE contact_id = ANY($) AND nucleus_id IS NULL` na mesma sessão drizzle da txn. +- Substituir os dois `payload.find` de existência (contacts por phone, supporters por contact_id) por `SELECT ... FROM contact WHERE phone = ANY($)` / `SELECT contact_id FROM supporter WHERE contact_id = ANY($) AND municipality_id IS NULL` na mesma sessão drizzle da txn. ### Fase 2 — Perf de leitura da lista - **`loadSupporterListOverviewData`** (`src/utilities/supporterPageData.ts`): derivar `total` do `result.totalDocs` da lista (já computado por Payload); o overview roda só os dois `COUNT(*) FILTER` (certo+tende, indeciso). Elimina um COUNT filtrado completo por navegação. -- **`computeSupporterListOverviewAggregate`** (`src/utilities/supporterListOverviewAggregate.ts`): memoizar o `resolveAccessConstraint` (lookup de núcleos acessíveis do coordenador) por request — lista e overview compartilham a mesma query `electoralNucleus`. +- **`computeSupporterListOverviewAggregate`** (`src/utilities/supporterListOverviewAggregate.ts`): ~~memoizar o lookup de municípios acessíveis por request~~ — **largamente absorvido** (2026-07-23): `getAccessibleMunicipalityIds` tem cache request-scoped por contexto e o aggregate recebe `advisorMunicipalityIds` pré-carregados; conferir no PR se lista e overview compartilham de fato uma única query `municipality`. - **Migration** `add_contact_trgm_index`: `CREATE EXTENSION IF NOT EXISTS pg_trgm` + `CREATE INDEX ... USING gin (name gin_trgm_ops)` e `(city gin_trgm_ops)` em `contact`. Torna `ILIKE '%q%'` sargable. Única migration do item. - Avaliar restringir `q` a mínimo 2–3 chars no cliente (reduz seq scans inúteis) — opcional, sem migration. ### Fase 3 — DRY de helpers - **`src/utilities/drizzleBulk.ts`** (novo): extrair `chunk`, `requireTable`, `INSERT_CHUNK_SIZE`, e o scaffold de tipos `DrizzleTx`/`PayloadDb` de `supporterImportBulk.ts` e `electionResultsImport.ts` (hoje byte-idênticos). Importar de ambos. -- **`getAccessibleNucleusIds`** (`src/utilities/campaignAccess.ts`): adicionar variante sem `PayloadRequest` (ou parametrizar a existente com cache opcional) para que `supporterListOverviewAggregate` reuse em vez de re-implementar o lookup `coordinators contains user.id`. -- **Type safety do bulk:** assertion runtime em `supporterImportBulk.ts` que `Object.keys(payload.db.tables.supporter)` contém as colunas esperadas (`contact`, `nucleus`, `consent`, `createdBy`, …) no init do módulo — pega o bug `contactId` vs `contact` antes do runtime. (Widen de `PostgresTransactionDatabase` fica como follow-up de plataforma.) +- ~~Variante de `getAccessibleNucleusIds` sem `PayloadRequest`~~ — **entregue** no split de access (hardening 2026-07-23): `getAdvisorMunicipalityIds` (`src/utilities/access/municipalities.ts`) é a implementação canônica do lookup `advisors contains user.id` sem `PayloadRequest`, reusada por `getAccessibleMunicipalityIds` (cache por contexto) e pelo aggregate. +- **Type safety do bulk:** assertion runtime em `supporterImportBulk.ts` que `Object.keys(payload.db.tables.supporter)` contém as colunas esperadas (`contact`, `municipality`, `consent`, `createdBy`, …) no init do módulo — pega o bug `contactId` vs `contact` antes do runtime. (Widen de `PostgresTransactionDatabase` fica como follow-up de plataforma.) -### Fase 4 — DRY de forms +### Fase 4 — DRY de forms — **ENTREGUE** (2026-07-21 + hardening 2026-07-23) -- Migrar os `formActions.ts` restantes para `mapCampaignFormActionError` + `campaignFormFields` (hoje o ladder `FormDataBoundaryError` → `ZodError` → `validationFieldErrors` → safe-message está inlined em vários arquivos): - - ~~`src/app/(campaign)/campanha/(app)/pracas/listFormActions.ts` + `pracas/[slug]/editar/formActions.ts`~~ — **entregue 2026-07-21 (C8 F4):** `plazaStaffFormActions.ts` (3 actions compartilhadas); `editar/formActions.ts` mantém só `updatePlazaStrategyFormAction`; lista e `/editar` importam o módulo compartilhado diretamente (Next.js não permite re-export em `'use server'`). - - `src/app/(campaign)/campanha/(app)/pracas/[slug]/updateFormActions.ts`, `pledgeFormActions.ts` (se ainda não no mapper) - - `src/app/(campaign)/campanha/convite/[token]/formActions.ts` - - `src/app/(campaign)/campanha/(app)/apoiadores/[id]/formActions.ts` - - `src/app/(campaign)/campanha/(app)/planos/formActions.ts` e `planos/[slug]/updateFormActions.ts`, `resultFormActions.ts`, `lifecycleFormActions.ts` - - Demais `formActions` de `/campanha` (lideranças, demandas, organizações, apoiadores/novo) conforme grep no PR -- Migrar `fieldError`/`firstError` local em forms legados (ex.: `CampaignInviteForm.tsx`) para importar de `campaignFormFields.ts`. -- **Nota pós-remodel:** paths `nucleos/*` foram superseded; não reabrir. +- ~~Migrar os `formActions.ts` restantes para `mapCampaignFormActionError` + `campaignFormFields`~~ — **completo**: + - Twin lista↔`/editar`: `municipalityStaffFormActions.ts` (entregue 2026-07-21 como `plazaStaffFormActions.ts` em `pracas/`; renomeado na M1); `editar/formActions.ts` mantém só `updateMunicipalityStrategyFormAction`. + - 4 ladders `*/nova` (demandas, dobradinhas, lideranças, organizações): migrados para `runCampaignRedirectFormAction` no hardening 2026-07-23 (Fase 5). + - Todos os demais `formActions` (`convite/[token]`, `apoiadores/*`, `planos/*`, `municipios/[slug]/*`, `demandas/[slug]`, `dobradinhas/[slug]`, `liderancas/[id]`, `organizacoes/[slug]`) já usam `mapCampaignFormActionError`; `planos/[slug]/lifecycleFormActions.ts` retorna só safe-messages fixas (sem ladder — nada a migrar). +- ~~Migrar `fieldError`/`firstError` local em forms legados~~ — `CampaignInviteForm.tsx` já importa de `campaignFormFields.ts`. +- **Nota pós-remodel:** paths `nucleos/*`/`pracas/*` foram superseded; não reabrir. **Migration:** Fases 1, 3, 4 sem schema. Fase 2: `pnpm migrate:create add_contact_trgm_index` (extensão + 2 GIN indexes). Sem Consent novo. ## Dependências - **Dura:** C6 Escala e DRY pós-C2 (código em `supporterImportBulk.ts` / `supporterListOverviewAggregate.ts` / `supporterPageData.ts` / `campaignFormActionError.ts`). -- Reusa: `src/utilities/contactPhoneInvariant.ts`, `postgresTransactionLocks.ts`, `getPostgresTransactionDatabase`, `relationshipId`, `campaignAccess.ts` (`getAccessibleNucleusIds`, `canManageCampaignUsers`), `electionResultsImport.ts` (para `drizzleBulk.ts`), C6 plano [escala-dry-pos-c2.md](escala-dry-pos-c2.md). +- Reusa: `src/utilities/contactPhoneInvariant.ts`, `postgresTransactionLocks.ts`, `getPostgresTransactionDatabase`, `relationshipId`, `src/utilities/access/municipalities.ts` (`getAccessibleMunicipalityIds`, `getAdvisorMunicipalityIds`), `electionResultsImport.ts` (para `drizzleBulk.ts`), C6 plano [escala-dry-pos-c2.md](escala-dry-pos-c2.md). ## Não escopo @@ -114,7 +113,7 @@ flowchart TD - `src/utilities/supporterListOverviewAggregate.ts` — aggregate SQL + `resolveAccessConstraint` - `src/utilities/supporterPageData.ts` — `loadSupporterListOverviewData` (duplo COUNT) - `src/utilities/electionResultsImport.ts` — `chunk`/`requireTable`/`INSERT_CHUNK_SIZE` duplicados -- `src/utilities/campaignAccess.ts` — `getAccessibleNucleusIds` +- `src/utilities/access/municipalities.ts` — `getAccessibleMunicipalityIds` / `getAdvisorMunicipalityIds` - `src/utilities/campaignFormActionError.ts` / `campaignFormFields.ts` — helpers adotados por 2 forms - AGENTS.md — `overrideAccess: false`, transações, locks advisory xact-level, naming @@ -134,9 +133,9 @@ Os débitos que os revisores marcaram como importantes e maiores que cleanup for ## Simplify (2026-07-21 pós-B9) -~~`/simplify` da entrega **B9** marcou como **maior que cleanup** o twin `listFormActions.ts` ↔ `editar/formActions.ts`~~ — **entregue 2026-07-21 (C8 F4):** `plazaStaffFormActions.ts` unifica os 3 wrappers compartilhados; `editar/formActions.ts` mantém só `updatePlazaStrategyFormAction`. +~~`/simplify` da entrega **B9** marcou como **maior que cleanup** o twin `listFormActions.ts` ↔ `editar/formActions.ts`~~ — **entregue 2026-07-21 (C8 F4):** `municipalityStaffFormActions.ts` (à época `plazaStaffFormActions.ts`) unifica os 3 wrappers compartilhados; `editar/formActions.ts` mantém só `updateMunicipalityStrategyFormAction`. -**Explicitamente fora (triage `capture-review-debts` 2026-07-21 pós-B9):** hook `usePlazaListPopoverForm` (gatilho: 4º inline editor); `PlazaAdvisorCheckboxList` (gatilho: 3º uso ou refactor do form `/editar`); layout responsivo único mobile+desktop na lista (gatilho: page size >25 ou profiling de hydration); lazy-load de `getEligibleAdvisorOptions` (staff set pequeno); `PopoverAnchor` export não usado (scaffold shadcn); ~~helper `parsePoliticalTrendStatus`~~ → entregue como `parsePoliticalTrendStatusFormValue` no prep A9+; unit tests dos `PlazaList*Control` (int access cobre domínio); E2E save inline (gatilho: smoke pós-merge B9). +**Explicitamente fora (triage `capture-review-debts` 2026-07-21 pós-B9):** hook `useMunicipalityListPopoverForm` (gatilho: 4º inline editor); `MunicipalityAdvisorCheckboxList` (gatilho: 3º uso ou refactor do form `/editar`); layout responsivo único mobile+desktop na lista (gatilho: page size >25 ou profiling de hydration); lazy-load de `getEligibleAdvisorOptions` (staff set pequeno); `PopoverAnchor` export não usado (scaffold shadcn); ~~helper `parsePoliticalTrendStatus`~~ → entregue como `parsePoliticalTrendStatusFormValue` no prep A9+; unit tests dos `MunicipalityList*Control` (int access cobre domínio); E2E save inline (gatilho: smoke pós-merge B9). ## Simplify (2026-07-21 pós-A9+) @@ -144,14 +143,14 @@ Os débitos que os revisores marcaram como importantes e maiores que cleanup for ## Simplify (2026-07-21 pós-C8 F4) -Passagem `/simplify` + `capture-review-debts` após fill-in **C8 F4** (plaza twin DRY). +Passagem `/simplify` + `capture-review-debts` após fill-in **C8 F4** (twin praça→município DRY). -**Já resolvido (não reabrir):** `plazaStaffFormActions.ts`; `plazaStaffEditSafeMessages`; tipo `PlazaStaffFormAction` em `PlazaList.tsx`; import direto em `editar/page.tsx` (Next.js não permite re-export em `'use server'`); hygiene de refs em planos-pai A9/B9/estimativa. +**Já resolvido (não reabrir):** `municipalityStaffFormActions.ts (à época plazaStaffFormActions)`; `municipalityStaffEditSafeMessages (à época plazaStaffEditSafeMessages)`; tipo `MunicipalityStaffFormAction` em `MunicipalityList.tsx`; import direto em `editar/page.tsx` (Next.js não permite re-export em `'use server'`); hygiene de refs em planos-pai A9/B9/estimativa. **Explicitamente fora (triage `capture-review-debts` 2026-07-21 pós-C8 F4):** - **Runner genérico `runCampaignFormAction`** — gatilho: 5+ wrappers idênticos ou 4º cluster de forms na mesma superfície (hoje ~18 arquivos com try/catch inline). -- **`revalidatePlazaListPaths` com `scope` por tipo de save** — gatilho: N saves inline + profiling de rerender inaceitável (ver [escala-dry-pos-a9.md](escala-dry-pos-a9.md) Adiado com gatilho). -- **`pledgeFormActions` / `updateFormActions` → `revalidatePlazaListPaths`** — gatilho: forms ganham `plazaSlug` ou unificação de revalidate no detalhe da Praça (permanecem em Fase 4 remanescente abaixo). -- **`reloadCampaignActor` por submit em `actions/plaza.ts`** — gatilho: profiling mostra latência dominante em saves staff inline. -- **Fundir `updatePlazaStrategyFormAction` em `plazaStaffFormActions.ts`** — parse/`safeMessages` distintos; superfície só `/editar`. +- **`revalidateMunicipalityListPaths` com `scope` por tipo de save** — gatilho: N saves inline + profiling de rerender inaceitável (ver [escala-dry-pos-a9.md](escala-dry-pos-a9.md) Adiado com gatilho). +- **`pledgeFormActions` / `updateFormActions` → `revalidateMunicipalityListPaths`** — gatilho: forms ganham `municipalitySlug` ou unificação de revalidate no detalhe do município. +- **`reloadCampaignActor` por submit em `actions/municipality.ts`** — gatilho: profiling mostra latência dominante em saves staff inline. +- **Fundir `updateMunicipalityStrategyFormAction` em `municipalityStaffFormActions.ts`** — parse/`safeMessages` distintos; superfície só `/editar`. diff --git a/docs/plans/escala-dry-pos-reset-senha-perfil.md b/docs/plans/escala-dry-pos-reset-senha-perfil.md index c796c690..cf10c906 100644 --- a/docs/plans/escala-dry-pos-reset-senha-perfil.md +++ b/docs/plans/escala-dry-pos-reset-senha-perfil.md @@ -17,7 +17,7 @@ O fill-in **Reset de senha self-service + foto de perfil** ([reset-senha-foto-pe ## Objetivos -- Auth hot path sem join de `media` em rotas que não exibem avatar (núcleos, apoiadores, planos, etc.). +- Auth hot path sem join de `media` em rotas que não exibem avatar (municípios, apoiadores, planos, etc.). - Upload de avatar sem segurar transação Postgres aberta durante I/O de blob/sharp. - Shell compartilhado das páginas públicas de auth (`login`, `esqueci-senha`, `redefinir-senha`) — **parcial ✓** (shell + card header + logo + critique login fechados; pendem password fields + login state — Fase 3). - Um único módulo de validação/campos de senha reutilizado por convite, reset e perfil. @@ -78,7 +78,7 @@ flowchart TD - Consolidar strings de liderança/convite (`CAMPAIGN_LEADERSHIP_*`, `CAMPAIGN_FIRST_ACCESS_HINT`, `CAMPAIGN_LOGIN_SUBTITLE` + mensagem server em `campaignPasswordReset.ts`) numa fonte única com variantes por contexto. - **`campaignRoleLabels`** ↔ opções do campo `role` em `CampaignUser.ts` (fonte única `campaignRoleOptions`). -- Propagar para **`NucleusUpdateFeed.tsx`** e outros `roleLabels` locais quando esses arquivos forem tocados. +- Propagar para **`MunicipalityUpdateFeed.tsx`** (ex-`NucleusUpdateFeed`) e outros `roleLabels` locais quando esses arquivos forem tocados. - **`password.ts` / `profile.ts`:** adotar `getCampaignActionContext()` (`src/utilities/campaignActionContext.ts`) como `supporter.ts` / `actionPlan.ts`. ### Fase 5 — Opcional / cortável diff --git a/docs/plans/escala-percentual-mapa-pracas.md b/docs/plans/escala-percentual-mapa-pracas.md index 659ebb8d..f35473b7 100644 --- a/docs/plans/escala-percentual-mapa-pracas.md +++ b/docs/plans/escala-percentual-mapa-pracas.md @@ -1,6 +1,8 @@ # Escala percentual no Mapa das Praças (0–100% dos válidos) -Status: entregue 2026-07-21 +> **Nota (2026-07-24):** entregue na era Praças e **em produção desde 2026-07-23**. A remodelagem M1 renomeou os identificadores `plaza*` → `municipality*` e as rotas `pracas/` → `municipios/`; os nomes neste plano são os da época. Nomes atuais: ver [remodelagem-municipios.md](remodelagem-municipios.md). + +Status: entregue 2026-07-21 (em produção desde 2026-07-23) Atualizado em: 2026-07-21 (implementação B11 + `capture-review-debts` pós-`/simplify`) Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B11) Impeccable: B — encaixe em `PlazaMapPanel` / `BahiaMap` / `ChoroplethLegend` (sem rota nova) diff --git a/docs/plans/escala-relativa-mapa.md b/docs/plans/escala-relativa-mapa.md index 597f47f1..c852de44 100644 --- a/docs/plans/escala-relativa-mapa.md +++ b/docs/plans/escala-relativa-mapa.md @@ -1,9 +1,9 @@ -# B13 — Escala relativa no Mapa das Praças (quantis/LQ/rank + símbolo proporcional) +# B13 — Escala relativa no mapa dos Municípios (quantis/LQ/rank + símbolo proporcional) Status: rascunho -Atualizado em: 2026-07-21 +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening) Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", B13; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md)) -Impeccable: B — estende `PlazaMapPanel`/`BahiaMap` existentes (seletor de escala, camada de símbolos); sem rota nova +Impeccable: B — estende `MunicipalityMapPanel`/`BahiaMap` existentes (seletor de escala, camada de símbolos); sem rota nova Appetite: ~2 dias eng; sem migration Responsável: — @@ -25,7 +25,7 @@ B11 entregou a escala "% dos válidos" com domínio fixo 0–100% — tecnicamen ## Objetivos -- Seletor de escala ganha modos relativos: **Quantis (5)** — default novo para métrica de candidato; **LQ** (diverging em torno de 1); **Rank na praça** (1º / 2º–3º / 4º+); mantém **Total (votos)** e **% dos válidos** (B11). +- Seletor de escala ganha modos relativos: **Quantis (5)** — default novo para métrica de candidato; **LQ** (diverging em torno de 1); **Rank no município** (1º / 2º–3º / 4º+); mantém **Total (votos)** e **% dos válidos** (B11). - Camada opcional de **símbolos proporcionais** (bolha por votos em jogo — válidos projetados E8; cor da bolha = classe E10) sobre o coropleto neutro; toggle no painel. - Legenda por classes com rótulos de faixa reais (quantis do conjunto filtrado atual, coerente com B7/B12). - Readout (`MapFeatureReadout`) mostra o valor bruto + a leitura relativa ("Q5 de 5 · LQ 2,8"). @@ -37,7 +37,7 @@ B11 entregou a escala "% dos válidos" com domínio fixo 0–100% — tecnicamen - **Quantis do conjunto filtrado como default relativo** (discriminam por construção; recomendação clássica para séries). **Rejeitado:** Jenks como default (menos estável entre filtros/anos — quebra a comparabilidade; pode entrar depois como opção); domínio dinâmico min–max contínuo (melhora pouco e perde classes nomeáveis). - **Símbolo proporcional em vez de cartograma/value-by-alpha como peso de eleitorado.** Operador navega pela geografia real; bolha codifica volume+status em 2 canais legíveis (relatório D3). **Rejeitado:** cartograma (anti-goal travado); value-by-alpha como default (canal sutil para decisão rápida — pode virar teste A/B futuro). - **Nenhum componente de mapa novo** — tudo entra por `BahiaMap`/`bahiaMapStyle.ts` (B6 manteve o layer estável exatamente para isso). **Rejeitado:** fork do painel para "mapa estratégico" separado. -- **i18n e naming:** `scaleMode` estendido (`absolute | percentValid | quantile | lq | rank`), `symbolLayer`, `computeQuantileBreaks`; labels pt-BR ("Quantis", "Padrão próprio (LQ)", "Posição na praça", "Bolhas por votos em jogo"). +- **i18n e naming:** `scaleMode` estendido (`absolute | percentValid | quantile | lq | rank`), `symbolLayer`, `computeQuantileBreaks`; labels pt-BR ("Quantis", "Padrão próprio (LQ)", "Posição no município", "Bolhas por votos em jogo"). ## Questões em aberto @@ -48,8 +48,8 @@ B11 entregou a escala "% dos válidos" com domínio fixo 0–100% — tecnicamen ```mermaid flowchart LR - Bundle["PlazaMapBundle
(plazaMapData.ts)"] - Pot["plazaPotential (E8)"] + Bundle["MunicipalityMapBundle
(municipalityMapData.ts)"] + Pot["municipalityPotential (E8)"] Class["computeDfTerritorialClass (E10)"] Breaks["computeQuantileBreaks / lq / rank
(bahiaMapStyle.ts)"] Map["BahiaMap
(fills + symbol layer)"] @@ -64,25 +64,25 @@ flowchart LR Componentes: - **`src/lib/bahiaMapStyle.ts`**: breaks de quantis/LQ/rank puros (unit-testáveis); paleta de 5 classes do tema. -- **`src/components/campaign/BahiaMap.tsx`**: camada `L.circleMarker` por praça (raio ∝ √votos, padrão cartográfico), regida pelo mesmo `pathByKeyRef`/restyle incremental de B6; hover/tap integram o readout existente. -- **`src/components/campaign/PlazaMapPanel.tsx`**: seletor de escala estendido + toggle de bolhas; regras de compare inalteradas. -- **`src/utilities/plazaMapData.ts`**: expõe métricas relativas no bundle (LQ, rank, classe) — deriva de E8/E10 sem query nova. +- **`src/components/campaign/BahiaMap.tsx`**: camada `L.circleMarker` por município (raio ∝ √votos, padrão cartográfico), regida pelo mesmo `pathByKeyRef`/restyle incremental de B6; hover/tap integram o readout existente. +- **`src/components/campaign/MunicipalityMapPanel.tsx`**: seletor de escala estendido + toggle de bolhas; regras de compare inalteradas. +- **`src/utilities/municipalityMapData.ts`**: expõe métricas relativas no bundle (LQ, rank, classe) — deriva de E8/E10 sem query nova. Desde o hardening 2026-07-23, o TIPO do bundle vive em `municipalityMapContract.ts` (módulo client-safe; `municipalityMapData.ts` é o loader server-only que o re-exporta) — estender o bundle = editar os dois; anos históricos vêm do artefato `bahiaElectionAggregates.ts`. - **Sem migration.** ## Dependências - Duras: **E8** (válidos projetados/LQ), **E10** (classe para a cor da bolha). Suaves: E14 (bolha pode encodar nível no futuro), B8 F2 (polígonos de zona — independente). -- Reusa: B6/B10/B11/B12 inteiros; `plazasByIbgeCode`; `canonicalMapKeysKey`. +- Reusa: B6/B10/B11/B12 inteiros; `municipalitiesByIbgeCode`; `canonicalMapKeysKey`. ## Não escopo -- Rollup/mapa por TI (E12); polígonos das Praças-zona (B8 F2); export/print do mapa; value-by-alpha (adiado abaixo). +- Rollup/mapa por TI (E12); polígonos das Municípios-zona (B8 F2); export/print do mapa; value-by-alpha (adiado abaixo). ## Rabbit holes - **Perf com 417 bolhas + hover.** B6 resolveu para fills; markers precisam do mesmo tratamento incremental — se o frame cair, bolhas só a partir de zoom N ou top-K por votos. Não otimizar antes de medir. -- **Legenda de quantis com faixas confusas quando o filtro retorna <10 praças.** Degradar para 3 classes ou valores diretos; não inventar interpolação. -- **Rank exigir todos os candidatos por praça em memória.** `plazaCandidateComparison.ts` já pagina/limita; rank v1 = posição do Solla entre os top-N carregados, documentado. +- **Legenda de quantis com faixas confusas quando o filtro retorna <10 municípios.** Degradar para 3 classes ou valores diretos; não inventar interpolação. +- **Rank exigir todos os candidatos por município em memória.** `municipalityCandidateComparison.ts` já pagina/limita; rank v1 = posição do Solla entre os top-N carregados, documentado. ## Adiado com gatilho @@ -94,5 +94,5 @@ Componentes: - `docs/roadmap.md` (Inteligência de campanha, B13) · [plano-mestre](inteligencia-campanha.md) - `docs/research/relatorio-entrevista-persona-campanha.md` D2/D3 (escalas, símbolo proporcional, "não vá" do cartograma) · compêndio §8.2 - `docs/plans/escala-percentual-mapa-pracas.md` (B11), `docs/plans/escala-dry-pos-b3.md` (B6), `docs/plans/aproximar-mapa-pracas.md` (B12), `docs/plans/hover-mapa-pracas.md` (B10) -- `src/components/campaign/BahiaMap.tsx`, `src/components/campaign/PlazaMapPanel.tsx`, `src/lib/bahiaMapStyle.ts`, `src/utilities/plazaMapData.ts` +- `src/components/campaign/BahiaMap.tsx`, `src/components/campaign/MunicipalityMapPanel.tsx`, `src/lib/bahiaMapStyle.ts`, `src/utilities/municipalityMapData.ts` - AGENTS.md — mapa (B6/B11/B12 as-built), naming diff --git a/docs/plans/estimativa-votos-praca.md b/docs/plans/estimativa-votos-praca.md index 9c649a87..93c9d961 100644 --- a/docs/plans/estimativa-votos-praca.md +++ b/docs/plans/estimativa-votos-praca.md @@ -1,6 +1,8 @@ # Estimativa de votos da Praça (total esperado) -Status: entregue 2026-07-21 (branch; deploy pendente com remodelagem) +> **Nota (2026-07-24):** entregue na era Praças e **em produção desde 2026-07-23**. A remodelagem M1 renomeou os identificadores `plaza*` → `municipality*` e as rotas `pracas/` → `municipios/`; os nomes neste plano são os da época. Nomes atuais: ver [remodelagem-municipios.md](remodelagem-municipios.md). + +Status: entregue 2026-07-21 (em produção desde 2026-07-23) Atualizado em: 2026-07-21 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha A, item A9) Impeccable: B — campo + migration + action + encaixe em `/editar` / detalhe / lista (leitura); edição inline na lista = B9 diff --git a/docs/plans/fila-de-alocacao.md b/docs/plans/fila-de-alocacao.md index 4f21fcb4..569087d8 100644 --- a/docs/plans/fila-de-alocacao.md +++ b/docs/plans/fila-de-alocacao.md @@ -1,56 +1,56 @@ # E9 — Fila de alocação (lista de decisão do coordenador) Status: rascunho -Atualizado em: 2026-07-21 +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening) Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", E9; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md)) -Impeccable: B — visão nova DENTRO de `/campanha/pracas` (ordenações + colunas + painel), sem rota nova +Impeccable: B — visão nova DENTRO de `/campanha/municipios` (ordenações + colunas + painel), sem rota nova Appetite: ~1,5 dia eng; sem migration (deriva de E8 + dados existentes) Responsável: — ## Design (Impeccable) -Âncoras: `PRODUCT.md` (princípios 2 "clarity under pressure", 3 "edit where you see", 5) / `DESIGN.md` (register `product`) · tema `data-theme='campaign'` · shells existentes (`PlazaList`, `PlazaListOverview`, `PlazaFilters`, `CampaignListPagination`). +Âncoras: `PRODUCT.md` (princípios 2 "clarity under pressure", 3 "edit where you see", 5) / `DESIGN.md` (register `product`) · tema `data-theme='campaign'` · shells existentes (`MunicipalityList`, `MunicipalityListOverview`, `MunicipalityFilters`, `CampaignListPagination`). Na implementação: craft compacto → critique → polish. -- **Persona / contexto:** "o mapa serve ao padrão espacial; a alocação de verdade se decide na lista" (relatório D3). Coordenador prepara a reunião de recurso; assessor abre a fila das suas Praças. +- **Persona / contexto:** "o mapa serve ao padrão espacial; a alocação de verdade se decide na lista" (relatório D3). Coordenador prepara a reunião de recurso; assessor abre a fila dos seus municípios. - **Job principal:** a primeira linha da lista é o lugar onde falta mais voto que a campanha planejou ter. - **Estratégia de cor:** Restrained; "coluna da vergonha" (sem responsável) como badge âmbar discreta, não alarme. -- **Edit where you see:** sim — responsável/meta/nível editáveis em contexto (reusa `PlazaListAdvisorsControl` e padrão B9). -- **Anti-goals:** data-grid/planilha full-row; segunda lista paralela à de Praças; ranking gamificado de assessores (alimenta gaming — relatório G4). +- **Edit where you see:** sim — responsável/meta/nível editáveis em contexto (reusa `MunicipalityListAdvisorsControl` e padrão B9). +- **Anti-goals:** data-grid/planilha full-row; segunda lista paralela à de municípios; ranking gamificado de assessores (alimenta gaming — relatório G4). ## Contexto -O relatório definiu a "fila de trabalho" com 7 colunas ranqueadas (FU3): votos em jogo; meta/cobertura/delta; classe operacional; desempenho relativo (LQ/captura); rede + frescor; competição local; tendência — com **ordenação default por déficit descoberto** (meta − comprometido, decrescente; empate por votos em jogo) e uma ordenação de risco (praças de defesa por frescor, a mais dormente primeiro). A lista de Praças atual (`PlazaList` + `loadPlazaListPageBundle`) já tem filtros/URL/overview/edição rápida; faltam as colunas derivadas e as ordenações de decisão. A hierarquia de triagem completa (5 níveis) vive no plano-mestre e vira produto no E11 — aqui entra só a fila "estática" de colunas+ordenações. +O relatório definiu a "fila de trabalho" com 7 colunas ranqueadas (FU3): votos em jogo; meta/cobertura/delta; classe operacional; desempenho relativo (LQ/captura); rede + frescor; competição local; tendência — com **ordenação default por déficit descoberto** (meta − comprometido, decrescente; empate por votos em jogo) e uma ordenação de risco (municípios de defesa por frescor, o mais dormente primeiro). A lista de municípios atual (`MunicipalityList` + `loadMunicipalityListPageBundle`) já tem filtros/URL/overview/edição rápida; faltam as colunas derivadas e as ordenações de decisão. A hierarquia de triagem completa (5 níveis) vive no plano-mestre e vira produto no E11 — aqui entra só a fila "estática" de colunas+ordenações. ## Objetivos - Novas ordenações na URL (`?sort=deficit|risco-frescor|votos-em-jogo`) via `campaignListUrl.ts`, preservando filtros existentes. -- Colunas derivadas na linha da praça: déficit (meta − comprometido), cobertura %, votos em jogo (válidos projetados), LQ/captura, frescor (dias desde último pledge/sinal/atualização), badge "sem responsável". -- Visão do assessor: mesma fila filtrada às suas Praças (access já garante o escopo — `getAccessiblePlazaIds`). -- Overview da lista ganha "coluna da vergonha": contagem de praças priorizadas sem responsável (link filtrado). -- `leader` não vê a fila (colunas staff-only seguem o padrão de redaction dos view models `plazaViewModels.ts`). +- Colunas derivadas na linha do município: déficit (meta − comprometido), cobertura %, votos em jogo (válidos projetados), LQ/captura, frescor (dias desde último pledge/sinal/atualização), badge "sem responsável". +- Visão do assessor: mesma fila filtrada às seus municípios (access já garante o escopo — `getAccessibleMunicipalityIds`). +- Overview da lista ganha "coluna da vergonha": contagem de municípios priorizados sem responsável (link filtrado). +- `leader` não vê a fila (colunas staff-only seguem o padrão de redaction dos view models `municipalityViewModels.ts`). ## Decisões travadas -- **A fila é a própria lista de Praças com ordenações/colunas novas — não uma rota nova.** Evita segundo sistema de listas e mantém filtros/URL/paginação existentes (C6 shells). **Rejeitado:** rota `/campanha/fila` dedicada (duplicaria filtros e navegação; a fila de _sugestões_ do E11 é outra superfície e outro item); tabela full-row editável (anti-goal PRODUCT.md). -- **Déficit usa meta efetiva = `voteGoals.regular ?? suggestedGoal`** (mesma regra de E8), e praças N0/N1 (pós-E14) saem do topo por meta mínima — até E14, praças sem meta ficam no fim, não no topo. **Rejeitado:** ordenar por déficit bruto sem tratamento (praça perdida com meta lixo polui o topo — FU3). -- **i18n e naming:** `plazaQueueSort`, `deficit`, `freshnessDays`, `unassignedPriorityCount`; labels pt-BR ("Déficit", "Frescor", "Sem responsável"). +- **A fila é a própria lista de municípios com ordenações/colunas novas — não uma rota nova.** Evita segundo sistema de listas e mantém filtros/URL/paginação existentes (C6 shells). **Rejeitado:** rota `/campanha/fila` dedicada (duplicaria filtros e navegação; a fila de _sugestões_ do E11 é outra superfície e outro item); tabela full-row editável (anti-goal PRODUCT.md). +- **Déficit usa meta efetiva = `voteGoals.regular ?? suggestedGoal`** (mesma regra de E8), e municípios N0/N1 (pós-E14) saem do topo por meta mínima — até E14, municípios sem meta ficam no fim, não no topo. **Rejeitado:** ordenar por déficit bruto sem tratamento (município perdido com meta lixo polui o topo — FU3). +- **i18n e naming:** `municipalityQueueSort`, `deficit`, `freshnessDays`, `unassignedPriorityCount`; labels pt-BR ("Déficit", "Frescor", "Sem responsável"). ## Questões em aberto -- **Frescor conta o quê?** Opções: só `declaredAt`/`estimatedAt` de pledges | máx(pledge, `plaza.lastUpdateAt`) | incluir planos. **Recomendação:** máx(pledge mais recente, `lastUpdateAt`) — sinais de rede já alimentam `lastUpdateAt` via `plazaUpdate`; planos entram quando C12 tipar sinais. +- **Frescor conta o quê?** Opções: só `declaredAt`/`estimatedAt` de pledges | máx(pledge, `municipality.lastUpdateAt`) | incluir planos. **Recomendação:** máx(pledge mais recente, `lastUpdateAt`) — sinais de rede já alimentam `lastUpdateAt` via `municipalityUpdate`; planos entram quando C12 tipar sinais. - **Colunas visíveis por default no mobile?** **Recomendação:** déficit + cobertura + frescor (3), resto em disclosure — campo usa celular (PRODUCT.md). ## Abordagem proposta ```mermaid flowchart LR - Bundle["loadPlazaListPageBundle
(plazaPageData.ts)"] - Pot["plazaPotential/goalCoverage (E8)"] + Bundle["loadMunicipalityListPageBundle
(municipalityPageData.ts)"] + Pot["municipalityPotential/goalCoverage (E8)"] Fresh["freshness helper
(pledges + lastUpdateAt)"] - Sort["plazaQueueSort
(déficit | risco | votos)"] - List["PlazaList + PlazaListOverview
(colunas novas)"] + Sort["municipalityQueueSort
(déficit | risco | votos)"] + List["MunicipalityList + MunicipalityListOverview
(colunas novas)"] Bundle --> Sort Pot --> Sort Fresh --> Sort @@ -59,15 +59,15 @@ flowchart LR Componentes: -- **`src/utilities/plazaQueue.ts`**: composição das linhas (déficit, cobertura, frescor, flags) sobre o bundle existente — uma passada, sem N+1; ordenações puras testáveis em unit. -- **`PlazaList.tsx` / `PlazaListOverview.tsx`**: colunas/badges novas; select de ordenação em `PlazaFilters.tsx` (mesmo padrão auto-aplicado do fill-in filtros-auto). +- **`src/utilities/municipalityQueue.ts`**: composição das linhas (déficit, cobertura, frescor, flags) sobre o bundle existente — uma passada, sem N+1; ordenações puras testáveis em unit. +- **`MunicipalityList.tsx` / `MunicipalityListOverview.tsx`**: colunas/badges novas; select de ordenação em `MunicipalityFilters.tsx` (mesmo padrão auto-aplicado do fill-in filtros-auto). - **URL**: `campaignListUrl.ts` ganha `sort`; default continua o atual (nome) até o usuário escolher — a fila é opt-in via select "Ordenar por". -- **Sem migration, sem collection, sem server action nova** (edição em contexto reusa `plazaStaffFormActions`). +- **Sem migration, sem collection, sem server action nova** (edição em contexto reusa `municipalityStaffFormActions`). ## Dependências - Dura: **E8** (meta efetiva, votos em jogo, cobertura). Suaves: E10 (coluna classe passa a usar classificação relativa quando existir; até lá mostra a atual), E14 (nível modula o topo), C12 (frescor de sinais tipados). -- Reusa: `plazaPageData.ts`, `votePledgeData.ts`, `campaignListUrl.ts`, `PlazaFilters`, padrão B9 de edição em contexto. +- Reusa: `municipalityPageData.ts`, `votePledgeData.ts`, `campaignListUrl.ts`, `MunicipalityFilters`, padrão B9 de edição em contexto. ## Não escopo @@ -76,7 +76,7 @@ Componentes: ## Rabbit holes - **Virar data-grid.** "Só mais uma coluna editável" até virar planilha. **Mitigação:** colunas derivadas são read-only; edição só nos 3 Popovers já existentes (B9) + meta (E8). -- **Ordenação server-side vs. client.** O bundle já carrega o conjunto filtrado inteiro para overview/mapa (A9+); ordenar em memória é O(n log n) sobre ~436 — não abrir paginação SQL nova por isso. +- **Ordenação server-side vs. client.** O bundle já carrega o conjunto filtrado inteiro para overview/mapa (A9+); ordenar em memória é O(n log n) sobre ~435 — não abrir paginação SQL nova por isso. ## Adiado com gatilho @@ -86,5 +86,5 @@ Componentes: - `docs/roadmap.md` (Inteligência de campanha, E9) · [plano-mestre](inteligencia-campanha.md) (fila canônica) - `docs/research/relatorio-entrevista-persona-campanha.md` FU3 (colunas e ordenações), §6.2 (triagem), D3 ("a alocação se decide na lista") -- `src/utilities/plazaPageData.ts`, `src/utilities/votePledgeData.ts`, `src/components/campaign/PlazaList.tsx`, `src/components/campaign/PlazaListOverview.tsx`, `src/components/campaign/PlazaFilters.tsx`, `src/utilities/campaignListUrl.ts` +- `src/utilities/municipalityPageData.ts`, `src/utilities/votePledgeData.ts`, `src/components/campaign/MunicipalityList.tsx`, `src/components/campaign/MunicipalityListOverview.tsx`, `src/components/campaign/MunicipalityFilters.tsx`, `src/utilities/campaignListUrl.ts` - AGENTS.md — access por papel, view models com redaction, URLs com chaves em inglês diff --git a/docs/plans/filtros-auto-pracas.md b/docs/plans/filtros-auto-pracas.md index 843c378d..0f4cec2a 100644 --- a/docs/plans/filtros-auto-pracas.md +++ b/docs/plans/filtros-auto-pracas.md @@ -1,13 +1,15 @@ -# Filtros auto-aplicados na lista de Praças +# Filtros auto-aplicados na lista de municípios -Status: entregue em código (2026-07-21; deploy pendente com remodelagem) -Atualizado em: 2026-07-21 +Status: entregue (2026-07-21; em produção desde 2026-07-23) +Atualizado em: 2026-07-24 Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins) -Impeccable: B — encaixe em `PlazaFilters` (`/campanha/pracas`); sem rota nova -Appetite: ~0,5 dia eng; só client em `PlazaFilters` (+ pending a11y); sem migration +Impeccable: B — encaixe em `MunicipalityFilters` (`/campanha/municipios`); sem rota nova +Appetite: ~0,5 dia eng; só client em `MunicipalityFilters` (+ pending a11y); sem migration Responsável: — -_Revisão 2026-07-21 (pós-implementação + `/simplify` + capture-review-debts): debounce 1s + Enter imediato + pending UI; `commitNavigation` com `buildPlazaFiltersKey` no-op; `shouldUpdatePlazaSearchUrl` em `plazaUi.ts`; testes `tests/unit/plazaUi.unit.spec.ts`. Débitos S1–S2 registrados em Adiado (não reabrir no simplify)._ +> **Revisão 2026-07-24 (remodelagem M1 + hardening):** identificadores renomeados `plaza*` → `municipality*` (`MunicipalityFilters`, `buildMunicipalityFiltersKey`, `shouldUpdateMunicipalitySearchUrl` em `municipalityUi.ts`, rota `/campanha/municipios`); nomes antigos abaixo são da época. O hardening 2026-07-23 entregou o **shell compartilhado de pending** (`CampaignListPendingBoundary` + `useCampaignListPending`), fechando um dos itens Adiados. + +_Revisão 2026-07-21 (pós-implementação + `/simplify` + capture-review-debts): debounce 1s + Enter imediato + pending UI; `commitNavigation` com no-op key; testes unit. Débitos S1–S2 registrados em Adiado (não reabrir no simplify)._ ## Design (Impeccable) @@ -106,18 +108,19 @@ Componentes: ## Adiado com gatilho -- **Sync `search` local ↔ `state.q` no back/forward do browser.** `key={buildPlazaFiltersKey(state)}` remonta na maioria dos casos; edge case: campo pode mostrar texto stale após voltar/avançar. **Gatilho:** QA ou R6 reportar desync em `/campanha/pracas`. -- **Shell compartilhado de pending UI** (`data-pending` + `aria-busy` + `aria-live` "Atualizando resultados…"). Hoje só `ActionPlanFilters` + `PlazaFilters` (`SupporterFilters` ainda sem pending). **Gatilho:** 3ª lista de campanha com o mesmo padrão pending, ou R6/critique citar duplicação em ≥2 superfícies. -- **Mesmo padrão em `SupporterFilters` (e outras listas com Buscar).** Revisitar quando produto pedir ou quando R6/critique citar “Buscar” como atrito repetido em ≥2 listas. -- **Extrair `useDebouncedSearchParam`.** Revisitar no 3º call site idêntico (Praças + apoiadores + 1). +- **Sync `search` local ↔ `state.q` no back/forward do browser.** `key={buildMunicipalityFiltersKey(state)}` remonta na maioria dos casos; edge case: campo pode mostrar texto stale após voltar/avançar. **Gatilho:** QA ou R6 reportar desync em `/campanha/municipios`. +- ~~**Shell compartilhado de pending UI**~~ — **entregue no hardening 2026-07-23:** `CampaignListPendingBoundary`/`useCampaignListPending` compartilhados por `MunicipalityFilters`, `ActionPlanFilters`, `SupporterFilters`, `CampaignSearchForm` e paginação (dim + `aria-busy`). +- **Auto-aplicação com debounce nas demais listas com Buscar** (`SupporterFilters`, `CampaignSearchForm` de lideranças/organizações/dobradinhas — hoje têm pending compartilhado, mas o `q` ainda é submit-driven). Revisitar quando produto pedir ou quando R6/critique citar “Buscar” como atrito repetido em ≥2 listas. +- **Extrair `useDebouncedSearchParam`.** Revisitar no 3º call site idêntico (municípios + apoiadores + 1). ## Referências -- `docs/roadmap.md` (Fill-ins; B7 suave; R6) -- `src/components/campaign/PlazaFilters.tsx` — alvo da mudança +- `docs/roadmap.md` (Fill-ins; R6) +- `src/components/campaign/MunicipalityFilters.tsx` — alvo da mudança - `src/components/campaign/ActionPlanFilters.tsx` — precedente auto-apply + pending -- `src/components/campaign/SupporterFilters.tsx` — contraste (ainda tem Buscar no `q`) -- `src/app/(campaign)/campanha/(app)/pracas/page.tsx` — `buildPlazaFiltersKey` +- `src/components/campaign/SupporterFilters.tsx` / `CampaignSearchForm.tsx` — pending compartilhado; `q` ainda submit-driven +- `src/components/campaign/CampaignListPending.tsx` — shell de pending do hardening +- `src/app/(campaign)/campanha/(app)/municipios/page.tsx` — `buildMunicipalityFiltersKey` - `src/utilities/plazaUi.ts` — URL canônica - AGENTS.md — naming; superfície `(campaign)` - `PRODUCT.md` / `DESIGN.md` — Field Desk / clarity under pressure diff --git a/docs/plans/hover-mapa-pracas.md b/docs/plans/hover-mapa-pracas.md index 7b5cd54c..3d326297 100644 --- a/docs/plans/hover-mapa-pracas.md +++ b/docs/plans/hover-mapa-pracas.md @@ -1,6 +1,8 @@ # Hover no Mapa das Praças (destaque + total de votos + navegação) -Status: entregue 2026-07-21 (branch; deploy pendente com remodelagem) +> **Nota (2026-07-24):** entregue na era Praças e **em produção desde 2026-07-23**. A remodelagem M1 renomeou os identificadores `plaza*` → `municipality*` e as rotas `pracas/` → `municipios/`; os nomes neste plano são os da época. Nomes atuais: ver [remodelagem-municipios.md](remodelagem-municipios.md). + +Status: entregue 2026-07-21 (em produção desde 2026-07-23) Atualizado em: 2026-07-21 (navegação: desktop click / mobile 2º tap; B6 ✓ setStyle incremental) Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B10 — entregue 2026-07-21) Impeccable: B — encaixe em `PlazaMapPanel` / `BahiaMap` (sem rota nova) diff --git a/docs/plans/insight-dobradinha-2026.md b/docs/plans/insight-dobradinha-2026.md index ba2a115d..e1468863 100644 --- a/docs/plans/insight-dobradinha-2026.md +++ b/docs/plans/insight-dobradinha-2026.md @@ -1,19 +1,21 @@ # Insight: oportunidades de dobradinha 2026 -Status: rascunho -Atualizado em: 2026-07-17 -Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Campanha → Próximos ciclos") +Status: rascunho (gatilho externo: TSE publica candidaturas — pós-15/08) +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios) +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Próximos — Demais itens abertos, A6) Responsável: — +> **Revisão 2026-07-24:** desde a M4, as dobradinhas OPERACIONAIS já existem no produto — entidade `stateDeputy` + `municipality.stateDeputies` / `leadership.stateDeputies` + vertical `/campanha/dobradinhas`. A6 vira a **camada de insight TSE** sobre esse registro: sugerir candidatos 2026 (por força local 2022 + alinhamento) para o staff registrar/priorizar como `stateDeputy` — não criar um segundo registro paralelo. Referências antigas a "núcleo" abaixo foram renomeadas para município. + ## Contexto -"Dobradinha" é a campanha conjunta entre dois candidatos que somam estrutura e votos num mesmo território. Sabendo, via baseline TSE 2022 + Fase 5 (ver [baseline-eleitoral-tse.md](baseline-eleitoral-tse.md)), **quais candidatos de 2022 voltam a concorrer em 2026** (`runningAgain2026`), podemos sugerir oportunidades de dobradinha por núcleo/território — priorizando por (a) **alinhamento político** com a chapa PT/Solla e (b) **força eleitoral local** (votos recebidos ali em 2022). É o insight mais estratégico do conjunto e depende de dados de 2026, por isso é plano separado. +"Dobradinha" é a campanha conjunta entre dois candidatos que somam estrutura e votos num mesmo território. Sabendo, via baseline TSE 2022 + Fase 5 (ver [baseline-eleitoral-tse.md](baseline-eleitoral-tse.md)), **quais candidatos de 2022 voltam a concorrer em 2026** (`runningAgain2026`), podemos sugerir oportunidades de dobradinha por município/território — priorizando por (a) **alinhamento político** com a chapa PT/Solla e (b) **força eleitoral local** (votos recebidos ali em 2022). É o insight mais estratégico do conjunto e depende de dados de 2026, por isso é plano separado. ## Objetivos -- Por núcleo, listar candidatos que concorrem de novo em 2026 (`runningAgain2026=sim`) com votos 2022 na geografia, partido/coligação e tier de alinhamento. +- Por município, listar candidatos que concorrem de novo em 2026 (`runningAgain2026=sim`) com votos 2022 na geografia, partido/coligação e tier de alinhamento. - Ranquear por score combinando alinhamento (peso a definir) e força eleitoral local (votos 2022). -- Exibir no detalhe do núcleo (aba overview) a lista ranqueada de potenciais parceiros de dobradinha; no overview, agregar top parceiros por território filtrado. +- Exibir no detalhe do município (aba Visão geral) a lista ranqueada de potenciais parceiros de dobradinha; no overview, agregar top parceiros por território filtrado. - Habilitar só quando 2026 estiver carregado; enquanto não, mostrar "indisponível até a candidatura de 2026". ## Decisões travadas @@ -48,14 +50,14 @@ flowchart LR ``` - **Helper** `src/lib/electionAlliances.ts`: mapeamento partido→tier (versionado, com proveniência das coligações 2022/2026). -- **Helper** `src/lib/electionInsights.ts`: `computeDobradinhaOpportunities(candidates2026, votes2022, alliances, nucleusGeography)` → `Array<{ candidate, party, coalition, tier, votes2022, score }>` ordenado por score. -- **Componente** `src/components/campaign/DobradinhaOpportunities.tsx` (server). Overview: bloco agregado no `NucleusListOverview`. +- **Helper** `src/lib/electionInsights.ts`: `computeDobradinhaOpportunities(candidates2026, votes2022, alliances, municipalityGeography)` → `Array<{ candidate, party, coalition, tier, votes2022, score }>` ordenado por score. +- **Componente** `src/components/campaign/DobradinhaOpportunities.tsx` (server). Overview: bloco agregado no `MunicipalityListOverview`. - **Teste int** cenários: sem 2026 carregado (`desconhecido` → estado indisponível); só adversários; misto. ## Arquivos a criar/alterar - Criar: `src/lib/electionAlliances.ts`, `src/components/campaign/DobradinhaOpportunities.tsx`. -- Alterar: `src/lib/electionInsights.ts` (nova função), `nucleos/[slug]/page.tsx` + `nucleusDetailPageData.ts`, overview da lista. +- Alterar: `src/lib/electionInsights.ts` (nova função), `municipios/[slug]/page.tsx` + `municipalityPageData.ts`, overview da lista. ## Dependências @@ -71,4 +73,4 @@ flowchart LR - [baseline-eleitoral-tse.md](baseline-eleitoral-tse.md) — baseline + Fase 5 (2026) - [insight-inteligencia-competitiva.md](insight-inteligencia-competitiva.md) — fornece o quadro competitivo base -- AGENTS.md — naming, "Bahia implícita no Núcleo" +- AGENTS.md — naming, modelo Municípios diff --git a/docs/plans/inteligencia-campanha.md b/docs/plans/inteligencia-campanha.md index 0c4f8149..289b80f7 100644 --- a/docs/plans/inteligencia-campanha.md +++ b/docs/plans/inteligencia-campanha.md @@ -1,68 +1,68 @@ # Inteligência de campanha — do relatório de discovery ao produto (programa E8–E15 + B13 + C12) Status: rascunho (plano-mestre; itens fatiados individualmente) -Atualizado em: 2026-07-21 +Atualizado em: 2026-07-24 (referências sincronizadas pós-remodelagem Municípios + hardening; remodelagem em produção desde 2026-07-23) Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha"; trilhas E/B/C) Impeccable: misto por item — E15 = A; E8/E9/C12/E10/B13/E14/E12 = B; E11/E13 = C (classe final no plano de cada fatia) -Appetite: programa ~2–3 semanas eng no total, fatiado em slices de ~1–3 dias; nenhuma fatia bloqueia deploy da remodelagem +Appetite: programa ~2–3 semanas eng no total, fatiado em slices de ~1–3 dias Responsável: — Fonte de produto: [`docs/research/relatorio-entrevista-persona-campanha.md`](../research/relatorio-entrevista-persona-campanha.md) (aprovado 2026-07-21) — kernel §2, playbook §6 (25 padrões P/T/I/J/K), OST §7 (O-A…O-K), anti-goals §8. Compêndio: [`docs/research/literatura-campanha-deputado-federal-ba.md`](../research/literatura-campanha-deputado-federal-ba.md). ## Contexto -O discovery (literatura → persona → entrevista sintética, 2026-07-21) fixou o diagnóstico: a disputa de DF na Bahia é uma conta de quociente fragmentada (cadeira ~80–150 mil votos; topo estadual 4,71% dos válidos), o gargalo é converter lealdade de campo em voto nominal praça a praça via rede de mediação, e a informação dessa rede hoje é oral e sem série. O Teqo já tem o **sistema de registro** (Praças, lideranças, pledges, planos, demandas, baseline TSE 2014–2022 com brancos/nulos/abstenção por cargo). O que falta é a **camada de inteligência**: metas derivadas dos dados, leitura relativa do território, fila de decisão priorizada, sugestões dado→decisão com humano no loop, e o registro ex-ante que torna tudo auditável (backtest). Anti-goal do programa inteiro: "planilha chique" — KPI absoluto, cadastro bruto, mapa 0–100%. +O discovery (literatura → persona → entrevista sintética, 2026-07-21) fixou o diagnóstico: a disputa de DF na Bahia é uma conta de quociente fragmentada (cadeira ~80–150 mil votos; topo estadual 4,71% dos válidos), o gargalo é converter lealdade de campo em voto nominal município a município via rede de mediação, e a informação dessa rede hoje é oral e sem série. O Teqo já tem o **sistema de registro** (Municípios, lideranças, pledges, planos, demandas, baseline TSE 2014–2022 com brancos/nulos/abstenção por cargo). O que falta é a **camada de inteligência**: metas derivadas dos dados, leitura relativa do território, fila de decisão priorizada, sugestões dado→decisão com humano no loop, e o registro ex-ante que torna tudo auditável (backtest). Anti-goal do programa inteiro: "planilha chique" — KPI absoluto, cadastro bruto, mapa 0–100%. ## Gaps de estrutura de dados (o que não estamos preparados para coletar/usar) -Auditoria relatório × schema atual (2026-07-21). O que **já existe e cobre**: `electionTally` tem `aptos/comparecimento/abstencoes/votosValidos/votosBranco/votosNulo` por cargo×ano×cidade×zona (roll-off e válidos projetados são deriváveis); `electionCandidate` tem `party`/`coalition`/`runningAgain2026` (P3/herança aberta); `plaza` tem `voteGoals` (bom/regular/mínimo), `expectedVotes`, `priority`, `politicalTrend`, `dobradinhaNotes`, `advisors`, `lastUpdateAt`; `votePledge` tem declarado×estimado com autores/datas; `plazaUpdate` é imutável; `bahiaTerritories.ts` já mapeia município→Território de Identidade; `actionPlan` registra eventos com Praça/status/presença do deputado. +Auditoria relatório × schema atual (2026-07-21). O que **já existe e cobre**: `electionTally` tem `aptos/comparecimento/abstencoes/votosValidos/votosBranco/votosNulo` por cargo×ano×cidade×zona (roll-off e válidos projetados são deriváveis); `electionCandidate` tem `party`/`coalition`/`runningAgain2026` (P3/herança aberta); `municipality` tem `voteGoals` (bom/regular/mínimo), `expectedVotes`, `priority`, `politicalTrend`, `dobradinhaNotes`, `advisors`, `lastUpdateAt`; `votePledge` tem declarado×estimado com autores/datas; `municipalityUpdate` é imutável; `bahiaTerritories.ts` já mapeia município→Território de Identidade; `actionPlan` registra eventos com município/status/presença do deputado. Desde o hardening 2026-07-23 existe também o artefato commitado `src/lib/electionAggregates/bahia-federal-baseline.json` (loader `bahiaElectionAggregates.ts`): série Solla + válidos federais 2014/2018/2022 por município, já cortada na geografia do catálogo — insumo direto para válidos projetados/captura sem re-query de `electionTally`. -| # | Gap | Hoje | Como fechar | Fatia | -| --- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | -| G1 | **Trajetória de pledge** ("o que sabíamos em 15/08") — valor atual sobrescreve o anterior | `votePledge` guarda só o corrente | `versions: true` (sem drafts) em `votePledge` + leitura da série; **nunca sobrescrever** vira garantia de plataforma | C12 | -| G2 | **Sinal qualitativo de rede tipado** (invasão, esfriamento, visita de adversário, proposta a broker; fonte; triangulação) | `plazaUpdate.kind` = semanal/urgente/nota, texto livre | Novo kind `sinal` + campos `signalType`, `signalSource`, `triangulated` (staff-only) | C12 | -| G3 | **Esforço/agenda por praça** (o lado privado que evapora: dias de candidato, origem da visita, recurso) | `actionPlan` tem evento+status; sem origem nem custo | `actionPlan.origin` (`dado \| pedido_broker \| obrigacao_politica` — J-B) + `resourceCost` staff-only opcional | C12 | -| G4 | **Decisão registrada ex-ante** (sugestão dada, aceita/descartada, leitura alternativa, foto da classificação no momento) | Nada | Nova collection `allocationDecision` (plaza, patternId, suggestion, decision `aceita\|descartada\|adiada`, alternativeReading, snapshot, decidedBy/At) — também registra mudanças de nível N0–N4 | C12 | -| G5 | **Meta estadual e decomposição derivada** (QE projetado, faixa da cadeira, potencial por praça) | `plaza.voteGoals` manual, sem conta de cima para baixo nem cobertura | Global `campaignGoals` (meta estadual, margem, ano-base) + utilities de potencial (válidos projetados, captura, headroom) e cobertura Σpledges÷meta | E8 | -| G6 | **Definição de "campo" por ano** (share intracampo, teto do campo) | `electionCandidate.party` existe; não há lista de partidos do campo por eleição | Lib estática curada `src/lib/campoParties.ts` (ano → siglas/números) — dado de configuração, não collection | E8 | -| G7 | **Nível de envolvimento N0–N4** com regras de movimento e sinais de reversão | `plaza.priority` = alta/normal | `plaza.engagementLevel` (n0…n4) + `levelNote`/`levelChangedAt`; mudança de nível registrada como `allocationDecision`; `priority` mapeado e aposentado | E14 | -| G8 | **Frescor como sinal de 1ª classe** | derivável (`declaredAt`, `lastUpdateAt`) mas não exposto/normatizado | Utilities de frescor + exposição na fila/lista ("coluna da vergonha": praça priorizada sem responsável/sem sinal) — sem migration | E9 | -| G9 | Perfil de abstenção por município (sexo/idade/escolaridade, TSE Eleitorado) | não importado | **Adiado com gatilho** (só se I-C virar prática): extensão do seed TSE | — | -| G10 | Malha Regiões Imediatas IBGE (teste de sensibilidade MAUP) | só TI (`bahiaTerritories.ts`) | **Adiado com gatilho** (decisão cara sustentada por análise regional divergente): variante do `build:geometries` | — | +| # | Gap | Hoje | Como fechar | Fatia | +| --- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| G1 | **Trajetória de pledge** ("o que sabíamos em 15/08") — valor atual sobrescreve o anterior | `votePledge` guarda só o corrente | `versions: true` (sem drafts) em `votePledge` + leitura da série; **nunca sobrescrever** vira garantia de plataforma | C12 | +| G2 | **Sinal qualitativo de rede tipado** (invasão, esfriamento, visita de adversário, proposta a broker; fonte; triangulação) | `municipalityUpdate.kind` = semanal/urgente/nota, texto livre | Novo kind `sinal` + campos `signalType`, `signalSource`, `triangulated` (staff-only) | C12 | +| G3 | **Esforço/agenda por município** (o lado privado que evapora: dias de candidato, origem da visita, recurso) | `actionPlan` tem evento+status; sem origem nem custo | `actionPlan.origin` (`dado \| pedido_broker \| obrigacao_politica` — J-B) + `resourceCost` staff-only opcional | C12 | +| G4 | **Decisão registrada ex-ante** (sugestão dada, aceita/descartada, leitura alternativa, foto da classificação no momento) | Nada | Nova collection `allocationDecision` (municipality, patternId, suggestion, decision `aceita\|descartada\|adiada`, alternativeReading, snapshot, decidedBy/At) — também registra mudanças de nível N0–N4 | C12 | +| G5 | **Meta estadual e decomposição derivada** (QE projetado, faixa da cadeira, potencial por município) | `municipality.voteGoals` manual, sem conta de cima para baixo nem cobertura | Global `campaignGoals` (meta estadual, margem, ano-base) + utilities de potencial (válidos projetados, captura, headroom) e cobertura Σpledges÷meta | E8 | +| G6 | **Definição de "campo" por ano** (share intracampo, teto do campo) | `electionCandidate.party` existe; não há lista de partidos do campo por eleição | Lib estática curada `src/lib/campoParties.ts` (ano → siglas/números) — dado de configuração, não collection | E8 | +| G7 | **Nível de envolvimento N0–N4** com regras de movimento e sinais de reversão | `municipality.priority` = alta/normal | `municipality.engagementLevel` (n0…n4) + `levelNote`/`levelChangedAt`; mudança de nível registrada como `allocationDecision`; `priority` mapeado e aposentado | E14 | +| G8 | **Frescor como sinal de 1ª classe** | derivável (`declaredAt`, `lastUpdateAt`) mas não exposto/normatizado | Utilities de frescor + exposição na fila/lista ("coluna da vergonha": município priorizado sem responsável/sem sinal) — sem migration | E9 | +| G9 | Perfil de abstenção por município (sexo/idade/escolaridade, TSE Eleitorado) | não importado | **Adiado com gatilho** (só se I-C virar prática): extensão do seed TSE | — | +| G10 | Malha Regiões Imediatas IBGE (teste de sensibilidade MAUP) | só TI (`bahiaTerritories.ts`) | **Adiado com gatilho** (decisão cara sustentada por análise regional divergente): variante do `build:geometries` | — | Migrations do programa: uma consolidada por fatia que toca schema (C12: versions+kinds+origin+`allocationDecision`; E8: global `campaignGoals`; E14: `engagementLevel`). Todas antes do congelamento (~20/09). ## A fila de prioridade do coordenador (desenho canônico) -Uma única fila ordena "todos" heterogêneos (gatilhos do playbook, demandas, praças sem responsável, sinais não triangulados, pledges velhos) pela hierarquia do relatório (§6.2), estendida aos objetos do produto: +Uma única fila ordena "todos" heterogêneos (gatilhos do playbook, demandas, municípios sem responsável, sinais não triangulados, pledges velhos) pela hierarquia do relatório (§6.2), estendida aos objetos do produto: -1. **Estoque em risco confirmado** — P1 ameaçado/P8 triangulado em praça de defesa relevante; demanda `escalada` de praça N3/N4. -2. **Falha de canal** — P4 (broker único ≥ limiar), liderança-hub esfriando, praça priorizada cujo responsável saiu. -3. **Cobertura zero onde a meta exige** — P6 priorizada, praça N2+ sem responsável/lideranças ("coluna da vergonha"), metas sem dono. -4. **Oportunidade com janela** — P3 herança aberta (`runningAgain2026 = false`), P5 expansão quente, convites pendentes de liderança nova, J-A (elegível madura com giro passando perto). +1. **Estoque em risco confirmado** — P1 ameaçado/P8 triangulado em município de defesa relevante; demanda `escalada` de município N3/N4. +2. **Falha de canal** — P4 (broker único ≥ limiar), liderança-hub esfriando, município priorizado cujo responsável saiu. +3. **Cobertura zero onde a meta exige** — P6 priorizado, município N2+ sem responsável/lideranças ("coluna da vergonha"), metas sem dono. +4. **Oportunidade com janela** — P3 herança aberta (`runningAgain2026 = false`), P5 expansão quente, convites pendentes de liderança nova, J-A (município elegível maduro com giro passando perto). 5. **Otimização/higiene** — T4 benchmark, T5 agenda dispersa, pledges sem atualização há N semanas, planos `realizado` sem resultado, K-C metas implausíveis. -Regras transversais (de §6.2/§6.4, viram comportamento do produto): **barato-antes-de-caro** (todo gatilho gera primeiro a checagem de 48h para o assessor; só gatilho persistente/triangulado sobe para a reunião de realocação); **desempate por votos em jogo**; **histerese** (realocação cara exige 2 semanas de persistência; nível 1 fura a fila); **gatilho de ausência dispara auditoria de registro, nunca ação de campo**. A fila tem duas visões: _do assessor_ (checagens e tarefas das suas Praças) e _do coordenador_ (decisões de recurso caro, pauta da reunião quinzenal). +Regras transversais (de §6.2/§6.4, viram comportamento do produto): **barato-antes-de-caro** (todo gatilho gera primeiro a checagem de 48h para o assessor; só gatilho persistente/triangulado sobe para a reunião de realocação); **desempate por votos em jogo**; **histerese** (realocação cara exige 2 semanas de persistência; nível 1 fura a fila); **gatilho de ausência dispara auditoria de registro, nunca ação de campo**. A fila tem duas visões: _do assessor_ (checagens e tarefas dos seus municípios) e _do coordenador_ (decisões de recurso caro, pauta da reunião quinzenal). ## 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 Σ(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 | +| ID | Fatia (plano) | Entrega | OST | Classe | Appetite | Janela | +| --- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ------ | -------- | --------- | +| E8 | **[Conta da cadeira](conta-da-cadeira.md)** | Global `campaignGoals`; potencial por município (válidos projetados, captura do campo, share intracampo via `campoParties.ts`, roll-off, headroom); decomposição meta estadual→município 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 `municipalityUpdate`; `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 no município; 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 município; 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+município crítico, 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 - **Sugestão nunca decide; staff decide.** O motor detecta e faz diagnóstico diferencial estrutural; escolher/executar é humano, e a decisão (com leitura descartada) é registrada (relatório §6.3; anti-goal 8). **Rejeitado:** automação de ações (erro sem dono, custo político invisível ao dado); estimar probabilidades/elasticidades sem dado (falsa precisão). -- **Denominadores: válidos projetados do cargo, nunca aptos** (I-B; abstenção estrutural >35% em praças pequenas infla potencial). **Rejeitado:** aptos (viés sistemático pró-praça vazia); válidos da majoritária (cargo errado). +- **Denominadores: válidos projetados do cargo, nunca aptos** (I-B; abstenção estrutural >35% em municípios pequenos infla potencial). **Rejeitado:** aptos (viés sistemático pró-município vazio); válidos da majoritária (cargo errado). - **Histórico por versions do Payload, não collection paralela de snapshots** (G1). Menos código, mesma garantia, admin de graça. **Rejeitado:** collection `pledgeSnapshot` (duplicaria access/hooks); sobrescrever com `updatedAt` (destrói o backtest). - **Nível N0–N4 é campo staff-only com access negando `leader`** — vocabulário duplo é requisito de produto, não polimento (relatório §6.8/anti-goal 11). **Rejeitado:** derivar nível só de `priority` existente (semântica insuficiente); expor nível na visão da liderança. - **"Campo" por ano é lib estática curada** (`campoParties.ts`), editável por PR. **Rejeitado:** collection administrável (federações mudam raramente; curadoria exige contexto político, não CRUD). @@ -72,7 +72,7 @@ Regras transversais (de §6.2/§6.4, viram comportamento do produto): **barato-a - **Quais padrões entram no motor v1?** Opções: todos os 25 | subconjunto de maior confiança. **Recomendação:** os 8 listados em E11 (dados 100% disponíveis pós-E8/C12, menor risco de falso positivo); T/I/J na fase 2 após 2 semanas de uso real. - **Meta estadual inicial?** Opções: QE cheio | faixa da cadeira 2022 (~80–150 mil) com margem. **Recomendação:** faixa da cadeira + margem definida pelo coordenador no global `campaignGoals` _(validar com produto na sessão Wed)_. -- **Limiar de broker único (P4)?** **Recomendação:** iniciar em 50% dos pledges da praça / top-3 ≥ 40% do total, marcado calibrável; revisar no primeiro mês. +- **Limiar de broker único (P4)?** **Recomendação:** iniciar em 50% dos pledges do município / top-3 ≥ 40% do total, marcado calibrável; revisar no primeiro mês. ## Abordagem proposta (visão de programa) @@ -103,18 +103,18 @@ flowchart TD E11 -.decisões acumuladas.-> E15 ``` -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`. +Reuso obrigatório (depth check): `municipalityElectoralBaseline.ts`/`municipalityElectionGeography.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`), `aggregatePledgesByMunicipality`/`loadMunicipalityListPageBundle` (A9+), `BahiaMap`+`bahiaMapStyle.ts` (B6/B11/B12), `bahiaTerritories.ts` (TI), `bahiaElectionAggregates.ts` (artefato commitado — série Solla/válidos por município), `electionInsights.ts` (substituição E10), `CampaignPageShell`/`CampaignMetricStrip`, `src/utilities/access/*` (via re-export `campaignAccess.ts`), `withPayloadTransaction`. ## Dependências -- 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). +- Duras: E8→E9/E10/E12/E13; C12→E11/E15. Satisfeitas: deploy da remodelagem (schema `municipality`/`votePledge` **em produção desde 2026-07-23**) e **A10** (cenários entregues — faixa pessimista/média/otimista antes da cobertura). 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 - Previsão estatística de votos e elasticidades (fora de escopo do ciclo — roadmap "Fora de escopo"; E15 só compara ex-ante×resultado). - Perfil de abstenção TSE e malha IBGE Imediatas (G9/G10 — adiados com gatilho acima). -- Polígonos de Praças-zona (B8) e qualquer mudança de geografia. +- Polígonos dos Municípios-zona de Salvador (B8 F2) e qualquer mudança de geografia. - Automação de mensagens/disparo (Res. TSE 23.610 art. 33; anti-goal). - Import de planilhas de projeção (decisão E4 mantida). @@ -122,7 +122,7 @@ Reuso obrigatório (depth check): `plazaElectoralBaseline.ts`/`plazaElectionGeog - **Motor v1 tentando cobrir os 25 padrões.** Se alguém "só adicionar mais um": cada padrão exige dado confiável + descartes + calibração; metade dos 25 depende de sinais que só existirão com C12 rodando semanas. **Mitigação:** subconjunto fixo em E11; fase 2 com gatilho (2 semanas de uso + primeiros `allocationDecision`). - **Score de elegibilidade virar otimizador de rota.** Roteirização (TSP, mapas de estrada) é produto inteiro. **Mitigação:** E13 ordena e agrupa por contiguidade de TI; rota fina é humana. -- **Fila única virar segundo sistema de tarefas.** Já existem demandas/planos/tasks. **Mitigação:** a fila REFERENCIA objetos existentes (demanda, plano, praça), não cria tarefa própria fora de `allocationDecision`. +- **Fila única virar segundo sistema de tarefas.** Já existem demandas/planos/tasks. **Mitigação:** a fila REFERENCIA objetos existentes (demanda, plano, município), não cria tarefa própria fora de `allocationDecision`. - **Rollup TI sem salvaguardas** (média que mente). **Mitigação:** salvaguardas MAUP são requisito de aceite de E12, não polimento. ## Adiado com gatilho @@ -136,7 +136,7 @@ Reuso obrigatório (depth check): `plazaElectoralBaseline.ts`/`plazaElectionGeog - `docs/research/relatorio-entrevista-persona-campanha.md` — §2 kernel, §5 análises, §6 playbook completo, §7 OST, §8 anti-goals - `docs/research/literatura-campanha-deputado-federal-ba.md` — fontes e implicações (§9) -- `src/collections/Plaza.ts`, `src/collections/VotePledge.ts`, `src/collections/PlazaUpdate.ts`, `src/collections/ActionPlan.ts`, `src/collections/ElectionTally.ts`, `src/collections/ElectionCandidate.ts` — schema auditado nos gaps -- `src/utilities/plazaElectoralBaseline.ts`, `src/utilities/votePledgeData.ts`, `src/utilities/plazaMapData.ts`, `src/lib/electionInsights.ts`, `src/lib/bahiaTerritories.ts` — pontos de encaixe -- `docs/plans/remodelagem-pracas.md` — precedente de plano-mestre; `docs/plans/escala-percentual-mapa-pracas.md` — precedente do mapa +- `src/collections/Municipality.ts`, `src/collections/VotePledge.ts`, `src/collections/MunicipalityUpdate.ts`, `src/collections/ActionPlan.ts`, `src/collections/ElectionTally.ts`, `src/collections/ElectionCandidate.ts` — schema auditado nos gaps +- `src/utilities/municipalityElectoralBaseline.ts`, `src/utilities/votePledgeData.ts`, `src/utilities/municipalityMapData.ts`, `src/lib/electionInsights.ts`, `src/lib/bahiaTerritories.ts` — pontos de encaixe +- `docs/plans/remodelagem-municipios.md` — plano-mestre vigente da remodelagem (precedente: `remodelagem-pracas.md`); `docs/plans/escala-percentual-mapa-pracas.md` — precedente do mapa (identificadores da época; renomeados na M1) - AGENTS.md — migrations (`pnpm migrate:create`), access (`overrideAccess: false`), transações, naming diff --git a/docs/plans/mapa-pracas-filtrado.md b/docs/plans/mapa-pracas-filtrado.md index a38da937..07525a13 100644 --- a/docs/plans/mapa-pracas-filtrado.md +++ b/docs/plans/mapa-pracas-filtrado.md @@ -1,6 +1,8 @@ # Mapa das Praças filtrado pela lista -Status: entregue +> **Nota (2026-07-24):** entregue na era Praças e **em produção desde 2026-07-23**. A remodelagem M1 renomeou os identificadores `plaza*` → `municipality*` e as rotas `pracas/` → `municipios/`; os nomes neste plano são os da época. Nomes atuais: ver [remodelagem-municipios.md](remodelagem-municipios.md). + +Status: entregue (em produção desde 2026-07-23) Atualizado em: 2026-07-21 (gatilho fitBounds → B12) Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B7 — entregue 2026-07-21) Impeccable: B — encaixe em `/campanha/pracas` (`PlazaMapPanel` + loader; sem rota nova) diff --git a/docs/plans/motor-de-sugestoes.md b/docs/plans/motor-de-sugestoes.md index 8b176c82..b4a2086b 100644 --- a/docs/plans/motor-de-sugestoes.md +++ b/docs/plans/motor-de-sugestoes.md @@ -1,7 +1,7 @@ # E11 — Motor de sugestões v1 (dado → decisão com humano no loop) Status: rascunho -Atualizado em: 2026-07-21 +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening) Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", E11; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md)) Impeccable: C — superfície nova de sugestões (painel/fila em `/campanha`), sem design-ref Appetite: ~3 dias eng; sem migration própria (lê E8/E10/E14 e grava via `allocationDecision` de C12) @@ -9,53 +9,53 @@ Responsável: — ## Design (Impeccable) -Âncoras: `PRODUCT.md` (princípios 2, 5 e 7) / `DESIGN.md` (register `product`) · `CampaignPageShell`/`CampaignMetricStrip` · precedente de stack de insights (Alert + badge) do detalhe da Praça. +Âncoras: `PRODUCT.md` (princípios 2, 5 e 7) / `DESIGN.md` (register `product`) · `CampaignPageShell`/`CampaignMetricStrip` · precedente de stack de insights (Alert + badge) do detalhe do município. Na implementação: shape → craft → critique → polish (classe C). Brief compacto: -- **Persona / contexto:** coordenador abrindo o Teqo segunda de manhã ("o que mudou? onde ajo?"); assessor com 20 Praças recebendo as checagens baratas da semana. -- **Job principal:** o grosso da análise chega pronto — praças com gatilho, diagnóstico diferencial e menu de ações; o humano escolhe, e o descarte também é registro. +- **Persona / contexto:** coordenador abrindo o Teqo segunda de manhã ("o que mudou? onde ajo?"); assessor com 20 municípios recebendo as checagens baratas da semana. +- **Job principal:** o grosso da análise chega pronto — municípios com gatilho, diagnóstico diferencial e menu de ações; o humano escolhe, e o descarte também é registro. - **Estratégia de cor:** Restrained; urgência via posição na triagem (níveis 1–5), não via alarme colorido — só o nível 1 usa o vermelho de campanha. - **Edit where you see:** sim — aceitar/adiar/descartar direto no card da sugestão; descarte exige leitura alternativa (1 campo). - **Anti-goals:** sugestão executada automaticamente; feed infinito estilo rede social; score numérico com falsa precisão ("87% de chance"); expor limiares exatos na UI (gaming — G4). ## Contexto -Relatório §6: catálogo de 25 padrões dado→decisão (P/T/I/J/K) com triagem (§6.2), divisão análise×julgamento (§6.3 — máquina detecta e diagnostica o estrutural; humano escolhe/executa) e modos de falha com mitigação (§6.4 — falso positivo de registro, viés da base, gaming, oráculo, churn). Pós-E8/E10/E14/C12, todos os insumos dos 8 padrões de maior confiança existem: P1 (reduto dormente/ameaçado), P2 (ataque campo forte), P3 (correligionário na frente — `runningAgain2026` dá a herança aberta), P5 (expansão acima do padrão), P6 (praça grande sem rede), P7 (pledges estagnados), K-A (não responde a investimento), K-B (perdida vestida de oportunidade). A "pauta do silêncio" (praças sem gatilho nenhum, revisadas como pergunta mensal) integra a v1 por ser barata e antídoto do viés da base. +Relatório §6: catálogo de 25 padrões dado→decisão (P/T/I/J/K) com triagem (§6.2), divisão análise×julgamento (§6.3 — máquina detecta e diagnostica o estrutural; humano escolhe/executa) e modos de falha com mitigação (§6.4 — falso positivo de registro, viés da base, gaming, oráculo, churn). Pós-E8/E10/E14/C12, todos os insumos dos 8 padrões de maior confiança existem: P1 (reduto dormente/ameaçado), P2 (ataque campo forte), P3 (correligionário na frente — `runningAgain2026` dá a herança aberta), P5 (expansão acima do padrão), P6 (município grande sem rede), P7 (pledges estagnados), K-A (não responde a investimento), K-B (perdida vestida de oportunidade). A "pauta do silêncio" (municípios sem gatilho nenhum, revisados como pergunta mensal) integra a v1 por ser barata e antídoto do viés da base. ## Objetivos -- **Avaliador de gatilhos** puro e determinístico: praça + derivados (E8/E10/E14) + sinais/frescor (C12) → lista de `TriggeredPattern` (patternId, fatores, nível de triagem 1–5, leitura provável, descartes, menu de ações com estatuto/fonte, contraindicação) — conteúdo textual vem de um catálogo versionado em código espelhando o relatório §6. -- **Superfície:** painel "Sugestões" no dashboard (top-N por triagem) + seção na Praça (gatilhos dela); visão do assessor filtrada às suas Praças com as checagens baratas ("checar rede em 48h") destacadas. +- **Avaliador de gatilhos** puro e determinístico: município + derivados (E8/E10/E14) + sinais/frescor (C12) → lista de `TriggeredPattern` (patternId, fatores, nível de triagem 1–5, leitura provável, descartes, menu de ações com estatuto/fonte, contraindicação) — conteúdo textual vem de um catálogo versionado em código espelhando o relatório §6. +- **Superfície:** painel "Sugestões" no dashboard (top-N por triagem) + seção no município (gatilhos dela); visão do assessor filtrada às seus municípios com as checagens baratas ("checar rede em 48h") destacadas. - **Ações:** aceitar (vira nota/encaminhamento e grava `allocationDecision` com snapshot), adiar (some por X dias), descartar (exige `alternativeReading`); tudo transacional. - **Regras anti-falha (§6.4):** gatilho de ausência gera "auditar registro", nunca ação de campo; histerese — sugestão de realocação cara só aparece com persistência de 2 avaliações semanais (persistência via snapshot em `allocationDecision` de avaliações, sem tabela nova: recomputa e compara com a última decisão/adiamento registrado); limiares nunca renderizados com número exato. -- **Pauta do silêncio:** lista mensal das praças N2+ sem nenhum gatilho e sem sinal — "silêncio é pergunta, não conforto". -- Access: staff; visão do assessor escopada (`getAccessiblePlazaIds`); `leader` não vê nada disso. +- **Pauta do silêncio:** lista mensal dos municípios N2+ sem nenhum gatilho e sem sinal — "silêncio é pergunta, não conforto". +- Access: staff; visão do assessor escopada (`getAccessibleMunicipalityIds`); `leader` não vê nada disso. ## Decisões travadas -- **Motor é avaliador em leitura, não job persistido.** Roda no load (dados de 436 praças já vêm no bundle A9+); persistência só de DECISÕES (C12). **Rejeitado:** cron + tabela de sugestões (estado duplicado que dessincroniza; nada aqui exige assincronia); LLM gerando sugestões em runtime (indeterminístico, não auditável — o catálogo é curado em código a partir do relatório). +- **Motor é avaliador em leitura, não job persistido.** Roda no load (dados de 435 municípios já vêm no bundle A9+); persistência só de DECISÕES (C12). **Rejeitado:** cron + tabela de sugestões (estado duplicado que dessincroniza; nada aqui exige assincronia); LLM gerando sugestões em runtime (indeterminístico, não auditável — o catálogo é curado em código a partir do relatório). - **8 padrões na v1, catálogo fechado.** **Rejeitado:** 25 padrões de uma vez (metade depende de semanas de sinais C12; falso positivo mata a confiança na primeira semana — E1 do relatório). - **Descartar exige leitura alternativa.** É o antídoto metodológico do oráculo (§6.4) e o dado que o backtest precisa. **Rejeitado:** descarte de 1 clique sem motivo. -- **i18n e naming:** `evaluatePlazaTriggers`, `TriggeredPattern`, `triageLevel`, `suggestionCatalog`; patternIds estáveis (`P1`…`K-B`); textos pt-BR citando estatuto ("hipótese de literatura — relatório §6.1"). +- **i18n e naming:** `evaluateMunicipalityTriggers`, `TriggeredPattern`, `triageLevel`, `suggestionCatalog`; patternIds estáveis (`P1`…`K-B`); textos pt-BR citando estatuto ("hipótese de literatura — relatório §6.1"). ## Questões em aberto -- **Onde mora a superfície principal?** Opções: painel no dashboard | rota `/campanha/sugestoes` | aba na lista de Praças. **Recomendação:** painel no dashboard (top-5) + seção completa no detalhe da Praça; rota dedicada só se o volume real pedir (adiado com gatilho). _(shape na implementação decide o layout final)_ +- **Onde mora a superfície principal?** Opções: painel no dashboard | rota `/campanha/sugestoes` | aba na lista de municípios. **Recomendação:** painel no dashboard (top-5) + seção completa no detalhe do município; rota dedicada só se o volume real pedir (adiado com gatilho). _(shape na implementação decide o layout final)_ - **Adiamento por quanto tempo?** **Recomendação:** 7 dias default, 14 para nível 4–5; registrado como `decision: adiada`. ## Abordagem proposta ```mermaid flowchart LR - Bundle["loadPlazaListPageBundle + derivados E8/E10/E14"] + Bundle["loadMunicipalityListPageBundle + derivados E8/E10/E14"] Signals["sinais + frescor (C12)"] Cat["suggestionCatalog
(8 padrões, texto curado)"] - Eval["evaluatePlazaTriggers
(puro, determinístico)"] + Eval["evaluateMunicipalityTriggers
(puro, determinístico)"] Triage["triagem 1–5 + histerese
(vs. última allocationDecision)"] - UI["Dashboard painel + Praça seção
(aceitar/adiar/descartar)"] + UI["Dashboard painel + seção do município
(aceitar/adiar/descartar)"] Dec["allocationDecision (C12)"] Bundle --> Eval Signals --> Eval @@ -67,25 +67,25 @@ flowchart LR Componentes: - **`src/lib/suggestionCatalog.ts`**: os 8 padrões com gatilho (função pura sobre derivados), textos (leitura/descartes/menu/contraindicação com estatuto) e nível de triagem — espelho versionado do relatório §6.1. -- **`src/utilities/plazaTriggers.ts`**: avaliação por praça + agregação/ordenação por triagem; consulta últimas `allocationDecision` para histerese/adiamento (1 query por load, `where` em plaza in […]). -- **UI:** `SuggestionCard`/`SuggestionPanel` em `src/components/campaign/` (compõe Alert/Badge existentes do stack de insights); painel no dashboard (`campaignDashboardData.ts` estende o load); seção no detalhe da Praça. +- **`src/utilities/municipalityTriggers.ts`**: avaliação por município + agregação/ordenação por triagem; consulta últimas `allocationDecision` para histerese/adiamento (1 query por load, `where` em municipality in […]). +- **UI:** `SuggestionCard`/`SuggestionPanel` em `src/components/campaign/` (compõe Alert/Badge existentes do stack de insights); painel no dashboard (`campaignDashboardData.ts` estende o load); seção no detalhe do município. - **Action:** `recordAllocationDecision` (C12) reusada; wrapper `resolveSuggestion` valida `alternativeReading` no descarte. - **Sem migration.** ## Dependências -- Duras: **E8**, **E10**, **C12**; **E9** (a fila de colunas é onde o coordenador confere o contexto da sugestão). Suaves: **E14** (nível no snapshot e em K-B→N0/N1), dados reais da Onda 0 (motor é inútil com praças vazias — smoke com dados fictícios apenas). +- Duras: **E8**, **E10**, **C12**; **E9** (a fila de colunas é onde o coordenador confere o contexto da sugestão). Suaves: **E14** (nível no snapshot e em K-B→N0/N1), dados reais da Onda 0 (motor é inútil com municípios vazios — smoke com dados fictícios apenas). - Reusa: bundle A9+, `campaignDashboardData.ts`, `campaignAccess.ts`, `withPayloadTransaction`. ## Não escopo -- Padrões T (TI — E12), I/J (alienação/agenda — fase 2), G2-hierarquia completa entre praças de níveis diferentes além da triagem 1–5; notificação push (D2); qualquer automação de ação. +- Padrões T (TI — E12), I/J (alienação/agenda — fase 2), G2-hierarquia completa entre municípios de níveis diferentes além da triagem 1–5; notificação push (D2); qualquer automação de ação. ## Rabbit holes - **Catálogo virar motor de regras genérico** (DSL, editor de regras). 8 funções puras nomeadas bastam; DSL é plataforma white-label, não este ciclo. - **Histerese exigir tabela de avaliações.** Comparar com a última `allocationDecision`/adiamento cobre a v1; se falhar na prática, aí sim revisitar persistência. -- **Explicabilidade virar relatório longo.** Card mostra 2 fatores + estatuto + link "por quê" (relatório §6.1); não gerar texto por praça. +- **Explicabilidade virar relatório longo.** Card mostra 2 fatores + estatuto + link "por quê" (relatório §6.1); não gerar texto por município. ## Adiado com gatilho @@ -97,6 +97,6 @@ Componentes: - `docs/roadmap.md` (Inteligência de campanha, E11) · [plano-mestre](inteligencia-campanha.md) (fila canônica, G4-mitigações) - `docs/research/relatorio-entrevista-persona-campanha.md` §6.1 (padrões), §6.2 (triagem), §6.3 (divisão análise×julgamento), §6.4 (modos de falha) -- `src/utilities/plazaPageData.ts`, `src/utilities/campaignDashboardData.ts`, `src/utilities/campaignAccess.ts`, `src/collections/ElectionCandidate.ts` (`runningAgain2026`) +- `src/utilities/municipalityPageData.ts`, `src/utilities/campaignDashboardData.ts`, `src/utilities/campaignAccess.ts`, `src/collections/ElectionCandidate.ts` (`runningAgain2026`) - `PRODUCT.md` princípio 5 / `DESIGN.md` — âncoras da superfície - AGENTS.md — access, transações, naming diff --git a/docs/plans/niveis-de-envolvimento.md b/docs/plans/niveis-de-envolvimento.md index 82051a8e..8f7f3a6a 100644 --- a/docs/plans/niveis-de-envolvimento.md +++ b/docs/plans/niveis-de-envolvimento.md @@ -1,19 +1,19 @@ -# E14 — Níveis de envolvimento N0–N4 por Praça +# E14 — Níveis de envolvimento N0–N4 por município Status: rascunho -Atualizado em: 2026-07-21 +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening) Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", E14; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md)) -Impeccable: B — campo novo no detalhe/lista da Praça (Popover B9) + badge staff; sem rota nova -Appetite: ~1 dia eng; migration pequena (campos em `plaza`) +Impeccable: B — campo novo no detalhe/lista do município (Popover B9) + badge staff; sem rota nova +Appetite: ~1 dia eng; migration pequena (campos em `municipality`) Responsável: — ## Design (Impeccable) -Âncoras: `PRODUCT.md` (princípios 3 e 5) / `DESIGN.md` (register `product`) · controles B9 (`PlazaList*Control`). +Âncoras: `PRODUCT.md` (princípios 3 e 5) / `DESIGN.md` (register `product`) · controles B9 (`MunicipalityList*Control`). Na implementação: craft compacto → critique → polish. -- **Persona / contexto:** coordenador na reunião quinzenal de realocação; assessor entendendo o que a praça dele "recebe". +- **Persona / contexto:** coordenador na reunião quinzenal de realocação; assessor entendendo o que o município dele "recebe". - **Job principal:** substituir priorizar/despriorizar binário por 5 níveis com critérios e movimento auditável. - **Estratégia de cor:** Restrained — nível como badge neutro numerado, sem escala de "importância" colorida (evita leitura de ranking público). - **Edit where you see:** sim — mudar nível via Popover na lista/detalhe, exigindo motivo (grava `allocationDecision`). @@ -21,12 +21,12 @@ Na implementação: craft compacto → critique → polish. ## Contexto -Relatório §6.8: a escala N0 Monitorar → N1 Presença de mandato → N2 Rede sem agenda → N3 Rede+agenda → N4 Investimento pleno, com regras de movimento (promoção: gatilho positivo 2 semanas + capacidade; rebaixamento: não-resposta 3–4 semanas / perda estrutural / rebalanceamento; histerese: sem 2 mudanças no mês, sem pulo de 2 níveis salvo choque triangulado, janela de proteção de 3 semanas pós-promoção) e o rito (registro ex-ante com motivo E sinais de reversão; vocabulário duplo). Hoje `plaza.priority` só tem `alta|normal`. O nível modula a fila (E9), o motor (E11) e as metas (praça N0/N1 carrega meta mínima — K-B/FU3). +Relatório §6.8: a escala N0 Monitorar → N1 Presença de mandato → N2 Rede sem agenda → N3 Rede+agenda → N4 Investimento pleno, com regras de movimento (promoção: gatilho positivo 2 semanas + capacidade; rebaixamento: não-resposta 3–4 semanas / perda estrutural / rebalanceamento; histerese: sem 2 mudanças no mês, sem pulo de 2 níveis salvo choque triangulado, janela de proteção de 3 semanas pós-promoção) e o rito (registro ex-ante com motivo E sinais de reversão; vocabulário duplo). Hoje `municipality.priority` só tem `alta|normal`. O nível modula a fila (E9), o motor (E11) e as metas (município N0/N1 carrega meta mínima — K-B/FU3). ## Objetivos -- `plaza.engagementLevel` (`n0…n4`, default `n2`), `levelNote` (motivo corrente), `levelChangedAt` (derivado) — todos staff-only (access nega `leader` na leitura, mesmo mecanismo de `supportStatus`). -- Mudança de nível SEMPRE grava `allocationDecision` (patternId `nivel`, snapshot com nível anterior/novo, motivo, sinais de reversão) — histórico auditável sem versionar `plaza`. +- `municipality.engagementLevel` (`n0…n4`, default `n2`), `levelNote` (motivo corrente), `levelChangedAt` (derivado) — todos staff-only (access nega `leader` na leitura, mesmo mecanismo de `supportStatus`). +- Mudança de nível SEMPRE grava `allocationDecision` (patternId `nivel`, snapshot com nível anterior/novo, motivo, sinais de reversão) — histórico auditável sem versionar `municipality`. - Validações de movimento no server action (pulo de 2 níveis exige flag "choque triangulado" com nota; aviso de janela de proteção) — avisos bloqueiam por default com override explícito do coordenador (registrado). - `priority` legada: mapeada (`alta`→N3 sugerido) e aposentada da UI; campo permanece no schema até limpeza futura (sem migração destrutiva agora). - Consumidores: badge staff no detalhe/lista; E9 usa nível na ordenação (N0/N1 fora do topo do déficit); metas E8 leem nível para "meta mínima". @@ -34,40 +34,40 @@ Relatório §6.8: a escala N0 Monitorar → N1 Presença de mandato → N2 Rede ## Decisões travadas - **Nível é staff-only fail-closed** (field access como `supportStatus`; nunca em view models de liderança). Vocabulário duplo é requisito de produto. **Rejeitado:** badge público "prioridade" (vaza o mapa de defesa). -- **Histórico via `allocationDecision`, não versions em `plaza`** (um mecanismo só para decisões — C12). **Rejeitado:** versions em plaza (custo alto, ruído); array `levelHistory` no doc (cresce sem access próprio). -- **Rebaixar meta ≠ rebaixar praça** (K-C): nível e meta são eixos independentes; a UI não acopla os dois automaticamente. **Rejeitado:** auto-derivar nível da cobertura (vira gaming imediato — G4). +- **Histórico via `allocationDecision`, não versions em `municipality`** (um mecanismo só para decisões — C12). **Rejeitado:** versions em municipality (custo alto, ruído); array `levelHistory` no doc (cresce sem access próprio). +- **Rebaixar meta ≠ rebaixar município** (K-C): nível e meta são eixos independentes; a UI não acopla os dois automaticamente. **Rejeitado:** auto-derivar nível da cobertura (vira gaming imediato — G4). - **i18n e naming:** `engagementLevel` (`n0|n1|n2|n3|n4`), `levelNote`, `levelChangedAt`; labels pt-BR ("N1 · Presença de mandato" etc.). ## Questões em aberto - **Default N2 para todas ou derivar sugestão inicial dos dados (E8/E10)?** Opções: N2 fixo | sugestão computada com aceite em lote. **Recomendação:** N2 fixo + tela de revisão em lote ordenada por sugestão — decisão continua humana, custo de setup ~1h de reunião. _(validar com produto)_ -- **Quem pode mudar nível?** Opções: só `coordinator` | `advisor` nas suas praças com aprovação. **Recomendação:** só `coordinator` (é decisão de realocação — rito §6.8); assessor propõe via nota/sinal. +- **Quem pode mudar nível?** Opções: só `coordinator` | `advisor` nos seus municípios com aprovação. **Recomendação:** só `coordinator` (é decisão de realocação — rito §6.8); assessor propõe via nota/sinal. ## Abordagem proposta ```mermaid flowchart LR UI["Popover nível (lista/detalhe)"] - Action["setPlazaEngagementLevel
(valida movimento)"] - Plaza["plaza.engagementLevel"] + Action["setMunicipalityEngagementLevel
(valida movimento)"] + Municipality["municipality.engagementLevel"] Dec["allocationDecision
(motivo + reversão + snapshot)"] Consumers["E9 fila · E8 metas · E11 motor"] - UI --> Action --> Plaza + UI --> Action --> Municipality Action --> Dec - Plaza --> Consumers + Municipality --> Consumers ``` Componentes: -- **`src/collections/Plaza.ts`**: 3 campos novos com access staff (padrão `politicalTrend`); `priority` sai da UI. -- **`src/utilities/plazaStaffFormActions.ts`** (ou action irmã): `setPlazaEngagementLevel` transacional (plaza + allocationDecision com `req`), validações de histerese puras em `src/lib/engagementLevel.ts` (unit-testável). -- **UI:** `PlazaListLevelControl` no padrão dos `PlazaList*Control` (B9); badge no header do detalhe. -- **Migration:** `pnpm migrate:create add_plaza_engagement_level`. +- **`src/collections/Municipality.ts`**: 3 campos novos com access staff (padrão `politicalTrend`); `priority` sai da UI. +- **`src/utilities/municipalityStaffFormActions.ts`** (ou action irmã): `setMunicipalityEngagementLevel` transacional (municipality + allocationDecision com `req`), validações de histerese puras em `src/lib/engagementLevel.ts` (unit-testável). +- **UI:** `MunicipalityListLevelControl` no padrão dos `MunicipalityList*Control` (B9); badge no header do detalhe. +- **Migration:** `pnpm migrate:create add_municipality_engagement_level`. ## Dependências - Duras: **C12** (`allocationDecision` existir). Suaves: E8 (metas por nível), E9 (ordenação), E11 (nível no snapshot dos gatilhos). -- Reusa: `plazaStaffFormActions.ts`, `campaignAccess.ts`, padrão de field access de `supportStatus` (`Leadership.ts`). +- Reusa: `municipalityStaffFormActions.ts`, `campaignAccess.ts`, padrão de field access de `supportStatus` (`Leadership.ts`). ## Não escopo @@ -80,12 +80,12 @@ Componentes: ## Adiado com gatilho -- **Notificação de mudança de nível ao assessor da praça.** Gatilho: D2 (sino) entregue. +- **Notificação de mudança de nível ao assessor do município.** Gatilho: D2 (sino) entregue. ## Referências - `docs/roadmap.md` (Inteligência de campanha, E14) · [plano-mestre](inteligencia-campanha.md) (G7) - `docs/research/relatorio-entrevista-persona-campanha.md` §6.8 (escala, movimento, rito, custo político), K1–K4 -- `src/collections/Plaza.ts` (priority/politicalTrend/access), `src/collections/Leadership.ts` (padrão supportStatus) -- `src/utilities/plazaStaffFormActions.ts`, `src/components/campaign/PlazaListTrendControl.tsx` (padrão de controle B9) +- `src/collections/Municipality.ts` (priority/politicalTrend/access), `src/collections/Leadership.ts` (padrão supportStatus) +- `src/utilities/municipalityStaffFormActions.ts`, `src/components/campaign/MunicipalityListTrendControl.tsx` (padrão de controle B9) - AGENTS.md — field access fail-closed, transações, migrations diff --git a/docs/plans/notifications.md b/docs/plans/notifications.md index 5e960463..80759caa 100644 --- a/docs/plans/notifications.md +++ b/docs/plans/notifications.md @@ -13,13 +13,13 @@ Design: [`Notificacoes-PWA.png`](../design-refs/latest/Notificacoes-PWA.png) · Como usar (parte deste plano — o sino): -- **Adotar a estrutura:** sino no header com contagem; bottom sheet "Notificações" com "4 não lidas", ação "Marcar todas como lidas", itens com ícone por tipo de evento, título com o núcleo/entidade ("Estimativa aguardando confirmação — Núcleo Quilombo Rio das Rãs"), linha de detalhe (autor + ação), data relativa e ponto de não lida. Os quatro tipos exemplificados (estimativa aguardando confirmação, novo apoiador, novo reporte de campo, plano de ação precisa de atenção) são um bom ponto de partida para o enum `type` da collection `Notification` — dois deles dependem dos domínios C2/C3 existirem. +- **Adotar a estrutura:** sino no header com contagem; bottom sheet "Notificações" com "4 não lidas", ação "Marcar todas como lidas", itens com ícone por tipo de evento, título com a entidade ("Estimativa aguardando confirmação — Quilombo Rio das Rãs"; no design-ref a entidade era um Núcleo — hoje é município/liderança), linha de detalhe (autor + ação), data relativa e ponto de não lida. Os quatro tipos exemplificados (estimativa aguardando confirmação, novo apoiador, novo reporte de campo, plano de ação precisa de atenção) são um bom ponto de partida para o enum `type` da collection `Notification` — dois deles dependem dos domínios C2/C3 existirem. - **Falta desenhar (não bloqueia):** a tela de opt-in de push com o checkbox de consentimento LGPD (`campanha-notificacoes-push`) e a nota de limitação do iOS — seguir o padrão do bloco "Consentimento LGPD" da [`Apoiador-Ficha.png`](../design-refs/latest/Apoiador-Ficha.png). - **Ajustar cores:** paleta antiga no HTML/PNG; implementar com tokens do tema `campaign` e componentes `Sheet`/`Drawer` existentes. ## Contexto -Coordenadores e lideranças precisam ser avisados de eventos da campanha (convite aceito, novo `nucleusUpdate`, estimativa aguardando confirmação) sem depender só do canal de WhatsApp. Este plano cobre dois canais de notificação dentro da vertical `/campanha`: +Coordenadores e lideranças precisam ser avisados de eventos da campanha (convite aceito, novo `municipalityUpdate`, estimativa aguardando confirmação) sem depender só do canal de WhatsApp. Este plano cobre dois canais de notificação dentro da vertical `/campanha`: - **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). @@ -43,15 +43,15 @@ Notificações por WhatsApp Business API ficam **fora** deste plano (Meta + Res. ## Questões em aberto - Unificar numa única collection `Notification` (log de eventos) que alimenta tanto o sino quanto o push, ou manter dois caminhos separados? **Recomendação:** unificar — um registro por evento, com canais de entrega como atributo. -- Quais eventos disparam notificação, e qual canal cada evento usa (ex.: estimativa aguardando confirmação → push para `geral`/coordenador; novo update → sino para coordenador do núcleo). -- Políticas de agrupamento/dedupe (ex.: `tag` do Push API por núcleo) e expurgo de notificações antigas. +- Quais eventos disparam notificação, e qual canal cada evento usa (ex.: estimativa aguardando confirmação → push para coordenador/candidato; novo update → sino para o assessor do município). +- Políticas de agrupamento/dedupe (ex.: `tag` do Push API por município) e expurgo de notificações antigas. - Onde guardar as VAPID keys (Vercel env) e política de rotação. ## Abordagem proposta ```mermaid flowchart LR - Hook["afterChange hook
leadership / nucleusUpdate /
campaignInvite / voteEstimate"] + Hook["afterChange hook
leadership / municipalityUpdate /
campaignInvite / voteEstimate"] Notif["Cria registro Notification
(com req, dentro da transação)"] Bell["Sino in-app
(lê Notification por campaignUser)"] Push["Push via Service Worker
(VAPID)"] @@ -69,7 +69,7 @@ flowchart LR Componentes: -- **Collection `Notification`** (admin group `Campanha`): `recipient` (rel → `campaignUser`), `type` (enum de evento), `payload` (json/richtext leve), `readAt`, `nucleus` (rel opcional). Access por `recipient = req.user.id`. +- **Collection `Notification`** (admin group `Campanha`): `recipient` (rel → `campaignUser`), `type` (enum de evento), `payload` (json/richtext leve), `readAt`, `municipality` (rel opcional). Access por `recipient = req.user.id`. - **Collection `PushSubscription`** (admin group `Campanha`): `user` (rel → `campaignUser`), `endpoint`, `keys.p256dh`, `keys.auth`, `expirationTime`. Várias subscriptions por usuário (vários dispositivos). - **Service Worker** `public/sw.js` com escopo `/campanha`, handler de `push` e `notificationclick` (abre a rota certa via `data.url`). - **Server action** de subscribe/unsubscribe + pedido de permissão, com opt-in gravando `Consent`. diff --git a/docs/plans/onda-0.md b/docs/plans/onda-0.md index 8ee5f151..567244f7 100644 --- a/docs/plans/onda-0.md +++ b/docs/plans/onda-0.md @@ -65,7 +65,7 @@ Refusa host não-local sem `ALLOW_REMOTE_DB=true` (mesma família de guard que ` **Local (PR):** 1. `pnpm migrate` (ou `db:seed:consent` se iterando textos). -2. Login `/campanha` → criar núcleo de teste. +2. Login `/campanha` → abrir um município e registrar dado fictício de teste. 3. Convite com Consent visível (contato fictício). 4. Apoiador de teste (+ intenção de voto opcional). 5. GET `/privacidade` com banner provisório. @@ -86,7 +86,7 @@ curl -X POST "https:///api/revalidate?tag=global_privacy-policy" \ ## Onboarding staff (sem PII de campo) - Criar `campaignUser` `geral`/`coordenador` via `/admin`. -- Estruturar núcleos de teste, agenda, baseline TSE. +- Estruturar municípios de teste, agenda, baseline TSE. - Treinamento de campo com titulares reais **após** revisão jurídica. ## RIPD diff --git a/docs/plans/planejador-de-giros.md b/docs/plans/planejador-de-giros.md index 4a5b8d16..a65e5740 100644 --- a/docs/plans/planejador-de-giros.md +++ b/docs/plans/planejador-de-giros.md @@ -1,7 +1,7 @@ # E13 — Planejador de presença e giros (agenda do candidato) Status: rascunho -Atualizado em: 2026-07-21 +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening) Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", E13; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md)) Impeccable: C — superfície nova de planejamento dentro de `/campanha/planos` (visão "Giros"), sem design-ref Appetite: ~1,5 dia eng; sem migration própria (usa `actionPlan.origin` de C12) @@ -16,28 +16,28 @@ Na implementação: shape → craft → critique → polish (classe C). Brief compacto: - **Persona / contexto:** coordenador montando a semana do candidato sob pressão de pedidos ("quem grita mais leva" — T5); precisa dizer não com critério. -- **Job principal:** compor giros de 2–3 dias por território com praças elegíveis — e tornar visível o que NÃO justifica visita. +- **Job principal:** compor giros de 2–3 dias por território com municípios elegíveis — e tornar visível o que NÃO justifica visita. - **Estratégia de cor:** Restrained; elegibilidade como checklist de 5 condições (✓/—), nunca score numérico com falsa precisão. -- **Edit where you see:** sim — criar `actionPlan` (kind visita/giro) direto da praça candidata. -- **Anti-goals:** otimizador de rota (TSP/mapas de estrada); score 0–100 de praça; agenda auto-aprovada; expor "não vá" com esse rótulo fora do staff (é despriorização — vocabulário duplo). +- **Edit where you see:** sim — criar `actionPlan` (kind visita/giro) direto do município candidata. +- **Anti-goals:** otimizador de rota (TSP/mapas de estrada); score 0–100 de município; agenda auto-aprovada; expor "não vá" com esse rótulo fora do staff (é despriorização — vocabulário duplo). ## Contexto -Relatório §6.7: visita tem efeito modesto e o canal é mobilização do núcleo — "a visita vale o que a rede local converte dela; agenda é multiplicador de estrutura". Elegibilidade = 5 condições (volume, headroom, rede de recepção, janela política, encaixe em giro); o calendário muda o produto (construção jul–ago / consolidação set / ativação última semana); há "não vá" explícitos e intermediários ("mande o coordenador/vídeo/dobradinha"); padrões J-A (elegível madura sem visita), J-B (pedida vs. justificada — `actionPlan.origin` de C12), J-C (composição do giro: âncora+satélites+semente, por contiguidade de TI). O `/campanha/planos` (C3) já tem eventos com Praça/advisors/status; falta a camada de decisão de agenda. +Relatório §6.7: visita tem efeito modesto e o canal é mobilização do núcleo — "a visita vale o que a rede local converte dela; agenda é multiplicador de estrutura". Elegibilidade = 5 condições (volume, headroom, rede de recepção, janela política, encaixe em giro); o calendário muda o produto (construção jul–ago / consolidação set / ativação última semana); há "não vá" explícitos e intermediários ("mande o coordenador/vídeo/dobradinha"); padrões J-A (município elegível maduro sem visita), J-B (pedida vs. justificada — `actionPlan.origin` de C12), J-C (composição do giro: âncora+satélites+semente, por contiguidade de TI). O `/campanha/planos` (C3) já tem eventos com município/advisors/status; falta a camada de decisão de agenda. ## Objetivos -- **Elegibilidade por praça:** checklist das 5 condições derivadas (volume E8; headroom E8; rede = lideranças ativas + responsável; janela = campo livre de datas + nota manual; encaixe = contiguidade TI com giro existente) — exposta na praça e numa lista "candidatas a visita" ordenada. +- **Elegibilidade por município:** checklist das 5 condições derivadas (volume E8; headroom E8; rede = lideranças ativas + responsável; janela = campo livre de datas + nota manual; encaixe = contiguidade TI com giro existente) — exposta no município e numa lista "candidatas a visita" ordenada. - **Fase do calendário:** rótulo automático (construção/consolidação/ativação por data) mudando o texto do "produto da visita" sugerido. - **Visão "Giros" em `/campanha/planos`:** agrupar planos de kind visita por giro (TI + intervalo de datas); compositor simples — escolher TI, ver âncora sugerida (maior estoque comprometido), satélites contíguos e 1 semente de expansão (P12), gerar os `actionPlan` em rascunho. - **J-B na prática:** criar plano a partir de pedido registra `origin=pedido_broker` + contra-oferta sugerida (coordenador/vídeo/parada em giro); painel "pedidos sem dado" para a reunião. -- **"Não vá" visível ao staff:** praça K-B/N0–N1/sem rede aparece com a contraindicação citada quando alguém tenta agendá-la (aviso com override registrado — não bloqueio duro). +- **"Não vá" visível ao staff:** município K-B/N0–N1/sem rede aparece com a contraindicação citada quando alguém tenta agendá-lo (aviso com override registrado — não bloqueio duro). ## Decisões travadas - **Planejador compõe `actionPlan`s existentes; não cria entidade "giro" persistida na v1** — giro = agrupamento por TI+datas dos planos gerados. **Rejeitado:** collection `tour` nova (migration + access + UI por um agrupamento derivável; revisitar com gatilho). - **Checklist ✓/— em vez de score numérico.** Falsa precisão é o erro documentado (Hersh — §6.4); a disciplina das 5 condições é o valor. **Rejeitado:** score composto 0–100. -- **Aviso com override registrado, não bloqueio.** "A geografia serve à política" (T5-contraindicação); override grava `allocationDecision` (patternId `J-B`/`nao-va`) com motivo. **Rejeitado:** hard-block de praças N0/N1. +- **Aviso com override registrado, não bloqueio.** "A geografia serve à política" (T5-contraindicação); override grava `allocationDecision` (patternId `J-B`/`nao-va`) com motivo. **Rejeitado:** hard-block de municípios N0/N1. - **i18n e naming:** `visitEligibility`, `tourComposer`, `calendarPhase` (`construcao|consolidacao|ativacao`), `origin` (C12); labels pt-BR. ## Questões em aberto @@ -62,9 +62,9 @@ flowchart LR Componentes: -- **`src/utilities/visitEligibility.ts`**: as 5 condições + fase, puro sobre derivados E8/bundle (lideranças ativas via `PlazaLeadershipsPanel` data, contiguidade via `bahiaTerritories.ts`). +- **`src/utilities/visitEligibility.ts`**: as 5 condições + fase, puro sobre derivados E8/bundle (lideranças ativas via `MunicipalityLeadershipsPanel` data, contiguidade via `bahiaTerritories.ts`). - **`src/components/campaign/TourComposer*.tsx`**: visão "Giros" na página de planos (tab nova ao lado de Próximos/Todos); gera rascunhos via action existente de criação de plano (estendida com `origin`). -- **Praça detalhe:** card compacto "Elegibilidade para visita" (checklist) com CTA "agendar em giro". +- **Detalhe do município:** card compacto "Elegibilidade para visita" (checklist) com CTA "agendar em giro". - **Sem migration própria** (assumindo kind `visita` na migration C12; senão `pnpm migrate:create add_action_plan_visit_kind`). ## Dependências @@ -79,19 +79,19 @@ Componentes: ## Rabbit holes - **Virar otimizador de rota.** Contiguidade de TI + ordenação é o teto; qualquer "distância por estrada" explode o item. -- **Compositor virar wizard de 6 passos.** Escolher TI → revisar 4–6 praças sugeridas → gerar rascunhos. Três interações. +- **Compositor virar wizard de 6 passos.** Escolher TI → revisar 4–6 municípios sugeridos → gerar rascunhos. Três interações. - **Janela política estruturada** (datas de festas/feiras por município). Catálogo inexistente; campo livre + conhecimento do assessor. Não construir base de eventos municipais. ## Adiado com gatilho - **Entidade `tour` persistida** (com resultado do giro). Gatilho: 3º giro real composto e time pedindo visão consolidada pós-giro. -- **Padrões J no motor (sugestão automática "praça madura sem visita").** Gatilho: E11 fase 2. +- **Padrões J no motor (sugestão automática "município maduro sem visita").** Gatilho: E11 fase 2. ## Referências - `docs/roadmap.md` (Inteligência de campanha, E13) · [plano-mestre](inteligencia-campanha.md) - `docs/research/relatorio-entrevista-persona-campanha.md` §6.7 (elegibilidade, fases, não-vá, J-A/J-B/J-C), Rodada 6 J1–J4 - `src/collections/ActionPlan.ts` (kinds/status/access), `src/app/(campaign)/campanha/(app)/planos/` (superfície C3) -- `src/utilities/plazaPageData.ts`, `src/lib/bahiaTerritories.ts` +- `src/utilities/municipalityPageData.ts`, `src/lib/bahiaTerritories.ts` - `PRODUCT.md`/`DESIGN.md` — âncoras da superfície nova - AGENTS.md — transações, access, naming diff --git a/docs/plans/poligonos-pracas-zona.md b/docs/plans/poligonos-pracas-zona.md index 11b2cce6..f9360fd0 100644 --- a/docs/plans/poligonos-pracas-zona.md +++ b/docs/plans/poligonos-pracas-zona.md @@ -1,161 +1,161 @@ -# Polígonos das Praças-zona (Salvador / Camaçari) +# B8 — Polígonos dos Municípios-zona de Salvador (ZE 1–19) -Status: F1 entregue em código (2026-07-21); F2 pendente -Atualizado em: 2026-07-21 -Revisão 2026-07-21: F1 implementada — `plazaZoneNeighborhoods.ts`, fixture+int, `PlazaZoneNeighborhoodsCard` no overview da Praça-zona; mapa inalterado (F2). +Status: F1 entregue em código (2026-07-21; em produção desde 2026-07-23); F2 pendente +Atualizado em: 2026-07-24 +Revisão 2026-07-24 (remodelagem Municípios M1): **Camaçari saiu do escopo** — virou município inteiro (ZE 170/171 agregadas), e as entradas de Camaçari foram removidas do catálogo (`MunicipalityZoneNeighborhoodCity = 'Salvador'`; fonte única `tre-ra-02-2017`). B8 agora cobre **só as 19 zonas de Salvador**. Identificadores renomeados `plaza*` → `municipality*` na M1. As menções a Camaçari abaixo foram mantidas apenas como histórico de pesquisa. +Revisão 2026-07-21: F1 implementada — `municipalityZoneNeighborhoods.ts`, fixture+int, `MunicipalityZoneNeighborhoodsCard` no overview do Município-zona; mapa inalterado (F2). Revisão 2026-07-21 (pós-`/simplify` + capture-review-debts): débitos S1–S3 absorvidos como **F2 prep** (~½ dia, dentro do appetite F2); S4–S10 em Explicitamente fora / Adiado; JR1–JR7 em Já resolvido (não reabrir no simplify). Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha B, item B8) -Impeccable: B — encaixe em `/campanha/pracas/[slug]` (lista de bairros) + `PlazaMapPanel`/`BahiaMap` (polígonos); sem rota nova -Appetite: ~2,5–3,5 dias eng; F1 catálogo+UI (~1 dia) shipável sozinha; F2 dissolve+TopoJSON+mapa (~1,5–2,5 dias) +Impeccable: B — encaixe em `/campanha/municipios/[slug]` (lista de bairros) + `MunicipalityMapPanel`/`BahiaMap` (polígonos); sem rota nova +Appetite: F2 restante ~1,5–2,5 dias eng (+~½ dia F2 prep); F1 (~1 dia) já entregue Responsável: — ## Design (Impeccable) -Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells existentes (`CampaignPageShell`, `PlazaMapPanel`, `BahiaMap`, cards do detalhe da Praça). +Âncoras: `PRODUCT.md` / `DESIGN.md` (register product — Field Desk) · tema `data-theme='campaign'` · shells existentes (`CampaignPageShell`, `MunicipalityMapPanel`, `BahiaMap`, cards do detalhe do município). Na implementação (`implement-roadmap-item`): craft compacto → critique → polish (superfícies já existem; F1 é bloco de leitura; F2 estende o mapa). Brief compacto: -- **Persona / contexto:** Assessor (ou Alex) abre uma Praça-zona de Salvador/Camaçari e precisa saber **quais bairros** ela cobre; no mapa, quer ver a zona colorida sozinha — não o município inteiro agregado. -- **Job principal:** geografia operacional da Praça-zona legível (bairros) e desenhável (polígono) sem inventar camada estadual de ZE. +- **Persona / contexto:** Assessor (ou Alex) abre um Município-zona de Salvador e precisa saber **quais bairros** ele cobre; no mapa, quer ver a zona colorida sozinha — não o município inteiro agregado. +- **Job principal:** geografia operacional do Município-zona legível (bairros) e desenhável (polígono) sem inventar camada estadual de ZE. - **Estratégia de cor:** Restrained; coroplético existente (sequencial / divergente) inalterado em tokens. -- **Anti-goals:** não geocodificar seções; não PostGIS; não redesign do mapa; não Praça = seção; não segundo sistema de geometrias paralelo ao padrão B2. +- **Anti-goals:** não geocodificar seções; não PostGIS; não redesign do mapa; não unidade = seção; não segundo sistema de geometrias paralelo ao padrão B2. ## Contexto -As Praças-zona (19 em Salvador ZE 1–19 + 2 em Camaçari ZE 170/171) são unidades operacionais reais, mas o mapa ainda pinta **só o polígono municipal** e lista as zonas num painel textual: +Os 19 Municípios-zona de Salvador (ZE 1–19) são unidades operacionais reais, mas o mapa ainda pinta **só o polígono municipal** de Salvador e lista as zonas num painel textual: -- `PlazaMapPanel` copy explícita: “Zonas não têm polígono oficial — Salvador e Camaçari aparecem agregadas…” -- `loadPlazaMapBundle` (`plazaMapData.ts`) soma votos da zona no `ibgeCode` do município; `zoneBreakdown` é a única visão por Praça-zona. +- `MunicipalityMapPanel` copy explícita: zonas não têm polígono oficial — Salvador aparece agregada. +- `loadMunicipalityMapBundle` (`municipalityMapData.ts`) soma votos da zona no `ibgeCode` do município; `zoneBreakdown` é a única visão por Município-zona. - [remodelagem-pracas.md](remodelagem-pracas.md) e o roadmap marcavam polígonos de zona como rabbit hole / fora de escopo (B4 histórico = dissolver municípios membros de ZE multi-município — **problema diferente**). -Pedido de produto (2026-07-21): mapear polígonos das Praças-zona; bairros por zona já são úteis na página da Praça; pesquisar Camaçari; se não houver fonte online de polígonos, planejar construção a partir dos bairros. +Pedido de produto (2026-07-21): mapear polígonos das unidades-zona; bairros por zona já são úteis na página do município. _(Na época o pedido incluía Camaçari; a remodelagem M1 de 2026-07-23 tirou Camaçari do modelo de zona.)_ ### Pesquisa de fontes (2026-07-21) -| Necessidade | Salvador | Camaçari | -| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Lista zona → bairros/localidades | **Oficial:** TRE-BA — Resolução Administrativa nº 2/2017 (Anexo I, circunscrição por bairro das ZE 1–19) + PDFs de rezoneamento em [tre-ba.jus.br/servicos-eleitorais/rezoneamento](https://www.tre-ba.jus.br/servicos-eleitorais/rezoneamento). Ainda **não** está no repo. | **Não há** lista oficial zona↔bairro comparável. Aprox.: relação TRE de **locais de votação** por zona (bairro/povoado no endereço) — ZE 171 concentra Abrantes, Arembepe, orla/povoados; ZE 170 a sede urbana — + curadoria manual com a coordenação. | -| Polígonos oficiais de ZE | **Não existem** no TSE/TRE-BA (só tabular). Consenso comunitário ([mapaslivres/zonas-eleitorais](https://github.com/mapaslivres/zonas-eleitorais)): construir por cruzamento. | Idem. | -| Polígonos de bairro | IBGE Censo 2022 `BA_bairros_CD2022` ([geoftp](https://geoftp.ibge.gov.br/organizacao_do_territorio/malhas_territoriais/malhas_de_setores_censitarios__divisoes_intramunicipais/censo_2022/bairros/shp/UF/)); alternativa municipal GeoSalvador. Bahia tem **455** bairros na malha IBGE — cobertura intramunicipal parcial; gaps exigem fallback. | IBGE pode ou não cobrir todos os nomes locais; gaps + orla/povoados → polígonos manuais ou casco aproximado. | +| Necessidade | Salvador | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Lista zona → bairros/localidades | **Oficial:** TRE-BA — Resolução Administrativa nº 2/2017 (Anexo I, circunscrição por bairro das ZE 1–19) + PDFs de rezoneamento em [tre-ba.jus.br/servicos-eleitorais/rezoneamento](https://www.tre-ba.jus.br/servicos-eleitorais/rezoneamento). **Já no repo** (F1). | +| Polígonos oficiais de ZE | **Não existem** no TSE/TRE-BA (só tabular). Consenso comunitário ([mapaslivres/zonas-eleitorais](https://github.com/mapaslivres/zonas-eleitorais)): construir por cruzamento. | +| Polígonos de bairro | IBGE Censo 2022 `BA_bairros_CD2022` ([geoftp](https://geoftp.ibge.gov.br/organizacao_do_territorio/malhas_territoriais/malhas_de_setores_censitarios__divisoes_intramunicipais/censo_2022/bairros/shp/UF/)); alternativa municipal GeoSalvador. Cobertura parcial; gaps exigem fallback. | + +_Histórico Camaçari (fora de escopo desde M1):_ não havia lista oficial zona↔bairro; a aproximação seria via locais de votação TRE (ZE 171 = Abrantes/Arembepe/orla; ZE 170 = sede) + curadoria. Registrado caso o modelo volte a dividir Camaçari. ## Objetivos -- **F1:** catálogo estático versionado `zoneNumber × city → neighborhoods[]` (Salvador completo a partir do TRE; Camaçari derivado + curado), com fixture/teste; bloco “Bairros desta Praça” no detalhe `/campanha/pracas/[slug]` quando `kind === 'zona'`. -- **F2:** TopoJSON das 21 Praças-zona (`properties: { plazaSlug, zoneNumber, cityCode }`) por **dissolução** dos polígonos de bairro mapeados; script re-executável no padrão B2; mapa pinta Praça-zona individualmente (não só o município). +- **F1 (entregue):** catálogo estático versionado `zoneNumber → neighborhoods[]` de Salvador (TRE RA 02/2017), com fixture/teste; bloco “Bairros” no detalhe `/campanha/municipios/[slug]` quando `kind === 'zona'`. +- **F2:** TopoJSON dos 19 Municípios-zona (`properties: { municipalitySlug, zoneNumber, cityCode }`) por **dissolução** dos polígonos de bairro mapeados; script re-executável no padrão B2; mapa pinta o Município-zona individualmente (não só Salvador agregada). - Guardrails: sem migration, sem collection, sem Consent, sem PostGIS; leitura com access existente; geometrias estáticas no repo; disclaimer de aproximação (não é limite oficial TSE). ## Decisões travadas -- **Item novo B8 (não reviver B4 nem absorver em B7).** B4 era “camada ZE estadual por dissolução de municípios membros”; aqui o gap é **intra-municipal** (19+2 Praças dentro de 2 municípios). B7 é filtro URL do mapa. (2026-07-21, roadmap-item.) **Rejeitado:** reabrir B4 com o desenho antigo (não resolve Salvador/Camaçari); fill-in sem plano (escopo explode em geoprocessamento). -- **F1 shipável sem F2.** Catálogo + UI na Praça entregam valor imediato e são pré-requisito do dissolve. **Rejeitado:** só polígonos sem lista (assessor não vê bairros no detalhe); só lista sem plano de polígono (não responde ao pedido do mapa). +- **Item novo B8 (não reviver B4 nem absorver em B7).** B4 era “camada ZE estadual por dissolução de municípios membros”; aqui o gap é **intra-municipal** (19 unidades dentro de Salvador). B7 é filtro URL do mapa. (2026-07-21, roadmap-item.) **Rejeitado:** reabrir B4 com o desenho antigo; fill-in sem plano (escopo explode em geoprocessamento). +- **F1 shipável sem F2.** Catálogo + UI no município entregam valor imediato e são pré-requisito do dissolve. (Confirmado: F1 está em produção sem F2.) - **Fonte Salvador = TRE-BA RA 02/2017 (+ PDFs de rezoneamento se divergirem).** Circunscrição oficial por bairro. **Rejeitado:** scrapers de “Onde Votar” / e-Título (frágil, PII-adjacent); aba SALVADOR da planilha E5 (DE-PARA não-oficial). -- **Fonte Camaçari = locais de votação TRE → localidades + curadoria.** Sem lista oficial zona↔bairro. **Rejeitado:** inventar paridade com Salvador sem evidência; esperar shapefile TRE (não existe). -- **Polígonos = dissolve de bairros (IBGE e/ou malha municipal), não geocodificar seções.** Mesmo padrão de dissolução já usado em TIs (B2). Gaps: polígono manual leve ou omitir pedaço com nota. **Rejeitado:** geocodificar milhares de seções → casco (rabbit hole da remodelagem); pedir shapefile ao TRE neste ciclo; PostGIS. -- **Escopo geográfico = só as 21 Praças-zona.** Demais ZE da BA continuam fora. **Rejeitado:** camada ZE estadual no mapa. -- **i18n e naming** (AGENTS.md): identificadores `plazaZoneNeighborhoods`, `buildPlazaZoneGeometries`, `bahia-plaza-zones.topo.json`, `getPlazaZoneFeature(slug)`; strings UI em pt-BR. +- **Escopo geográfico = só os 19 Municípios-zona de Salvador** (desde M1). Demais ZE da BA continuam fora; Camaçari é município inteiro. **Rejeitado:** camada ZE estadual no mapa; manter Camaçari dividida. +- **Polígonos = dissolve de bairros (IBGE e/ou malha municipal), não geocodificar seções.** Mesmo padrão de dissolução já usado em TIs (B2). Gaps: polígono manual leve ou omitir pedaço com nota. **Rejeitado:** geocodificar milhares de seções → casco; pedir shapefile ao TRE neste ciclo; PostGIS. +- **i18n e naming** (AGENTS.md): identificadores `municipalityZoneNeighborhoods` (existente), `build-municipality-zone-geometries.mjs`, `bahia-municipality-zones.topo.json`, `getMunicipalityZoneFeature(slug)`; strings UI em pt-BR. ## Questões em aberto -- **Malha de bairros: IBGE 2022 vs GeoSalvador (só capital)?** **Opções:** A) só IBGE BA | B) GeoSalvador + IBGE/manual Camaçari | C) só desenho manual. **Recomendação:** A na F2 com reconciliação de nomes TRE↔`NM_BAIRRO`; se cobertura de Salvador for insuficiente no spike de ½ dia, pivotar B para a capital. Validar no spike antes de lock de artefato. -- **Camaçari F2 se a malha de bairros for pobre?** **Opções:** A) dois polígonos manuais grossos (sede × orla/Abrantes) | B) adiar F2 Camaçari | C) casco por geocoding de locais de votação. **Recomendação:** A se F1 já tiver localidades estáveis; B se a coordenação aceitar mapa só Salvador; C só se A falhar (ainda sem seção-a-seção). -- **Chave do coroplético no mapa: `plazaSlug` ou continuar `ibgeCode` + overlay?** **Opções:** A) mode/`values` por `plazaSlug` para Praças-zona + município para o resto | B) split Salvador/Camaçari em features de zona substituindo o polígono municipal. **Recomendação:** B no footprint SSA/CMS (um polígono por Praça-zona; município some nesses dois) e A/`ibgeCode` no restante da Bahia — evita dupla contagem visual. _(assumido — validar no craft)_ +- **Malha de bairros: IBGE 2022 vs GeoSalvador?** **Opções:** A) só IBGE BA | B) GeoSalvador. **Recomendação:** A na F2 com reconciliação de nomes TRE↔`NM_BAIRRO`; se cobertura de Salvador for insuficiente no spike de ½ dia, pivotar B. Validar no spike antes de lock de artefato. +- **Chave do coroplético no mapa: `municipalitySlug` ou continuar `ibgeCode` + overlay?** **Opções:** A) mode/`values` por `municipalitySlug` para Municípios-zona + município para o resto | B) split Salvador em features de zona substituindo o polígono municipal. **Recomendação:** B no footprint de Salvador (um polígono por Município-zona; o polígono municipal some lá) e A/`ibgeCode` no restante da Bahia — evita dupla contagem visual. _(assumido — validar no craft)_ ## Abordagem proposta ```mermaid flowchart TD - treSSA["TRE-BA RA 02/2017
ZE → bairros Salvador"] --> catalog["src/lib/plazaZoneNeighborhoods.ts
+ fixture"] - treCMS["Locais de votação ZE 170/171
+ curadoria"] --> catalog - catalog --> ui["Bloco Bairros
pracas/slug overview"] + treSSA["TRE-BA RA 02/2017
ZE → bairros Salvador"] --> catalog["src/lib/municipalityZoneNeighborhoods.ts
+ fixture (F1 ✓)"] + catalog --> ui["Bloco Bairros
municipios/slug overview (F1 ✓)"] catalog --> reconcile["Reconciliação nomes
↔ NM_BAIRRO IBGE/Geo"] - ibge["BA_bairros_CD2022
(ou GeoSalvador)"] --> script["scripts/build-plaza-zone-geometries.mjs"] + ibge["BA_bairros_CD2022
(ou GeoSalvador)"] --> script["scripts/build-municipality-zone-geometries.mjs"] reconcile --> script - script --> topo["bahia-plaza-zones.topo.json"] - topo --> map["BahiaMap + plazaMapData
Praça-zona = feature própria"] + script --> topo["bahia-municipality-zones.topo.json"] + topo --> map["BahiaMap + municipalityMapData
Município-zona = feature própria"] ``` Componentes: -- **`src/lib/plazaZoneNeighborhoods.ts`** — mapa estático `{ city: 'Salvador'|'Camaçari', zoneNumber, neighborhoods: string[] }` indexado por `plazaSlug`; helper `plazaZoneNeighborhoodEntryForSlug(slug)` + `plazaZoneNeighborhoodSourceLabel(source)`; cabeçalho de proveniência (URL + data da resolução / lista de locais). Fixture `tests/fixtures/plaza-zone-neighborhoods.official.json` + int (19+2 zonas, sem bairro órfão duplicado entre ZE do mesmo município quando a fonte for exclusiva). **F2 prep:** refatorar para autorar só `{ plazaSlug, source, neighborhoods[] }` e hidratar `city`/`zoneNumber` de `getPlazaCatalogEntry` (ver seção abaixo). -- **UI detalhe** (`pracas/[slug]/page.tsx` overview): se `view.kind === 'zona'`, card/lista “Bairros desta Praça” a partir do helper (sem Payload). Copy: fonte TRE / aproximação Camaçari. -- **`scripts/build-plaza-zone-geometries.mjs`** (`pnpm build:plaza-zone-geometries`): baixa/cache malha de bairros (padrão `downloadToBuffer` / cache `data/geometries/`), filtra município, dissolve por zona via catálogo reconciliado (`topojson` merge como TIs), emite `src/lib/geometries/bahia-plaza-zones.topo.json`; reporta bairros sem match. -- **`src/lib/bahiaGeometries.ts`** (ou módulo irmão profundo, sem pass-through): `getPlazaZoneFeature(slug)` + tipo da topologia; lazy com o mapa (alinhar B5 F1 se ainda aberto). -- **`plazaMapData.ts` / `PlazaMapPanel` / `BahiaMap`:** valores por `plazaSlug` (ou chave estável da feature) para Praças-zona; remover/ajustar copy “sem polígono oficial”; manter disclaimer de aproximação. `zoneBreakdown` pode permanecer como lista ou virar highlight no mapa. -- **Depth check:** reusar padrão B2 (`bahiaGeometries`, `build-bahia-geometries`, fixtures); não criar collection `plazaGeometry`; não segundo Leaflet stack. +- **`src/lib/municipalityZoneNeighborhoods.ts`** _(F1, existente)_ — catálogo estático Salvador indexado por `municipalitySlug`; helpers `municipalityZoneNeighborhoodEntryForSlug(slug)` + `municipalityZoneNeighborhoodSourceLabel(source)`; cabeçalho de proveniência. Fixture `tests/fixtures/municipality-zone-neighborhoods.official.json` + int (19 zonas, sem bairro órfão duplicado). **F2 prep:** refatorar para autorar só `{ municipalitySlug, source, neighborhoods[] }` e hidratar `city`/`zoneNumber` de `getMunicipalityCatalogEntry` (ver seção abaixo). +- **UI detalhe** _(F1, existente)_ (`municipios/[slug]/page.tsx` overview): se `view.kind === 'zona'`, `MunicipalityZoneNeighborhoodsCard` a partir do helper (sem Payload). Copy: fonte TRE. +- **`scripts/build-municipality-zone-geometries.mjs`** (`pnpm build:municipality-zone-geometries`): baixa/cache malha de bairros (padrão `downloadToBuffer` / cache `data/geometries/`), filtra Salvador, dissolve por zona via catálogo reconciliado (`topojson` merge como TIs), emite `src/lib/geometries/bahia-municipality-zones.topo.json`; reporta bairros sem match. +- **`src/lib/bahiaGeometries.ts`** (ou módulo irmão profundo, sem pass-through): `getMunicipalityZoneFeature(slug)` + tipo da topologia; decode lazy como o restante das geometrias (B5 F1 já entregue no hardening — seguir `loadMunicipalityGeometryModule`). +- **`municipalityMapData.ts` / `municipalityMapContract.ts` / `MunicipalityMapPanel` / `BahiaMap`:** valores por `municipalitySlug` (ou chave estável da feature) para Municípios-zona; remover/ajustar copy “sem polígono oficial”; manter disclaimer de aproximação. `zoneBreakdown` pode permanecer como lista ou virar highlight no mapa. Tipos do bundle vivem em `municipalityMapContract.ts` (client-safe) — estender lá. +- **Depth check:** reusar padrão B2 (`bahiaGeometries`, `build-bahia-geometries`, fixtures); não criar collection de geometria; não segundo Leaflet stack. - **Migration:** Sem migration, sem collection, sem server action. ### Fases -| Fase | Entrega | Appetite | -| -------- | ----------------------------------------------------------------------- | ------------- | -| F1 | Catálogo + testes + bloco na Praça | ~1 dia | -| F2 prep | Hidratação via `plazaCatalog` + canônico JSON→TS + teste ordem de slugs | ~½ dia | -| F2 | Spike cobertura malha → script → TopoJSON → mapa | ~1,5–2,5 dias | +| Fase | Entrega | Appetite | +| ------- | ------------------------------------------------------------------------------ | ------------- | +| F1 ✓ | Catálogo + testes + bloco no município (entregue; Salvador-only desde M1) | ~1 dia | +| F2 prep | Hidratação via `municipalityCatalog` + canônico JSON→TS + teste ordem de slugs | ~½ dia | +| F2 | Spike cobertura malha → script → TopoJSON → mapa | ~1,5–2,5 dias | **F2 prep** (débitos pós-`/simplify`, capture-review-debts 2026-07-21 — não item paralelo no roadmap): -1. **S1 — Hidratar identidade do catálogo:** autorar entradas como `{ plazaSlug, source, neighborhoods[] }`; derivar `city` e `zoneNumber` de `getPlazaCatalogEntry(slug)` em `entry()` ou no read path (padrão `plazaElectionGeographyForSlug`). Reduz ~42 campos duplicados e risco de drift antes do dissolve. -2. **S2 — Fonte única TS ↔ fixture:** o script `build-plaza-zone-geometries.mjs` (ou gerador irmão) emite `plazaZoneNeighborhoods.ts` a partir de `plaza-zone-neighborhoods.official.json` (ou o inverso com JSON como canônico de evidência). Elimina ~300 linhas espelhadas; `evidenceSha256` permanece no fixture. -3. **S3 — Ordem de slugs:** int leve `plazaZoneNeighborhoods.map(e => e.plazaSlug)` vs slugs `kind === 'zona'` de `plazaCatalog` (alinha ao snapshot SHA de `plazaCatalog.int.spec.ts`). +1. **S1 — Hidratar identidade do catálogo:** autorar entradas como `{ municipalitySlug, source, neighborhoods[] }`; derivar `city` e `zoneNumber` de `getMunicipalityCatalogEntry(slug)` em `entry()` ou no read path (padrão `municipalityElectionGeographyForSlug`). Reduz campos duplicados e risco de drift antes do dissolve. +2. **S2 — Fonte única TS ↔ fixture:** o script `build-municipality-zone-geometries.mjs` (ou gerador irmão) emite `municipalityZoneNeighborhoods.ts` a partir de `municipality-zone-neighborhoods.official.json` (ou o inverso com JSON como canônico de evidência). Elimina linhas espelhadas; `evidenceSha256` permanece no fixture. +3. **S3 — Ordem de slugs:** int leve `municipalityZoneNeighborhoods.map(e => e.municipalitySlug)` vs slugs `kind === 'zona'` de `municipalityCatalog` (alinha ao snapshot do catálogo). ## Dependências -- **Dura:** R2 (mapa + detalhe Praça) — entregue. -- **Suave:** padrão B2 geometrias; B7 (filtro do mapa) independente — B8 deve respeitar o mesmo `buildPlazaListWhere` quando B7 existir. -- Reusa: `plazaCatalog`, `PlazaMapPanel`, `BahiaMap`, `plazaMapData`, `bahiaGeometries` / script B2. +- **Dura:** R2 (mapa + detalhe) — entregue e em produção (2026-07-23). +- **Suave:** padrão B2 geometrias; B7 (filtro do mapa) entregue — B8 respeita o mesmo `buildMunicipalityListWhere`. +- Reusa: `municipalityCatalog`, `MunicipalityMapPanel`, `BahiaMap`, `municipalityMapData`/`municipalityMapContract`, `bahiaGeometries` / script B2. ## Não escopo -- Geocodificação de seções / Praça = seção → permanece **fora de escopo** no roadmap. +- Geocodificação de seções / unidade = seção → permanece **fora de escopo** no roadmap. - Camada ZE para toda a Bahia / B4 histórico (dissolve multi-município) — não reabrir. -- E5 Salvador por bairro como unidade operacional (supersedido; aqui bairro é **atributo** da Praça-zona). -- Filtro URL do mapa → **B7**; `setStyle` incremental → **B6**. +- **Camaçari** (município inteiro desde M1) — só volta se o modelo territorial mudar. +- E5 Salvador por bairro como unidade operacional (supersedido; aqui bairro é **atributo** do Município-zona). +- Filtro URL do mapa → **B7** (entregue); `setStyle` incremental → **B6** (entregue). - PostGIS; edição de geografia no admin. ## Rabbit holes - **Geocodificar seções “só para fechar o polígono”.** Semanas + cascos frágeis. **Mitigação:** dissolve por bairro + gaps manuais; seções fora deste item. - **Reconciliação perfeita de 100% dos nomes TRE↔IBGE.** Explode o appetite. **Mitigação:** cobertura mínima documentada (ex. ≥90% Salvador); lista de unmatched no script; gaps manuais só nos críticos. -- **Desenhar 19 polígonos Salvador à mão no QGIS sem catálogo.** Irrepetível e sem teste. **Mitigação:** F1 obrigatória; manual só para gaps Camaçari/ unmatched. -- **Tratar polígono derivado como limite oficial TSE.** Risco jurídico/comunicação. **Mitigação:** copy “aproximação a partir de bairros / locais de votação; não é limite oficial”. +- **Desenhar 19 polígonos à mão no QGIS sem catálogo.** Irrepetível e sem teste. **Mitigação:** F1 obrigatória (feita); manual só para unmatched. +- **Tratar polígono derivado como limite oficial TSE.** Risco jurídico/comunicação. **Mitigação:** copy “aproximação a partir de bairros; não é limite oficial”. ## Já resolvido no simplify/critique (não reabrir) -- **JR1** — `PlazaZoneNeighborhoodsCard`: um lookup `plazaZoneNeighborhoodEntryForSlug` (sem duplo hit no `entryBySlug`). -- **JR2** — export `neighborhoodsForPlazaSlug` removido (wrapper redundante). +- **JR1** — `MunicipalityZoneNeighborhoodsCard`: um lookup `municipalityZoneNeighborhoodEntryForSlug` (sem duplo hit no `entryBySlug`). +- **JR2** — export wrapper redundante removido. - **JR3** — guard do card: `!entry?.neighborhoods.length`. -- **JR4** — `assertPlazaZoneNeighborhoodCatalogIntegrity` só no módulo (não exportado). +- **JR4** — assert de integridade só no módulo (não exportado). - **JR5** — testes redundantes de sort e checksum duplicado removidos do int spec. -- **JR6** — cobertura usa `zonePlazas.length` em vez de `21` fixo. -- **JR7** — comentário no spec: sem teste de exclusividade Camaçari (fonte curada/aproximada). +- **JR6** — cobertura usa `zoneMunicipalities.length` em vez de constante fixa. +- **JR7** — _(histórico)_ exclusividade de Camaçari sem teste — obsoleto: Camaçari saiu do catálogo na M1. - Impeccable compacto na F1: sem snapshot `.impeccable/critique/` nem P0–P3 abertos nesta superfície; polish visual amplo → **R6**. ## Explicitamente fora (skips `/simplify` + descartes do triage) -- **S4 — Remover assert de integridade no import.** Fail-fast no import é aceitável aqui; int spec já cobre slugs zona. Não mover só para spec sem motivo de perf. -- **S5 — Incluir `source` no SHA-256 canônico.** `deepEqual` fixture↔código já detecta mudança de `source`; refresh de `evidenceSha256` seria churn sem ganho. -- **S7 — `PlazaZoneNeighborhoodCity` vs `ZONE_PLAZA_CITIES`.** Absorvido por S1 se F2 prep rodar; não tipo paralelo isolado. -- **S8 — Proveniência em 3 camadas** (header TS, fixture `provenance`, `plazaZoneNeighborhoodSourceLabel`). Padrão aceitável para catálogo com copy UI distinta por `source`. +- **S4 — Remover assert de integridade no import.** Fail-fast no import é aceitável aqui; int spec já cobre slugs zona. +- **S5 — Incluir `source` no SHA-256 canônico.** `deepEqual` fixture↔código já detecta mudança de `source`. +- **S7 — Tipo de cidade paralelo.** Absorvido por S1 se F2 prep rodar (hoje `MunicipalityZoneNeighborhoodCity = 'Salvador'`). +- **S8 — Proveniência em 3 camadas** (header TS, fixture `provenance`, `municipalityZoneNeighborhoodSourceLabel`). Padrão aceitável para catálogo com copy UI por `source`. - **S9 — Double gate** `view.kind === 'zona'` na página + card `null`. Defesa em profundidade; não simplificar só por DRY. -- **S10 — `key={neighborhood}` no card.** Seguro com exclusividade Salvador testada; prefixar slug só se QA reportar colisão intra-zona. +- **S10 — `key={neighborhood}` no card.** Seguro com exclusividade Salvador testada. ## Adiado com gatilho -- **S6 — CSS compartilhado lista zona** (`PlazaMapPanel` zone breakdown ↔ `PlazaZoneNeighborhoodsCard`). **Gatilho:** F2 alterar `PlazaMapPanel`/`BahiaMap` ou R6 citar duplicação visual em ≥2 superfícies de zona. +- **S6 — CSS compartilhado lista zona** (`MunicipalityMapPanel` zone breakdown ↔ `MunicipalityZoneNeighborhoodsCard`). **Gatilho:** F2 alterar `MunicipalityMapPanel`/`BahiaMap` ou R6 citar duplicação visual em ≥2 superfícies de zona. - **Malha GeoSalvador em vez de IBGE.** Revisitar quando: spike F2 mostrar unmatched críticos em Salvador (>10% bairros TRE sem polígono). -- **Refino fino de Camaçari (povoados).** Revisitar quando: coordenação de campo reportar confusão operacional entre ZE 170/171 no mapa. -- **Camada ZE estadual (B4 antigo).** Revisitar quando: produto pedir mapa de ZE fora de SSA/CMS (improvável neste ciclo). +- **Camada ZE estadual (B4 antigo).** Revisitar quando: produto pedir mapa de ZE fora de Salvador (improvável neste ciclo). ## Referências - `docs/roadmap.md` (B8; fora de escopo estreitado; supersedido B4) - `docs/plans/mapa-bahia-geometrias.md` — padrão estático + dissolução; B4 histórico (multi-município) -- `docs/plans/remodelagem-pracas.md` — rabbit hole de polígono de zona; 21 Praças-zona -- `docs/plans/mapa-pracas-filtrado.md` — B7 (filtro; independente) -- `src/utilities/plazaMapData.ts`, `src/components/campaign/PlazaMapPanel.tsx`, `src/lib/plazaCatalog.ts`, `src/lib/bahiaGeometries.ts`, `scripts/build-bahia-geometries.mjs` -- TRE-BA RA 02/2017 Anexo I; [rezoneamento](https://www.tre-ba.jus.br/servicos-eleitorais/rezoneamento); relação de locais de votação TRE-BA 2024 +- `docs/plans/remodelagem-municipios.md` — M1 (Camaçari inteira; rename `plaza*`→`municipality*`); precedente: `remodelagem-pracas.md` +- `docs/plans/mapa-pracas-filtrado.md` — B7 (filtro; entregue) +- `src/utilities/municipalityMapData.ts`, `src/utilities/municipalityMapContract.ts`, `src/components/campaign/MunicipalityMapPanel.tsx`, `src/lib/municipalityCatalog.ts`, `src/lib/municipalityZoneNeighborhoods.ts`, `src/lib/bahiaGeometries.ts`, `scripts/build-bahia-geometries.mjs` +- TRE-BA RA 02/2017 Anexo I; [rezoneamento](https://www.tre-ba.jus.br/servicos-eleitorais/rezoneamento) - IBGE `BA_bairros_CD2022.zip` (geoftp) -- AGENTS.md — geometrias B2, naming, Praças-zona +- AGENTS.md — geometrias B2, naming, modelo Municípios - `PRODUCT.md` / `DESIGN.md` — Field Desk / Restrained diff --git a/docs/plans/registro-fundacao.md b/docs/plans/registro-fundacao.md index 33f658e1..f6fffe42 100644 --- a/docs/plans/registro-fundacao.md +++ b/docs/plans/registro-fundacao.md @@ -1,9 +1,9 @@ # C12 — Registro-fundação (trajetória, sinais tipados, origem de visita, decisões ex-ante) Status: rascunho -Atualizado em: 2026-07-21 +Atualizado em: 2026-07-24 (refs sincronizadas pós-remodelagem Municípios + hardening) Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Inteligência de campanha", C12; plano-mestre [inteligencia-campanha.md](inteligencia-campanha.md)) -Impeccable: B — sem rota nova; campos novos em forms existentes (`plazaUpdate`, `actionPlan`) + collection admin-hidden +Impeccable: B — sem rota nova; campos novos em forms existentes (`municipalityUpdate`, `actionPlan`) + collection admin-hidden Appetite: ~2 dias eng; 1 migration consolidada Responsável: — @@ -16,32 +16,32 @@ Na implementação: craft compacto → critique → polish (as superfícies são - **Persona / contexto:** assessor registrando "vereador esfriou" em 15 segundos no celular; coordenador registrando por que descartou uma sugestão. - **Job principal:** transformar sinal oral em série datada sem virar formulário de 10 campos (anti-goal E1 do relatório: extração sem devolução). - **Estratégia de cor:** Restrained. -- **Edit where you see:** sim — o log de 1 linha nasce do mesmo form de atualização já usado no detalhe da Praça. -- **Anti-goals:** formulário longo obrigatório; sobrescrita silenciosa de valores; segundo feed paralelo ao `plazaUpdate`. +- **Edit where you see:** sim — o log de 1 linha nasce do mesmo form de atualização já usado no detalhe do município. +- **Anti-goals:** formulário longo obrigatório; sobrescrita silenciosa de valores; segundo feed paralelo ao `municipalityUpdate`. ## Contexto -FU4 do relatório: backtest compara o que se sabia antes com o que aconteceu depois — "se o valor novo apaga o velho, o 'antes' deixa de existir". Hoje: `votePledge` guarda só o valor corrente (declarado/estimado sobrescrevem); `plazaUpdate` é imutável mas sem semântica de sinal (kinds `semanal|urgente|nota`); `actionPlan` não registra origem da visita (pedida × justificada — J-B) nem custo; e não existe registro de decisão (sugestão dada, aceita/descartada, leitura alternativa). Este item fecha os gaps G1–G4 do plano-mestre e é pré-requisito do motor (E11) e do backtest (E15). "O TSE estará lá em novembro; o lado da campanha não é reconstruível retroativamente." +FU4 do relatório: backtest compara o que se sabia antes com o que aconteceu depois — "se o valor novo apaga o velho, o 'antes' deixa de existir". Hoje: `votePledge` guarda só o valor corrente (declarado/estimado sobrescrevem); `municipalityUpdate` é imutável mas sem semântica de sinal (kinds `semanal|urgente|nota`); `actionPlan` não registra origem da visita (pedida × justificada — J-B) nem custo; e não existe registro de decisão (sugestão dada, aceita/descartada, leitura alternativa). Este item fecha os gaps G1–G4 do plano-mestre e é pré-requisito do motor (E11) e do backtest (E15). "O TSE estará lá em novembro; o lado da campanha não é reconstruível retroativamente." ## Objetivos -- **G1 — trajetória de pledge:** `versions: true` (sem drafts) em `votePledge`; leitura da série por praça/liderança para o delta semanal (E8) e o backtest (E15). -- **G2 — sinais tipados:** `plazaUpdate.kind` ganha `sinal`; campos condicionais `signalType` (`invasao | esfriamento | visita_adversario | proposta_broker | outro`), `signalSource` (texto curto), `triangulated` (checkbox staff-only). Continua imutável e mantendo `plaza.lastUpdateAt`. +- **G1 — trajetória de pledge:** `versions: true` (sem drafts) em `votePledge`; leitura da série por município/liderança para o delta semanal (E8) e o backtest (E15). +- **G2 — sinais tipados:** `municipalityUpdate.kind` ganha `sinal`; campos condicionais `signalType` (`invasao | esfriamento | visita_adversario | proposta_broker | outro`), `signalSource` (texto curto), `triangulated` (checkbox staff-only). Continua imutável e mantendo `municipality.lastUpdateAt`. - **G3 — origem/custo de plano:** `actionPlan.origin` (`dado | pedido_broker | obrigacao_politica`, default `dado`, staff-only) + `resourceCost` (number, staff-only, opcional). -- **G4 — decisões ex-ante:** collection `allocationDecision` (admin group `Campanha`, `admin.hidden` como `supporterImportBatch`): `plaza`, `patternId` (texto — P1…K-C ou `manual`), `suggestion` (texto), `decision` (`aceita | descartada | adiada`), `alternativeReading` (obrigatório ao descartar), `snapshot` (json — classificação/números no momento), `decidedBy` (derivado), `note`. Imutável após criação. +- **G4 — decisões ex-ante:** collection `allocationDecision` (admin group `Campanha`, `admin.hidden` como `supporterImportBatch`): `municipality`, `patternId` (texto — P1…K-C ou `manual`), `suggestion` (texto), `decision` (`aceita | descartada | adiada`), `alternativeReading` (obrigatório ao descartar), `snapshot` (json — classificação/números no momento), `decidedBy` (derivado), `note`. Imutável após criação. - Access: tudo staff; `leader` não lê sinais staff-only (`triangulated`), não lê `allocationDecision`, não lê `origin`/`resourceCost` — mesmo padrão `canReadCampaignStaffField`. - Garantia transversal: nenhuma action nova sobrescreve histórico; escritas multi-collection com `withPayloadTransaction` + `req`. ## Decisões travadas - **Histórico via Payload `versions`, não collection de snapshots.** Menos código, admin de graça, migration gerada. **Rejeitado:** `pledgeSnapshot` própria (duplica access/hooks); campo `history` json (não consultável, cresce sem índice). -- **Decisão registrada é collection própria e imutável** — é o dado que o backtest paga caro para ter; mudanças de nível N0–N4 (E14) também gravam aqui, um mecanismo só. **Rejeitado:** log em `plazaUpdate` (semântica de feed de campo, autoria de liderança possível — decisão é de coordenação); campo no `plaza` (sobrescreve). +- **Decisão registrada é collection própria e imutável** — é o dado que o backtest paga caro para ter; mudanças de nível N0–N4 (E14) também gravam aqui, um mecanismo só. **Rejeitado:** log em `municipalityUpdate` (semântica de feed de campo, autoria de liderança possível — decisão é de coordenação); campo no `municipality` (sobrescreve). - **Sinal nasce no form existente de atualização** (custo de digitação mínimo; "o campo dita, a sede registra" continua possível por quem tiver acesso). **Rejeitado:** collection nova de sinais (segundo feed; FU1 pede agregação, não outra caixa). - **i18n e naming:** `signalType`, `signalSource`, `triangulated`, `origin`, `resourceCost`, `allocationDecision`, `patternId`, `alternativeReading`; enums como valores de dado em pt (`invasao`, `pedido_broker`, `aceita`) por paridade com enums existentes (`semanal`, `alta`); labels pt-BR. ## Questões em aberto -- **`versions` também em `plaza` (para foto da classificação)?** Opções: sim | não — snapshot vai no `allocationDecision.snapshot`. **Recomendação:** não; o snapshot por decisão cobre o "ex-ante" com custo menor que versionar 436 praças a cada edição. +- **`versions` também em `municipality` (para foto da classificação)?** Opções: sim | não — snapshot vai no `allocationDecision.snapshot`. **Recomendação:** não; o snapshot por decisão cobre o "ex-ante" com custo menor que versionar 435 municípios a cada edição. - **Retenção de versions de `votePledge`?** **Recomendação:** `maxPerDoc: 0` (ilimitado) até a eleição; avaliar poda pós-E15. ## Abordagem proposta @@ -49,7 +49,7 @@ FU4 do relatório: backtest compara o que se sabia antes com o que aconteceu dep ```mermaid flowchart LR Pledge["votePledge + versions"] - Update["plazaUpdate kind=sinal
(signalType/source/triangulated)"] + Update["municipalityUpdate kind=sinal
(signalType/source/triangulated)"] Plan["actionPlan origin/resourceCost"] Dec["allocationDecision
(imutável, staff)"] Series["leitores de série
(E8 delta · E11 gatilhos · E15 backtest)"] @@ -62,17 +62,17 @@ flowchart LR Componentes: - **`src/collections/VotePledge.ts`**: `versions: true`; sem mudança de campos. -- **`src/collections/PlazaUpdate.ts`**: opção `sinal` no kind + 3 campos condicionais (`admin.condition` como os campos de `semanal`); access staff no `triangulated`. +- **`src/collections/MunicipalityUpdate.ts`**: opção `sinal` no kind + 3 campos condicionais (`admin.condition` como os campos de `semanal`); access staff no `triangulated`. - **`src/collections/ActionPlan.ts`**: `origin` + `resourceCost` com `canReadCampaignStaffField`. - **`src/collections/AllocationDecision.ts`** (nova): imutável (update/delete admin-only), `createdBy` derivado no hook (padrão `Supporter.createdBy`). -- **`src/utilities/pledgeHistory.ts`**: leitura da série de versions por praça (consumida por E8/E15). +- **`src/utilities/pledgeHistory.ts`**: leitura da série de versions por município (consumida por E8/E15). - **Actions:** `recordAllocationDecision` em `src/app/(campaign)/campanha/actions/` (usada por E11; utilizável manualmente antes disso), com `withPayloadTransaction`. - **Migration**: `pnpm migrate:create add_intel_foundation` — tabelas de versions do `votePledge`, campos novos, collection `allocationDecision`. ## Dependências -- Dura: deploy da remodelagem. Nenhuma de outro plano do programa (é a fundação; E8 usa se existir). -- Reusa: `withPayloadTransaction` (`payloadTransaction.ts`), `campaignAccess.ts`, padrão `createdBy` de `Supporter.ts`, `plazaUpdatePageData.ts` (feed). +- Dura: nenhuma pendente — deploy da remodelagem aplicado em produção (2026-07-23). Nenhuma de outro plano do programa (é a fundação; E8 usa se existir). +- Reusa: `withPayloadTransaction` (`payloadTransaction.ts`), `campaignAccess.ts`, padrão `createdBy` de `Supporter.ts`, `municipalityUpdatePageData.ts` (feed). ## Não escopo @@ -80,19 +80,19 @@ Componentes: ## Rabbit holes -- **"Já que estamos", versionar tudo.** Versions em `plaza`/`leadership` multiplica tabelas e ruído de admin. **Mitigação:** só `votePledge` neste item; demais têm gatilho abaixo. +- **"Já que estamos", versionar tudo.** Versions em `municipality`/`leadership` multiplica tabelas e ruído de admin. **Mitigação:** só `votePledge` neste item; demais têm gatilho abaixo. - **Sinal virar formulário de inteligência.** 3 campos, um deles opcional; qualquer campo além disso é feature do E11, não do registro. -- **`snapshot` virar dump gigante.** Limitar ao objeto pequeno (classe, cobertura, nível, números-chave) — não serializar o bundle da praça. +- **`snapshot` virar dump gigante.** Limitar ao objeto pequeno (classe, cobertura, nível, números-chave) — não serializar o bundle do município. ## Adiado com gatilho -- **Versions em `plaza` (histórico de estratégia).** Gatilho: E15 precisar de foto além do `allocationDecision.snapshot` em caso real. +- **Versions em `municipality` (histórico de estratégia).** Gatilho: E15 precisar de foto além do `allocationDecision.snapshot` em caso real. - **Import de sinais via WhatsApp/copy-paste estruturado.** Gatilho: volume real de sinais >20/semana com reclamação de digitação. ## Referências - `docs/roadmap.md` (Inteligência de campanha, C12) · [plano-mestre](inteligencia-campanha.md) (gaps G1–G4) - `docs/research/relatorio-entrevista-persona-campanha.md` FU4 (5 registros; nunca sobrescrever), FU1 (anedota estrutural), §6.4 (falso positivo de registro), J-B (origem da visita) -- `src/collections/VotePledge.ts`, `src/collections/PlazaUpdate.ts`, `src/collections/ActionPlan.ts`, `src/collections/Supporter.ts` (padrão createdBy/access), `src/collections/SupporterImportBatch.ts` (padrão admin-hidden) +- `src/collections/VotePledge.ts`, `src/collections/MunicipalityUpdate.ts`, `src/collections/ActionPlan.ts`, `src/collections/Supporter.ts` (padrão createdBy/access), `src/collections/SupporterImportBatch.ts` (padrão admin-hidden) - `src/utilities/payloadTransaction.ts`, `src/utilities/campaignAccess.ts` - AGENTS.md — migrations, transações com `req`, access fail-closed diff --git a/docs/plans/remodelagem-municipios.md b/docs/plans/remodelagem-municipios.md index 39cc3446..9d11f332 100644 --- a/docs/plans/remodelagem-municipios.md +++ b/docs/plans/remodelagem-municipios.md @@ -1,7 +1,7 @@ # Remodelagem Municípios — mudanças de rumo 2026-07-23 -Status: em execução -Atualizado em: 2026-07-23 +Status: entregue (M1–M5 em 2026-07-23) e **deployado em produção em 2026-07-23** (builds Vercel aplicaram as migrações destrutivas `20260723_200000_remodel_municipalities` + `20260723_202000_reconcile_municipality_remodel`) +Atualizado em: 2026-07-24 Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Remodelagem Municípios", fases M1–M5) Impeccable: C — vertical `/campanha` com rename, lockdown de liderança, dobradinhas e mapa no Início Appetite: ~5–5,5 dias eng (M1–M5) diff --git a/docs/plans/remodelagem-pracas.md b/docs/plans/remodelagem-pracas.md index d98a5851..3d3b499b 100644 --- a/docs/plans/remodelagem-pracas.md +++ b/docs/plans/remodelagem-pracas.md @@ -1,7 +1,9 @@ # Remodelagem da /campanha: Núcleo Eleitoral → Praça -Status: entregue (código; deploy pendente — revisar SQL destrutivo antes do build de produção) -Atualizado em: 2026-07-21 +> **SUPERSEDIDO (2026-07-23):** o plano-mestre vigente é [remodelagem-municipios.md](remodelagem-municipios.md) — rename Praça→Município (435 entradas; Camaçari inteira), role `candidate`, lockdown da liderança, dobradinhas `stateDeputy`, mapa no Início. O deploy foi **realizado em produção em 2026-07-23** (cadeia destrutiva `20260721_020109` → `20260723_200000` + `20260723_202000` aplicada via build Vercel). Este arquivo permanece como registro histórico R0–R5; identificadores `plaza*`/436 abaixo são da época. + +Status: entregue e supersedido (R0–R5 em código 2026-07-21; modelo substituído pela remodelagem Municípios; em produção desde 2026-07-23) +Atualizado em: 2026-07-24 Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Remodelagem Praças", itens R0–R5) Impeccable: C — vertical `/campanha` remodelada (rotas novas `/campanha/pracas`, `/campanha/organizacoes`, `/campanha/demandas`; adaptação de planos/apoiadores/dashboard) Appetite: ~2,5–3 semanas eng (fases R0–R5), funcional antes de 16/08 (início da propaganda) diff --git a/docs/plans/whatsapp-canal-fundacao.md b/docs/plans/whatsapp-canal-fundacao.md index ec6ad4be..b0337de1 100644 --- a/docs/plans/whatsapp-canal-fundacao.md +++ b/docs/plans/whatsapp-canal-fundacao.md @@ -45,7 +45,7 @@ Socket WhatsApp não roda em Vercel serverless → sidecar multi-sessão. Sem fu - 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. +- 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 @@ -115,7 +115,7 @@ Componentes: - **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. +- **Advisor-only allowlist por município no inbound.** Revisitar se falso positivo de match cruzado aparecer. ## Referências diff --git a/docs/plans/whatsapp-envio-liderancas.md b/docs/plans/whatsapp-envio-liderancas.md index ec1c7073..9c051641 100644 --- a/docs/plans/whatsapp-envio-liderancas.md +++ b/docs/plans/whatsapp-envio-liderancas.md @@ -34,12 +34,12 @@ Com [D3](whatsapp-canal-fundacao.md), cada staff tem (ou não) a própria sessã - 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. +- 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 da Praça”. +- **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”). diff --git a/docs/plans/whatsapp-interno-campanha.md b/docs/plans/whatsapp-interno-campanha.md index 78ac2806..d3d1e690 100644 --- a/docs/plans/whatsapp-interno-campanha.md +++ b/docs/plans/whatsapp-interno-campanha.md @@ -26,8 +26,8 @@ Hoje o produto só usa WhatsApp como **atalho `wa.me`** (convites, share, kit de - 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. +- 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) @@ -52,7 +52,7 @@ flowchart TD - **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. +- **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. diff --git a/docs/plans/whatsapp-sugestao-atualizacoes.md b/docs/plans/whatsapp-sugestao-atualizacoes.md index a631683c..ff2e6c0d 100644 --- a/docs/plans/whatsapp-sugestao-atualizacoes.md +++ b/docs/plans/whatsapp-sugestao-atualizacoes.md @@ -9,55 +9,55 @@ 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). +Â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** (`plazaUpdate` / demanda / nota de voto) e só gravar no domínio após aceite humano. +- **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 a Praça/liderança. +- **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 da Praça X, demanda, ou só arquivo?” + - 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 da Praça?” + - 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×praça. +- **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: `plazaUpdate` (`PlazaUpdateForm` / feed), `campaignDemand`, `votePledge.declaredVotes` (liderança) / estimativas staff (assimetria). +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 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). +- 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 às Praças acessíveis; `leader` não usa inbox; sem processar phones fora do CRM. +- 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-`plazaUpdate` por NLP; webhook que escreve domínio direto. +- **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`, `suggestPlazaUpdateFromMessage`, `acceptWhatsappDraft`; rotas/params em inglês; UI pt-BR (“Sugestões do WhatsApp”). +- **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ó `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)_ +- **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 @@ -67,16 +67,16 @@ flowchart LR Match["match Contact → leadership"] Draft["whatsappInboundDraft
heurística + edição"] Human["Aceitar / Editar / Descartar"] - Dom["plazaUpdate / campaignDemand
(+ pledge form)"] + Dom["municipalityUpdate / 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). +- **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 `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. +- **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 @@ -93,20 +93,20 @@ Componentes: ## 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”. +- **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 `plazaUpdate`.** Revisitar quando: D3 passar a persistir mídia. +- **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/PlazaUpdate.ts`, `src/components/campaign/PlazaUpdateForm.tsx` +- `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` diff --git a/docs/roadmap.md b/docs/roadmap.md index 41705641..fb94a697 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap — Teqo -Atualizado em: 2026-07-23 (A10 cenários entregue — [plano](plans/cenarios-estimativa-votos.md); D3–D5 WhatsApp; export CSV admin entregue) +Atualizado em: 2026-07-24 (janela 1 vigente — convenções até 05/08; foco: smoke pós-deploy, R6, E8; último compile) 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). @@ -8,9 +8,9 @@ Registro canônico dos **próximos** planos e débitos. Histórico de entregas: | Data | Marco | Consequência para o produto | | ----------- | --------------------------------------- | --------------------------------------------------------------------------------------------- | -| 20/07–05/08 | Convenções partidárias | Remodelagem Praças em curso; assessores operando no modelo novo o quanto antes | +| 20/07–05/08 | Convenções partidárias | Vertical remodelada já em produção; smoke + onboarding do time no modelo novo | | 15/08 | Prazo final de registro de candidaturas | TSE publica candidaturas 2026 → destrava A6 (dobradinha) | -| 16/08 | Início da propaganda eleitoral | Vertical remodelada (Praças, demandas, agenda, base nominal) em produção **antes** desta data | +| 16/08 | Início da propaganda eleitoral | Base nominal com dados reais + inteligência mínima (E8–E9, C12) operando **antes** desta data | | 09–23/10 | Propaganda gratuita rádio/TV | Congelar mudanças arriscadas (~20/09) | | 04/10 | 1º turno | GOTV (C5) — confirmação de comparecimento da base nominal | | 25/10 | Eventual 2º turno | Chapa majoritária; Solla decidido no 1º turno | @@ -20,110 +20,93 @@ Registro canônico dos **próximos** planos e débitos. Histórico de entregas: - Ownership de audiência e portabilidade de dados; módulos reutilizáveis em outros contextos políticos. - 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). Comunicação **1:1 interna** staff↔lideranças via bridge WhatsApp não oficial = programa **D3–D5** (não é Business API nem blast). +- **Município = unidade operacional pré-definida** (remodelagem 2026-07-23, **em produção**): 435 entradas — um por município da Bahia, exceto Salvador (19 Municípios-zona, ZE 1–19); Camaçari é município inteiro (ZE 170/171 agregadas); geografia seedada e read-only. Substitui Praça/Núcleo Eleitoral — plano-mestre [remodelagem-municipios.md](plans/remodelagem-municipios.md). +- Roles: `coordinator` ("Coordenador Geral"), `advisor` ("Assessor"), `leader` ("Liderança") e `candidate` ("Candidato", visão irrestrita); assessor vê só os municípios que administra; liderança em lockdown (só a ferramenta de contatos de apoiadores). Assimetria de votos: staff registra `declaredVotes` e estima em 3 cenários (A10); liderança nunca vê estimativas. 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). +- Padrões de engenharia vigentes (hardening 2026-07-23): access explícito em toda collection, zero lint warnings, ban `as never`, knip no CI, caching ladder — regra `.cursor/rules/engineering-standards.mdc`; débitos no ledger [TECH-DEBT.md](TECH-DEBT.md). ## 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`, **`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. +2. **Smoke pós-deploy** _(desbloqueado — deploy da remodelagem aplicado em produção em 2026-07-23)_ — `NEXT_PUBLIC_SITE_URL` HTTPS, login `/campanha`, município 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. +4. **Onboarding do time** — usuários `coordinator`/`advisor`, primeiros municípios assumidos, treino de campo. **O0+** (escala/DRY pós-Onda 0) não bloqueia jurídico nem smoke fictício — [plano](plans/escala-dry-pos-onda0.md). -## Remodelagem Praças (R0–R5) — caminho crítico de produto ✅ (código pronto; deploy pendente) +## Já entregue (resumo) -Feedback da coordenação (2026-07-20) invalidou o modelo de Núcleo: a campanha se organiza por territórios pré-definidos ("Praças"), jargão é "Assessor", votos são declarados por liderança×Praça e estimados pelo assessor (assimetria), demandas nascem da liderança com workflow de aprovação, tendência é conjuntura política manual, e a análise-chave é comparar candidatos por Praça através dos anos. Reset dos dados de campanha em produção (sem dados reais na vertical). Plano-mestre: [remodelagem-pracas.md](plans/remodelagem-pracas.md). +- **Era Núcleos (2026-07-15 → 2026-07-20)** — MVP + Ciclo 2 (auth `campaignUser`, território A1/A2, baseline TSE A3/A4, overview B1, share C1, PWA D1, geometrias B2, Leaflet B3), C2 apoiadores (eng.), C3 agenda, C6–C11 escala, E1+E3 metas/estratégia, E2 série TSE 2014/2018/2022, A5 conversão/classificação/alavancagem/mobilização, A7 F1–F2, A8 perfis IBGE, fill-ins (reset senha/perfil, visitados recentes, Field Desk polish). Infra e padrões (locks, transações, consent por chave, shells, mapa, dados eleitorais) reaproveitados pela remodelagem; superfícies e modelo de Núcleo substituídos. +- **Plataforma** — local Postgres + guards, migrations baselined, posts/tags do site público com cache `posts`, Onda 0 textos provisórios + `/privacidade`; toolchain TS6 (tooling) + TS7 (typecheck). +- **Site público (2026-07-21)** — Pixel do Meta nos abaixo-assinados ([plano](plans/pixel-meta-abaixo-assinado.md)). +- **Remodelagem Praças → Municípios (R0–R5 2026-07-21; M1–M5 2026-07-23; em produção 2026-07-23)** — modelo Município (435 entradas; Salvador 19 zonas; Camaçari inteira), role `candidate`, demandas staff-only, lockdown da liderança (ferramenta de contatos, `source: lideranca`), dobradinhas `stateDeputy` + `/campanha/dobradinhas`, mapa no Início; migrações destrutivas da vertical aplicadas em produção via build Vercel. Planos: [remodelagem-pracas.md](plans/remodelagem-pracas.md) → [remodelagem-municipios.md](plans/remodelagem-municipios.md). +- **Mapa e lista (2026-07-21, na era Praças; identificadores renomeados na M1)** — A9 total esperado ([plano](plans/estimativa-votos-praca.md)) + A9+ loader compartilhado ([plano](plans/escala-dry-pos-a9.md)); B6 setStyle incremental ([plano](plans/escala-dry-pos-b3.md)); B7 mapa filtrado ([plano](plans/mapa-pracas-filtrado.md)); B9 edição rápida ([plano](plans/edicao-rapida-lista-pracas.md)); B10 hover/tap ([plano](plans/hover-mapa-pracas.md)); B11 escala % dos válidos ([plano](plans/escala-percentual-mapa-pracas.md)); B12 fit ao footprint filtrado ([plano](plans/aproximar-mapa-pracas.md)); B8 F1 catálogo bairros das zonas ([plano](plans/poligonos-pracas-zona.md) — hoje Salvador-only, entradas de Camaçari removidas na M1); fill-ins filtros-auto ([plano](plans/filtros-auto-pracas.md)) e C8 F4 ([plano](plans/escala-dry-pos-c6.md)). +- **A10 (2026-07-23)** — cenários pessimista/média/otimista em `votePledge.estimatedVotes` e `municipality.expectedVotes`; agregação por cenário (default média); seletor no mapa/overview; liderança segue com um `declaredVotes` ([plano](plans/cenarios-estimativa-votos.md)). Desbloqueou **E8**. +- **Admin Payload (2026-07-23)** — export CSV de assinaturas e contatos ([plano](plans/exportar-csv-assinaturas.md)). +- **Hardening de engenharia (2026-07-23, Fases 0–6)** — access lockdown (PII/CMS admin-only + gates de rota do leader), tooling gate (knip no CI, `--max-warnings=0`, ban `as never`), artefato TSE commitado + cache (`bahiaElectionAggregates`, `election-tse`, `loadMunicipalityScope`), pending honesto + Suspense streaming, state scoping, splits (`src/utilities/access/*`, `supporterImport.ts`, shells DRY). Tracker: [IMPROVE-CODE-QUALITY-PLAN.md](IMPROVE-CODE-QUALITY-PLAN.md) · ledger: [TECH-DEBT.md](TECH-DEBT.md) · mapa de testes: [TESTING.md](TESTING.md). -| Fase | Escopo | Status | -| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -| R0 | Documentação (plano-mestre, roadmap, AGENTS/notebook/PRODUCT/CUSTOMER) | entregue 2026-07-21 | -| R1 | Domínio: catálogo 436 Praças, collections (`municipality`, `leadership`, `votePledge`, `organization`, `campaignDemand`, `municipalityUpdate`), access, migração consolidada `20260721_020109_remodel_municipalities` | entregue 2026-07-21 | -| R2 | Superfícies core: `/campanha/municipios` lista+detalhe+mapa (seletor de ano), CRM `/campanha/liderancas` multi-Praça, votos declarados×estimados, dashboard, convites | entregue 2026-07-21 | -| R3 | `/campanha/organizacoes`, planos com Praça/orgs/presença/resultado, `/campanha/demandas` com workflow e comprovantes | entregue 2026-07-21 | -| R4 | Inteligência: comparativo multi-candidato, mapa divergente (vermelho↔branco↔azul), tendência política manual, rename série E2 → "Evolução" | entregue 2026-07-21 | -| R5 | Hardening: testes por papel (unit 187 / int 306 / e2e 9 verdes), Aikido, checklist de deploy (migração destrutiva revisada) | entregue 2026-07-21 — critique/polish visual fino registrado como R6 | +## Próximos — Campanha (`/campanha`) -## Remodelagem Municípios (M1–M5) — mudanças de rumo 2026-07-23 +### Programa Inteligência de campanha (E8–E15, B13, C12) -Reunião com coordenador geral + deputado (2026-07-23) definiu 7 mudanças **antes** do deploy da remodelagem Praças: rename completo Praça→Município (`municipality`, URLs `/campanha/municipios`), Camaçari inteira (435 entradas), role `candidate`, demandas staff-only, lockdown da liderança (só ferramenta de contatos), dobradinhas estruturadas (`stateDeputy`) e mapa analítico na aba Início. Plano-mestre: [remodelagem-municipios.md](plans/remodelagem-municipios.md). +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 município a município 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). -| Fase | Escopo | Status | -| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | -| M1 | Schema + rename mecânico: catálogo 435, collections `municipality`/`municipalityUpdate`, migration `remodel_municipalities` + `reconcile_municipality_remodel`, sweep `plaza*`→`municipality*`, rota `municipios/` | entregue 2026-07-23 | -| M2 | Roles: `candidate` (`isCampaignUnrestricted`), lockdown leader, demandas staff-only, staff edita `declaredVotes`, int specs por papel | entregue 2026-07-23 | -| M3 | Ferramenta de contatos do leader (phone-first, `source: lideranca`, lê só `createdBy`) | entregue 2026-07-23 | -| M4 | Vertical `/campanha/dobradinhas` + seletores município/liderança | entregue 2026-07-23 | -| M5 | Mapa no Início; lista de municípios sem mapa | entregue 2026-07-23 | +| ID | Fatia | Plano | Entrega essencial | Classe | Appetite | Janela | Depende de | +| --- | ---------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | -------- | --------- | ------------------------------ | +| E8 | Conta da cadeira | [detalhes](plans/conta-da-cadeira.md) | Global `campaignGoals`; potencial por município (válidos projetados, captura do campo, roll-off, headroom); decomposição meta→município; cobertura Σpledges÷meta (cenário default = média) | B | ~2d | 1–2 | — (A10 ✓, remodelagem em prod) | +| 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 `municipalityUpdate`; `actionPlan.origin`+custo; collection `allocationDecision` | B | ~2d | 2 | — (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 | -**Notas:** B8 polígonos passa a escopo **Salvador-only** (Camaçari não é mais Praça-zona). A6 insight TSE alimenta sugestão futura sobre o registro operacional `stateDeputy`. Deploy único substitui o deploy pendente de `remodel_municipalities` — revisar SQL destrutivo `remodel_municipalities` antes do build. +### Demais itens abertos -## Inteligência de campanha (discovery 2026-07-21) +- **A6** dobradinha 2026 automática quando o TSE publicar candidaturas · gatilho externo: pós-15/08 · camada de insight sobre o registro operacional `stateDeputy` (M4) · 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 F2** polígonos dos Municípios-zona de Salvador (ZE 1–19; Camaçari saiu do escopo na M1 — município inteiro): F1 catálogo zona→bairros entregue (hoje Salvador-only); falta prep do catálogo (~½ dia) + dissolve IBGE/malha → TopoJSON no mapa · Janela 3 · cortável · [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 (`municipalityUpdate`/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 -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). +### Fill-ins abertos -| 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 | 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 `municipalityUpdate`; `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 | +- **O0+** escala/DRY pós-Onda 0 · [plano](plans/escala-dry-pos-onda0.md) +- **RS+** auth read leve + shells de senha · [plano](plans/escala-dry-pos-reset-senha-perfil.md) +- **C10 / C11** escala apoiadores/planos · reavaliar nas superfícies remodeladas · gatilhos originais intactos: base nominal crescendo ([plano](plans/escala-dry-pos-c9.md)) / volume de planos medido ([plano](plans/escala-dry-pos-c7.md)) +- **C8 F1–F3 restantes** perf de import/lista + DRY de helpers (F4 DRY de forms entregue; parte de F2/F3 absorvida pelo split de access) · gatilho: import em volume real pós-Onda 0 · [plano](plans/escala-dry-pos-c6.md) +- Higiene PascalCase de componentes legados + +### A validar (assumptions) + +- **C5** GOTV — validar com produto antes de planejar (hoje só design-ref; depende de C2 dados reais). + +### Grafo de dependências (abertos + predecessores mínimos) ```mermaid flowchart TD - subgraph Onda0["Onda 0 — produção"] - JUR["Lote jurídico único
(Consent finais + privacidade)"] - end - - subgraph Remodel["Remodelagem Praças"] - R1["R1 Domínio"] - R2["R2 Superfícies core"] - R3["R3 Orgs + Planos + Demandas"] - R4["R4 Inteligência"] - R5["R5 Hardening"] - end - - R1 --> R2 --> R3 --> R5 - R2 --> R4 --> R5 + JUR["Lote jurídico único
(Consent finais + privacidade)"] JUR --> C2prod["C2 dados reais
(base nominal)"] - R2 --> C2prod - TSE2026["TSE candidaturas 2026
(após 15/08)"] --> A6["A6 Dobradinha 2026
(reenquadrada p/ Praça)"] - R4 -.baseline por Praça.-> A6 - R2 --> D2["D2 Push + sino"] - R2 --> A9["A9 Estimativa votos ✓"] - A9 --> A9plus["A9+ Loader lista ✓"] - A9plus --> B9["B9 Edição rápida lista ✓"] - A9 --> A10["A10 Cenários estimativa ✓"] - R2 --> A10 - R2 -.-> B7["B7 Mapa filtrado ✓"] - B7 --> B12["B12 Aproximar mapa
ao filtro ✓"] - R2 -.-> B8["B8 Polígonos Praças-zona
(SSA/CMS)"] - R2 -.-> B10["B10 Hover/tap mapa ✓"] - 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 + TSE2026["TSE candidaturas 2026
(após 15/08)"] --> A6["A6 Dobradinha 2026"] + JUR -.chave push.-> D2["D2 Push + sino"] + JUR -.chave WA canal.-> D3["D3 WA canal fundação"] + C2prod -.telefones reais.-> D3 D3 --> D4["D4 WA envio 1:1"] D3 --> D5["D5 WA → rascunhos"] D4 -.thread.-> D5 - C2prod -.telefones reais.-> D3 + B8F2["B8 F2 Polígonos
zonas de Salvador"] - subgraph Intel["Inteligência de campanha — discovery 2026-07-21"] + subgraph Intel["Inteligência de campanha"] E8i["E8 Conta da cadeira"] E9i["E9 Fila de alocação"] C12i["C12 Registro-fundação"] @@ -136,10 +119,6 @@ flowchart TD E15i["E15 Backtest pós-eleição"] end - R4 --> E8i - A10 --> E8i - R5 --> C12i - A10 -.versions no group.-> C12i E8i --> E9i --> E11i C12i --> E11i C12i --> E14i @@ -156,102 +135,53 @@ flowchart TD E11i -.decisões acumuladas.-> E15i ``` -Paralelizáveis agora: **E8** (pós-A10), fill-ins (O0+, RS+, …). **D3** (baixa/média prioridade; após remodelagem em prod + folga — não bloqueia E8/E9). ~~**A9** / **A9+** / **A10** / **B9** / **B7** / **B10** / **B11** / **B6** / **B12** / **filtros-auto** / **C8** F4~~ entregues 2026-07-21–23. +Paralelizáveis agora: **E8** (A10 ✓ e remodelagem em produção), **C12** (paralelo a E8), fill-ins (O0+, RS+). **D3** só após smoke + folga (não compete com E8/E9). -### Sequência por janela +### Sequência por janela (só pendentes) -**Janela 1 — agora → 05/08 (convenções):** ~~R0 → R1 → R2~~ entregues; ~~**A9** estimativa de votos~~ / ~~**A10** cenários pessimista/média/otimista~~ / ~~**B9** edição rápida na lista~~ / ~~**B7** mapa filtrado~~ / ~~**B10** hover+click-nav~~ / ~~**B11** escala % dos válidos~~ / ~~**B12** aproximar mapa ao footprint filtrado~~ / ~~**filtros-auto** lista de Praças~~ entregues; **deploy da remodelagem** (revisar SQL destrutivo da migração antes do build) + smoke em produção; R6 critique/polish; Onda 0 jurídica em paralelo. +**Janela 1 — agora → 05/08 (convenções):** smoke pós-deploy em produção + onboarding do time (Onda 0 §2/§4); **R6** critique/polish; **E8** pode começar em paralelo; Onda 0 jurídica em paralelo (externa). **Janela 2 — 05/08 → 16/08 (pré-propaganda):** C2 dados reais assim que o jurídico liberar; **E8** conta da cadeira → **E9** fila de alocação, com **C12** registro-fundação em paralelo (migrations cedo, longe do congelamento); D2 se sobrar folga. -**Janela 3 — 16/08 → set:** A6 dobradinha (pós-TSE 15/08), **E10** classificação relativa → **B13** escala relativa no mapa, **E14** níveis N0–N4, **E11** motor de sugestões v1, **E12** camada TI, **E13** planejador de giros; **B8** polígonos das Praças-zona Salvador/Camaçari (F1 catálogo de bairros shipável antes; F2 dissolve), débitos sobreviventes (abaixo). Se houver folga **e** Little Hire ainda vazar para o ZAP: começar **D3** (fundação do canal) — não compete com E8/E9 na Janela 2. +**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 F2** polígonos das zonas de Salvador; débitos sobreviventes. Se houver folga **e** Little Hire ainda vazar para o ZAP: começar **D3**. -**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). +**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); ~~**A10**~~ (faixa pessimista/média/otimista — entregue 2026-07-23); **E8**+**E9**+**C12** (a conta da cadeira, a fila e o registro ex-ante são o mínimo de "inteligência, não planilha" — e C12 é irrecuperável se não registrar durante a campanha). +**Não cortáveis:** Onda 0 (jurídico/Consent); C2 dados reais; assimetria declarado×estimado (relação de campo); **E8**+**E9**+**C12** (a conta da cadeira, a fila e o registro ex-ante são o mínimo de "inteligência, não planilha" — e C12 é irrecuperável se não registrar durante a campanha). -**Cortes seguros** (se o prazo apertar, nesta ordem): **D5** inbox→rascunhos (manter registro manual + `wa.me`); **D4** envio bridge (manter `wa.me`); **D3** fundação do canal (atalho `wa.me` continua); **E13** planejador de giros (agenda segue manual com J-A/J-B como guia); **E12** camada TI (rollup manual por lista); **E15** (pós-eleição por definição — cortar = perder o aprendizado 2030); **E11** motor v1 (manter fila E9 sem sugestões); **B13** símbolo proporcional (manter quantis/LQ como escala); **E14** (manter `priority` alta/normal); R4 mapa comparativo (manter tabela comparativa); painel de detalhe por zona no mapa; R3 organizações (manter demandas); resultado de plano com mídia (manter texto); Eleitorado/IBGE na Praça; D2 push (manter sino); A6; **B8** (F2 polígonos; manter F1 bairros na Praça se já entregue — mapa continua agregado no município); débitos/fill-ins. ~~**B9** / **B10** / **B11** / **B6** / **B12**~~ / ~~**export CSV de assinaturas/contatos**~~ (entregues — não cortar). - -## Já entregue (resumo) - -- **Era Núcleos (2026-07-15 → 2026-07-20)** — MVP + Ciclo 2 (auth `campaignUser`, território A1/A2, baseline TSE A3/A4, overview B1, share C1, PWA D1, geometrias B2, Leaflet B3), C2 apoiadores (eng.), C3 agenda, C6–C11 escala, E1+E3 metas/estratégia, E2 série TSE 2014/2018/2022, A5 conversão/classificação/alavancagem/mobilização, A7 F1–F2, A8 perfis IBGE, fill-ins (reset senha/perfil, visitados recentes, Field Desk polish). Infra e padrões (locks, transações, consent por chave, shells, mapa, dados eleitorais) **são reaproveitados pela remodelagem**; as superfícies e o modelo de Núcleo são substituídos. -- **Plataforma** — local Postgres + guards, migrations baselined, posts/tags do site público com cache `posts`, Onda 0 textos provisórios + `/privacidade`. -- **Site público (2026-07-21)** — **Pixel do Meta nos abaixo-assinados** (`tracking.facebookPixelId` no admin `petition`, `PageView`/`Lead` na página pública via `MetaPixel` + `trackMetaLead`; migration `20260721_133531_add_petition_facebook_pixel_id`) — [plano](plans/pixel-meta-abaixo-assinado.md). -- **A9 (2026-07-21)** — **Estimativa de votos da Praça** (`municipality.expectedVotes` staff-only; fallback `expectedVotes ?? effectiveTotal` em mapa 2026/overview/dashboard; UI `/editar` + leitura lista/detalhe; migration `20260721_133444_add_municipality_expected_votes`) — [plano](plans/estimativa-votos-praca.md). -- **A9+ (2026-07-21)** — **Loader compartilhado da lista de Praças** (`loadMunicipalityListPageBundle`: 1× `aggregatePledgesByMunicipality` + `buildMunicipalityMapBundleFromMunicipalities`; `municipalityRevalidation.ts` + `municipalitySlug` para revalidate estreita no detalhe; int `municipalityPageData.int.spec.ts`; sem migration) — [plano](plans/escala-dry-pos-a9.md). -- **B7 (2026-07-21)** — **Mapa das Praças filtrado pela lista** (`buildMunicipalityListWhere` em `loadMunicipalityMapBundle`; `rawSearchParams` na página; empty → omitir painel; int `municipalityMapData.int.spec.ts`) — [plano](plans/mapa-pracas-filtrado.md). -- **B9 (2026-07-21)** — **Edição rápida na lista de Praças** (Assessores / Tendência / `expectedVotes` via Popovers em `MunicipalityList*Control`; `municipalityStaffFormActions`; sem migration) — [plano](plans/edicao-rapida-lista-pracas.md). -- **B10 (2026-07-21)** — **Hover/tap no Mapa das Praças** (destaque + `MapFeatureReadout`; desktop click navega; mobile 2º tap; SSA/CMS N>1 → `zoneBreakdown`; `municipalitiesByIbgeCode` / `resolveMunicipalityMapNavigation`) — [plano](plans/hover-mapa-pracas.md). -- **B11 (2026-07-21)** — **Escala % dos válidos no Mapa das Praças** (`validVotesByYear` no bundle; seletor `Total (votos)` / `% dos válidos`; domínio fixo 0–100%; 2026 usa válidos 2022; compare desliga %; readout em %) — [plano](plans/escala-percentual-mapa-pracas.md). -- **B12 (2026-07-21)** — **Aproximar mapa ao footprint filtrado + correção hover** (`fitToKeys` + `interactiveKeys` em `BahiaMap` a partir de `municipalitiesByIbgeCode`; `canonicalMapKeysKey`; hover stroke-only sem alterar fill; clear síncrono no mouseout; fit só quando footprint muda — sem re-zoom em Ano/Escala) — [plano](plans/aproximar-mapa-pracas.md). -- **B6 (2026-07-21)** — **`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 em `bahiaMapStyle.ts`) — [plano](plans/escala-dry-pos-b3.md). -- **B8 F1 (2026-07-21)** — **Catálogo bairros das Praças-zona** (`municipalityZoneNeighborhoods`: Salvador TRE-BA RA 02/2017 + Camaçari curado; fixture+int; card **Bairros desta Praça** no overview de Praças `kind=zona`; sem migration) — [plano](plans/poligonos-pracas-zona.md). **F2 pendente** (F2 prep catálogo ~½ dia + TopoJSON + mapa). Débitos pós-`/simplify`: hidratação via `municipalityCatalog`, canônico JSON→TS, teste ordem slugs → F2 prep no plano (S6 CSS compartilhado adiado até F2/R6). -- **Fill-in filtros-auto (2026-07-21)** — **Filtros auto-aplicados na lista de Praças** (`MunicipalityFilters`: debounce 1s no `q`, Enter imediato, selects imediatos, remove Buscar; `useTransition` + pending a11y; `shouldUpdateMunicipalitySearchUrl` + no-op via `buildMunicipalityFiltersKey`; sem migration) — [plano](plans/filtros-auto-pracas.md). Débitos pós-`/simplify`: sync back/forward `search`↔`state.q` e shell pending compartilhado — gatilhos no plano. -- **Fill-in C8 F4 (2026-07-21)** — **DRY municipality staff form actions** (`municipalityStaffFormActions.ts`: votos estimados, tendência e assessores compartilhados entre lista e `/editar`; `editar/formActions.ts` mantém só `updateMunicipalityStrategy`; sem migration) — [plano](plans/escala-dry-pos-c6.md). -- **Admin Payload (2026-07-23)** — **Export CSV de assinaturas e contatos** (`@payloadcms/plugin-import-export@3.82.0` em `signature` + `contact`; export CSV síncrono; flatten de `contact`/`petition` via `toCSV` em assinaturas; campos nativos em contatos; `consent` excluído; access `exports`/`imports` restrito a `users`; migration `20260723_025513_add_import_export_plugin`) — [plano](plans/exportar-csv-assinaturas.md). - -## Supersedidos pela remodelagem (2026-07-20) - -| Item antigo | Destino | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -| A5 competitiva (parte aberta) | Absorvido por **R4** (comparativo multi-candidato por Praça é a versão pedida pela coordenação) | -| A7 F3–F5 (escala pós-A4) | Morre com os loaders de núcleo; fetch único lista+mapa vira requisito nativo do mapa de Praças (R2) | -| A8+ (escala pós-A8) | Reavaliar pós-R2 se o Eleitorado IBGE for portado ao detalhe da Praça | -| B4 zonas TSE no mapa | Rabbit hole documentado no plano-mestre; **B8** reabre só Praças-zona SSA/CMS (dissolve por bairro — não o B4 multi-município) | -| C4 demandas | Absorvido por **R3** (agora nascem da liderança, com workflow assessor→coordenador geral e comprovantes staff-only) | -| C10 / C11 (escala apoiadores/planos) | Reavaliar pós-R2/R3 nas superfícies adaptadas; gatilhos originais (base nominal crescendo / volume medido) continuam valendo | -| E5 Salvador por bairro | Praças-zona de Salvador cobrem a granularidade; bairro só com pedido real pós-eleição | -| E6 (escala pós-E1) / E7 (escala pós-E2) | Overview de núcleos e trend VM morrem; débitos int reavaliar pós-R4 | -| FD+ / FD2 (Field Desk pós-critique) | Superfícies redesenhadas em R2; princípios (glossário inline, filas, bulk) entram no critique de R5 | -| VR+ (visitados recentemente) | Painel portado para Praças em R2; débito de refresh mantém gatilho original | -| Listas globais (lideranças/atualizações) | CRM de liderança já nasce global em R2 (liderança é 1 registro por pessoa) | - -## 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) · **E8** desbloqueado por A10 ✓ -- **A6** dobradinha 2026 automática quando o TSE publicar candidaturas · gatilho externo: pós-15/08 · reenquadrar para Praça · alimenta **E13** · [plano](plans/insight-dobradinha-2026.md) -- **B5 F2–F3** cache CLI compartilhado + factory mun/TI (scripts continuam) · [plano](plans/escala-dry-pos-b2.md) -- **B8** Polígonos das Praças-zona (Salvador ZE 1–19 / Camaçari ZE 170–171): ~~F1 catálogo zona→bairros + UI na Praça~~ (entregue 2026-07-21); F2 prep catálogo (~½ dia) + dissolve IBGE/malha → TopoJSON no mapa · Janela 3 · cortável (F2) · [plano](plans/poligonos-pracas-zona.md) -- **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 (`municipalityUpdate`/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 +**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** backtest (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** níveis (manter `priority` alta/normal); **B8 F2** polígonos (mapa continua agregado no município; manter F1 bairros); **D2** push (adiar); **A6**; débitos/fill-ins. ## Bloqueadores atuais -| Item | Status | Fonte | -| -------------------------------------------------------------------------------------------------------- | ------------------------------------ | ---------------------------------------------------- | -| Lote jurídico final LGPD (Consent + privacidade + RIPD) — PII real em hold | **caminho crítico para dados reais** | notebook; onda-0; AGENTS | -| Deploy da remodelagem Praças (migração destrutiva `remodel_municipalities` — revisar SQL antes do build) | **pronto para deploy** | [remodelagem-pracas.md](plans/remodelagem-pracas.md) | -| RBAC em `users` (admin Payload) antes de abrir `/admin` a equipe maior | pendente | AGENTS Known Gap #1 | -| Consent público ainda por ID numérico (`submitWhatsapp.ts` etc.) | pendente | AGENTS Known Gap #2 | -| Collection `Pages` + hero/copy da home editáveis | pendente (não bloqueia `/campanha`) | AGENTS Known Gap #3 | +| Item | Status | Fonte | +| ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ------------------------------ | +| Lote jurídico final LGPD (Consent + privacidade + RIPD) — PII real em hold | **caminho crítico para dados reais** | notebook; onda-0; AGENTS | +| Smoke pós-deploy + onboarding do time (deploy da remodelagem aplicado em produção 2026-07-23) | pendente (operacional) | Onda 0 §2/§4; checklist AGENTS | +| Campo `roles` em `users` antes de abrir `/admin` a equipe maior (access explícito por collection já shipped — hardening Fase 0) | pendente (migration) | AGENTS Known Gap #1; TECH-DEBT | +| Consent público ainda por ID numérico (`submitWhatsapp.ts` etc.) | pendente | AGENTS Known Gap #2 | +| Collection `Pages` + hero/copy da home editáveis | pendente (não bloqueia `/campanha`) | AGENTS Known Gap #3 | ## Site público -**Já entregue:** `post`/`tag`, listagens/artigos, seed, cache `posts`; `/privacidade` (texto provisório Onda 0); **Pixel do Meta nos abaixo-assinados** ([plano](plans/pixel-meta-abaixo-assinado.md)). +**Já entregue:** `post`/`tag`, listagens/artigos, seed, cache `posts`; `/privacidade` (texto provisório Onda 0); Pixel do Meta nos abaixo-assinados ([plano](plans/pixel-meta-abaixo-assinado.md)); export CSV no admin. **Próximos:** -- Textos finais de privacidade + polish O0+ (revalidate globals, DRY Lexical) — mencionar cookies/Meta com Pixel em uso _(suave; ver roadmap Site público)_ +- Textos finais de privacidade + polish O0+ (revalidate globals, DRY Lexical) — mencionar cookies/Meta com Pixel em uso - `Pages` institucionais (bio, mandato, propostas) + hero/copy editáveis - Agenda/multimídia via links oficiais; CTA Doar → QueroApoiar - Migrar Consent dos fluxos públicos para chave estável ## Admin Payload -- `roles` em `users` + access real antes de ampliar `/admin` — access explícito por collection SHIPPED 2026-07-23 (hardening Fase 0); falta apenas o campo `roles` (migration) +- Campo `roles` em `users` (migration) antes de ampliar `/admin` — o access explícito por collection foi shipped em 2026-07-23 (hardening Fase 0: `users`/`signature`/`subscription`/`consent` admin-only; escrita de CMS admin-only) - Seed reproduzível de `Consent` por chave (não por ID) ## Débitos de engenharia (hardening 2026-07-23) -- Ledger vivo em [`TECH-DEBT.md`](TECH-DEBT.md) (tracker: [`IMPROVE-CODE-QUALITY-PLAN.md`](IMPROVE-CODE-QUALITY-PLAN.md)); itens maiores: dirigir `knip` unused-exports de warn→error após os splits, redesenho do lease da janela de consent ausente nos testes, e2e fino, CI só no mirror GitHub +- Ledger vivo em [`TECH-DEBT.md`](TECH-DEBT.md) (tracker: [`IMPROVE-CODE-QUALITY-PLAN.md`](IMPROVE-CODE-QUALITY-PLAN.md)); itens maiores: migration `users.roles`, dirigir `knip` unused-exports de warn→error após os splits, redesenho do lease da janela de consent ausente nos testes, e2e fino, CI só no mirror GitHub ## Plataforma white-label @@ -260,7 +190,7 @@ Paralelizáveis agora: **E8** (pós-A10), fill-ins (O0+, RS+, …). **D3** (baix ## Fora de escopo (por enquanto) - 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) +- Geocodificação de seções eleitorais / unidade = seção (polígonos **aproximados** dos 19 Municípios-zona de Salvador = **B8 F2**, sem seções) - PWA do site/`/admin`; PostGIS sem query espacial real - 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 @@ -269,8 +199,8 @@ Paralelizáveis agora: **E8** (pós-A10), fill-ins (O0+, RS+, …). **D3** (baix ## Fontes - [`AGENTS.md`](../AGENTS.md) — decisões travadas, Known Gaps, checklist -- [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc) — status operacional (era Núcleos → Praças) +- [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc) — status operacional (era Núcleos → Praças → Municípios) - [`README.md`](../README.md) — missão -- [`docs/plans/*.md`](plans/) — planos por item ([remodelagem-pracas.md](plans/remodelagem-pracas.md) e [inteligencia-campanha.md](plans/inteligencia-campanha.md) são os planos-mestre vigentes) +- [`docs/plans/*.md`](plans/) — planos por item ([remodelagem-municipios.md](plans/remodelagem-municipios.md) e [inteligencia-campanha.md](plans/inteligencia-campanha.md) são os planos-mestre vigentes) - [`docs/research/`](research/) — embasamento de produto/domínio (compêndio de literatura + relatório de discovery aprovado 2026-07-21) - Res. TSE 23.760/2026 · Res. TSE 23.610/2019 art. 33 · Politipédia AVM