mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 08:53:11 +00:00
Sync all project docs with the deployed remodel and hardening; compile roadmap
- Compile docs/roadmap.md into a next-steps document (277→206 lines): R0–R5/M1–M5 tables and the delivered map/list item paragraphs compressed into 'Já entregue'; janelas, grafo and cortes list only open IDs; C10/C11/C8 fill-ins keep their original triggers; C5 moved under 'A validar'. - Record the production deploy of the destructive remodel chain (2026-07-23 Vercel builds) across roadmap, AGENTS.md, the project notebook and plans — supersedes every 'deploy pendente'; smoke pós-deploy + onboarding remain the open blockers. - Fix AGENTS.md internal contradictions: Município locked-decisions bullet (435, Camaçari whole, candidate role), campaignUser role enum, deploy-checklist item 7 as historical, access/* structure note, election-aggregates artifact + election-tse revalidate runbook. - Sync open plans to real identifiers (plaza*/nucleus* → municipality*): the intelligence program (master + E8–E15/B13/C12 with artifact/caching context and satisfied deps), B8 rescoped to Salvador ZE 1–19 only, WhatsApp D3–D5, D2, A6 (now the insight layer over stateDeputy), RS+, onda-0 smoke step. - Close items verified done in code: C8 F4 (DRY de forms) complete, filtros-auto shared pending shell, petition generateStaticParams select/depth; ledger the surviving user-visible 'Praça' copy in src as new P2 debt. - Banner delivered praça-era plans (production note + rename pointer); mark remodelagem-pracas.md superseded and remodelagem-municipios.md delivered. - TECH-DEBT smell statuses, TESTING RBAC row, README quality gates, campanha UI rules updated to current component names.
This commit is contained in:
parent
8712ae9181
commit
adc0f31f5b
42 changed files with 573 additions and 612 deletions
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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<T>`), 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<T>` 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).
|
||||
|
|
|
|||
26
AGENTS.md
26
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://<prod-domain>/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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: —
|
||||
|
|
|
|||
|
|
@ -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<br/>(município→TI, frozen 2026)"]
|
||||
Roll["computeTerritoryRollups<br/>(Σ, 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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<br/>(série + válidos)"]
|
||||
Pot["plazaPotential (E8)<br/>captura/LQ/teto"]
|
||||
Base["municipalityElectoralBaseline<br/>(série + válidos)"]
|
||||
Pot["municipalityPotential (E8)<br/>captura/LQ/teto"]
|
||||
Class["computeDfTerritorialClass<br/>(â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
|
||||
|
|
|
|||
|
|
@ -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<br/>(série 2014/2018/2022)"]
|
||||
Campo["campoParties.ts"]
|
||||
Goals["global campaignGoals"]
|
||||
Pot["computePlazaPotential<br/>(válidos proj., teto, captura, roll-off)"]
|
||||
Pot["computeMunicipalityPotential<br/>(válidos proj., teto, captura, roll-off)"]
|
||||
Cov["computeGoalCoverage<br/>(Σ pledges ÷ meta)"]
|
||||
Pledges["aggregatePledgesByPlaza<br/>(votePledgeData.ts)"]
|
||||
UI["dashboard + PlazaList/Overview + detalhe"]
|
||||
Pledges["aggregatePledgesByMunicipality<br/>(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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<br/>locks em 1 RT · .returning · drizzle reads"]
|
||||
F2["Fase 2 — Perf lista<br/>1 COUNT · trgm · memo accessible nuclei"]
|
||||
F3["Fase 3 — DRY helpers<br/>drizzleBulk · getAccessibleNucleusIds variant · DrizzleTx assertion"]
|
||||
F4["Fase 4 — DRY forms<br/>migrar formActions restantes"]
|
||||
F3["Fase 3 — DRY helpers<br/>drizzleBulk · getAdvisorMunicipalityIds ✓ · DrizzleTx assertion"]
|
||||
F4["Fase 4 — DRY forms ✓<br/>(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`.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<br/>(plazaMapData.ts)"]
|
||||
Pot["plazaPotential (E8)"]
|
||||
Bundle["MunicipalityMapBundle<br/>(municipalityMapData.ts)"]
|
||||
Pot["municipalityPotential (E8)"]
|
||||
Class["computeDfTerritorialClass (E10)"]
|
||||
Breaks["computeQuantileBreaks / lq / rank<br/>(bahiaMapStyle.ts)"]
|
||||
Map["BahiaMap<br/>(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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<br/>(plazaPageData.ts)"]
|
||||
Pot["plazaPotential/goalCoverage (E8)"]
|
||||
Bundle["loadMunicipalityListPageBundle<br/>(municipalityPageData.ts)"]
|
||||
Pot["municipalityPotential/goalCoverage (E8)"]
|
||||
Fresh["freshness helper<br/>(pledges + lastUpdateAt)"]
|
||||
Sort["plazaQueueSort<br/>(déficit | risco | votos)"]
|
||||
List["PlazaList + PlazaListOverview<br/>(colunas novas)"]
|
||||
Sort["municipalityQueueSort<br/>(déficit | risco | votos)"]
|
||||
List["MunicipalityList + MunicipalityListOverview<br/>(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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<br/>(8 padrões, texto curado)"]
|
||||
Eval["evaluatePlazaTriggers<br/>(puro, determinístico)"]
|
||||
Eval["evaluateMunicipalityTriggers<br/>(puro, determinístico)"]
|
||||
Triage["triagem 1–5 + histerese<br/>(vs. última allocationDecision)"]
|
||||
UI["Dashboard painel + Praça seção<br/>(aceitar/adiar/descartar)"]
|
||||
UI["Dashboard painel + seção do município<br/>(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
|
||||
|
|
|
|||
|
|
@ -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<br/>(valida movimento)"]
|
||||
Plaza["plaza.engagementLevel"]
|
||||
Action["setMunicipalityEngagementLevel<br/>(valida movimento)"]
|
||||
Municipality["municipality.engagementLevel"]
|
||||
Dec["allocationDecision<br/>(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
|
||||
|
|
|
|||
|
|
@ -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<br/>leadership / nucleusUpdate /<br/>campaignInvite / voteEstimate"]
|
||||
Hook["afterChange hook<br/>leadership / municipalityUpdate /<br/>campaignInvite / voteEstimate"]
|
||||
Notif["Cria registro Notification<br/>(com req, dentro da transação)"]
|
||||
Bell["Sino in-app<br/>(lê Notification por campaignUser)"]
|
||||
Push["Push via Service Worker<br/>(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`.
|
||||
|
|
|
|||
|
|
@ -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://<prod-domain>/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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<br/>ZE → bairros Salvador"] --> catalog["src/lib/plazaZoneNeighborhoods.ts<br/>+ fixture"]
|
||||
treCMS["Locais de votação ZE 170/171<br/>+ curadoria"] --> catalog
|
||||
catalog --> ui["Bloco Bairros<br/>pracas/slug overview"]
|
||||
treSSA["TRE-BA RA 02/2017<br/>ZE → bairros Salvador"] --> catalog["src/lib/municipalityZoneNeighborhoods.ts<br/>+ fixture (F1 ✓)"]
|
||||
catalog --> ui["Bloco Bairros<br/>municipios/slug overview (F1 ✓)"]
|
||||
catalog --> reconcile["Reconciliação nomes<br/>↔ NM_BAIRRO IBGE/Geo"]
|
||||
ibge["BA_bairros_CD2022<br/>(ou GeoSalvador)"] --> script["scripts/build-plaza-zone-geometries.mjs"]
|
||||
ibge["BA_bairros_CD2022<br/>(ou GeoSalvador)"] --> script["scripts/build-municipality-zone-geometries.mjs"]
|
||||
reconcile --> script
|
||||
script --> topo["bahia-plaza-zones.topo.json"]
|
||||
topo --> map["BahiaMap + plazaMapData<br/>Praça-zona = feature própria"]
|
||||
script --> topo["bahia-municipality-zones.topo.json"]
|
||||
topo --> map["BahiaMap + municipalityMapData<br/>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
|
||||
|
|
|
|||
|
|
@ -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<br/>(signalType/source/triangulated)"]
|
||||
Update["municipalityUpdate kind=sinal<br/>(signalType/source/triangulated)"]
|
||||
Plan["actionPlan origin/resourceCost"]
|
||||
Dec["allocationDecision<br/>(imutável, staff)"]
|
||||
Series["leitores de série<br/>(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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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”).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<br/>heurística + edição"]
|
||||
Human["Aceitar / Editar / Descartar"]
|
||||
Dom["plazaUpdate / campaignDemand<br/>(+ pledge form)"]
|
||||
Dom["municipalityUpdate / campaignDemand<br/>(+ 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`
|
||||
|
|
|
|||
236
docs/roadmap.md
236
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<br/>(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<br/>(Consent finais + privacidade)"]
|
||||
JUR --> C2prod["C2 dados reais<br/>(base nominal)"]
|
||||
R2 --> C2prod
|
||||
TSE2026["TSE candidaturas 2026<br/>(após 15/08)"] --> A6["A6 Dobradinha 2026<br/>(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<br/>ao filtro ✓"]
|
||||
R2 -.-> B8["B8 Polígonos Praças-zona<br/>(SSA/CMS)"]
|
||||
R2 -.-> B10["B10 Hover/tap mapa ✓"]
|
||||
R2 -.-> B11["B11 Escala % válidos<br/>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<br/>fundação"]
|
||||
JUR -.chave WA canal.-> D3
|
||||
TSE2026["TSE candidaturas 2026<br/>(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<br/>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
|
||||
|
|
|
|||
Loading…
Reference in a new issue