Switch election geography queries from cityName to TSE cityCode via a static Bahia map and nucleusElectionGeography module, with simplify cleanup and plan updates for F3–F5 follow-ups.
19 KiB
Escala e DRY pós-A4 (baseline TSE no produto)
Status: Fases 1–2 implementadas (Fases 3–5 pendentes; merge pendente)
Atualizado em: 2026-07-20 (capture-review-debts pós-A7 F2 /simplify: débitos absorvidos no plano; gatilhos defer no Explicitamente fora)
Item do roadmap: docs/roadmap.md (Trilha A, item A7)
Responsável: —
Contexto
O A4 (baseline-eleitoral-tse.md Fases 2–4) entregou o baseline TSE 2022 no detalhe do núcleo e no overview da lista — implementado e mesclado em main (2026-07-18): getNucleusElectoralBaseline / loadNucleusBaseline2022Overview, computeGapVs2022, UI NucleusElectoralBaseline + NucleusInsights, card "Baseline 2022". Três passagens /simplify no mesmo PR já limparam o que cabia em cleanup: papéis genéricos (BASELINE_TICKET_2022), geographyWhere por cidade com zoneNumber in […], overview só consulta núcleos com estimativa confirmada, helpers compartilhados (toNucleusElectionGeographyInput, formatElectionNumber, NO_ELECTION_BASELINE_MESSAGE).
Os revisores (performance / reuse / quality) marcaram como importantes e maiores que simplify os follow-ups abaixo. Sem registro, o A5 (insights) e o coroplético do B3 herdariam o mesmo custo de I/O no path do detalhe.
- Detalhe puxa todas as linhas nominais de dep. federal (1º turno) na geografia.
Resolvido na Fase 1 (2026-07-19):detailVoteWhere+loadElectionVotesloadFederalCandidateTotalsAggregated(src/utilities/federalCandidateTotalsAggregate.ts) agrega via drizzleSUM(votes) GROUP BY candidate_number; o detalhe usa Local API só para presidente/governador da chapa (loadTicketOfficeVotes). A série histórica E2 (loadCandidateSeriesByGeography) já era magra porcandidateNumber. - Filtro geográfico por
cityName(texto), não porcityCodeTSE.O índice natural das collections de eleição éResolvido na Fase 2 (2026-07-20): mapa estático(year, office, turn, cityCode, zoneNumber); filtrar por nome impedia o plano ótimo e complicava OR grandes na união do overview.bahiaTseCityCodes.ts+nucleusElectionGeography.ts;geographyWheree drizzle federal usamcityCode+zoneNumber in […]. - DRY de UI do Gap "acima".
NucleusInsightsusaAlert variant="pending"para abaixo/neutro, mas o estado "acima" aplica classes ad-hoc com tokens--estimate-confirmed*. O slice A5-1 conversão reutiliza o mesmoconfirmedInsightAlertClasspara o alerta de taxa de conversão (reduto/consolidado) — interim até a Fase 3. A barra do candidato no card também reimplementarole="progressbar"em vez de reusarProgress(NucleusListOverview). - Query duplicada de votos 2022 na lista de núcleos com coroplético B3.
nucleusListOverviewPageData.tschama em paraleloloadNucleusListElectionOverview(union →loadCandidateSeriesByGeographymulti-ano) eloadNucleusChoroplethBundle→loadBaseline2022VotesByCityNames(segunda passagemloadCandidateSeriesByGeographysó 2022 sobre geografia sobreposta). O simplify B3 unificou resolve interno do choropleth, mas não compartilha o resultado da série 2022 já obtida no loader de gap/tendência/conversão. - N+1 de agregação federal no flip da lista (A5 Fase 2).
loadNucleusListElectionOverviewchamaloadFederalCandidateTotalsAggregateduma vez por núcleo com geografia resolvida paracomputeTicketFlipForGeography, enquanto série/ticket/majoritarian já vêm numa união. Com dezenas de núcleos filtrados,Promise.allparaleliza latência mas não reduz carga no Postgres — mesmo anti-padrão que A7 F1 resolveu no detalhe. - Re-scan in-memory de linhas union por núcleo (leverage A5). No loop
comparableIndexes,majoritarianTicketVoteTotalsfiltra todoticketVotes(union) por geografia a cada iteração; o flip por núcleo re-filtramajoritarianTalliesunion. Aceitável em filtros pequenos; escala com TI inteira.
Já resolvido no simplify pós-F1 / capture-review-debts (não reabrir): remoção de cities derivado em NucleusElectionGeography (só zonesByCity + cityZonePairs); assertCanReadElectionData com asserts e entrada CampaignUser | User | null | undefined; testes reais de loadFederalCandidateTotalsAggregated (mock drizzle) em federalCandidateTotalsAggregate.unit.spec.ts; int spy estrutural (whereContainsField*) em vez de JSON.stringify(where); inline do SQL builder federal; comentário em aggregateFederalCandidateTotals como oracle in-memory para unit tests.
Já resolvido no simplify pós-A5 alavancagem (não reabrir): ticketFlip pré-computado no VM (aggregateNucleusElectoralBaseline) e consumido em NucleusInsights (paridade detail/overview em empates majoritários); resolveMajoritarianWinners compartilhado; winners/federalRace reutilizados sem segunda passagem no detalhe; Promise.all série+ticket+majoritarian no overview (remove waterfall); loop flip sem null filter; guard redundante leverage.ticketVotes; testes gap com GapVs2022Baseline; assert ticketFlip no unit de baseline.
Já resolvido no simplify pós-A5-2 classificação territorial (não reabrir): sumFederalTallyForGeography único (substitui sumElectorateForGeography + helper paralelo); reuso de classificationTalliesByCityZone no loop de conversão (elimina 2º loadElectionTallies em conversionUnion); loop de classificação in-memory antes do await do flip N+1; push só de bandas comparáveis (isComparableTerritorialClass) — evita linha overview 0 defesa · …; TERRITORIAL_CLASS_UI canônico + accessors finos; TerritorialClassIcon no detalhe.
Já resolvido no simplify pós-F2 cityCode (não reabrir): matchesGeography permanece helper privado em nucleusElectoralBaseline.ts (não exportado de nucleusElectionGeography); consumers de geografia importam direto de nucleusElectionGeography (nucleusChoropleth*, nucleusDetailPageData, nucleusListOverviewPageData, campaignDashboardPageData, specs); re-exports mortos removidos do barrel baseline; testes de resolveNucleusElectionGeography em tests/unit/nucleusElectionGeography.unit.spec.ts; int spy federal T1 + tally cityCode fundidos num único teste (tseCityCodeForMunicipality('Salvador') em vez de código hardcoded); mock obsoleto toNucleusElectionGeographyInput removido de campaignNucleusDetailTabs.unit.spec.ts.
Explicitamente fora (revisores pediram skip no simplify ou descartados no triage): map JSX de linhas Lula/Jerônimo, dropar candidate.rank / electorate.aptos / ratio do tipo público só por higiene (só fariam sentido se a F1 deixar de calcular rank), cast as unknown as RawOverviewNucleus da B1 (não é débito novo do A4), parity test drizzle vs aggregateFederalCandidateTotals (int + unit cobrem comportamento), typed drizzle rows/zod no drizzleResultRows (padrão plataforma C6/C8), requirePostgresDrizzle em supporterListOverviewAggregate.ts (fora do escopo A7 — DRY de ~6 linhas; ver C10 quando o plano existir), micro-opt aggregateElectorate/matchesGeography com Set (impacto negligível), cache zonesByCityCode no objeto NucleusElectionGeography (defer: só se hot path comprovado ou ≥3 recomputações por request), mover buildUnionGeography para nucleusElectionGeography (defer: 3º call site de union geography ou refactor A7 F5), BahiaMap setStyle incremental → B6 (escala-dry-pos-b3.md), factory geometrias mun/TI → B5 F3 (escala-dry-pos-b2.md); pós-A5: buildUnionGeography 2× (micro-opt), mover testes leverage/flip para electionInsights.unit.spec.ts, unions discriminadas TicketLeverage/TicketFlip, estreitar campos VM/result flip, ticketFlipAlertVariant/formatPercent export, MajoritarianTallyRow tipo separado, maps de ícones em NucleusInsights, wrapper NucleusInsightAlert além da F3 (E7 adia DRY do stack até mais insights); pós-A5-2: agregador genérico conversion+territorial, achatar NucleusClassificationOverviewAggregate, percentValid só em testes, merge CPU dos loops sumCandidateVotesForGeography trend+classificação, 4ª query loadElectionTallies no union (custo intencional A5-2), territorialClassLabel na linha do overview (conversão também hardcoded), componente compartilhado de linha de distribuição → E6 F3; pós-F2: assertions redundantes no int bahiaTseCityCodes (padrão intencional espelhando fixture IBGE); falhas pré-existentes campaignNucleusIntelligenceUi.unit.spec.ts (fora do escopo A7 F2).
Objetivos
- Abrir a aba Visão geral de um núcleo com TI/multi-município permanece barato: o loader de baseline não transferir o conjunto completo de candidatos federais da geografia.
- Filtros de geografia eleitoral preferem
cityCodeTSE quando o mapa nome→código existir, sem mudar a UX (continua resolvendo a partir decities/regionsdo núcleo). - Gap "acima" usa a mesma família de variantes do Alert que o âmbar (
pending); a barra do candidato reusaProgress. - Lista com flip A5 não escala com N round-trips drizzle de federal por núcleo; Fase 5 batch na união antes de particionar por geografia.
- Guardrails: preferir sem migration nas Fases 1 e 3; Fase 2 pode ser só tabela estática (como
bahiaMunicipalityCodes/ seed TSE) — se precisar de índice novo no Postgres, migration dedicada e cortável. Sem Consent (dado público TSE). Access continuaoverrideAccess: false+canReadElectionData.
Decisões travadas
- Um item A7, cinco fases ordenadas. Mesmo racional do B5/C7/C8/E7: um ID de roadmap, PRs por fase. Ordem: agregação do detalhe (custo real) →
cityCode(escala BA-wide) → DRY de Alert/Progress + helpers gap (barato, UI) → fetch único 2022 lista+coroplético (B3) → batch federal + indexação union na lista (A5 flip/leverage). - Dependência dura de A4. Só faz sentido com o loader/UI já no produto; não reabre o escopo do Gap vs 2022 nem do card Baseline.
- Fase 1 não remove o "mais votado aqui" da UI. O produto do A4 inclui
winnerFederal; a otimização agrega no SQL/drizzle (ou limita o payload), não corta a feature. Se produto decidir que rank/winner saem do card, a F1 encolhe para "só o candidato da chapa + tallies". - Cortável se a geografia real dos núcleos permanecer pequena (1–2 municípios tipados). Vira não-cortável de qualidade quando A5/B3 passarem a martelar o mesmo helper em TI inteiro.
- Não registrar no A7 o cast
as unknowndo overview B1 — débito de tipagem do select Payload da lista; só tocar se um PR de A7 já estiver no arquivo. - i18n e naming (AGENTS.md): identificadores em inglês (
aggregateFederalTotalsByGeography,tseCityCodeForMunicipality,alertVariants.confirmed), strings visíveis em pt-BR inalteradas. - Impeccable: Fase 1 = classe A (só backend); Fase 2 = A; Fase 3 = B (encaixe em
Alert/NucleusInsights/NucleusElectoralBaselineexistentes).
Questões em aberto
- Fase 1: Local API
find+ agregação em memória vspayload.db.drizzleGROUP BY? Resolvido (2026-07-19): drizzleGROUP BY candidate_numberemfederalCandidateTotalsAggregate.ts, com gate ACL explícito (assertCanReadElectionData) antes doexecute; presidente/governador e série histórica permanecem no Local API magro. - Fase 2: de onde vem o mapa nome canônico →
CD_MUNICIPIOTSE? Recomendação: extrair do próprio seed/electionTally/electionCandidateVote(já têmcityCode+cityName) para uma tabela estática gerada (src/lib/bahiaTseCityCodes.ts) no mesmo espírito debahiaMunicipalityCodes(IBGE) — não confundir com código IBGE. Script re-executável opcional; sem migration se for só arquivo versionado. - Fase 3: nome da variante Alert —
confirmedouestimate-confirmed? Recomendação:confirmed, alinhado aos tokens CSS--estimate-confirmed/ classe utilitária já usada pelo Badgeestimate-confirmed, sem inventar terceira nomenclatura.
Abordagem proposta
flowchart TD
A4["A4 Baseline no produto ✓"] --> F1
F1["Fase 1 — Agregar federal no detalhe<br/>(GROUP BY / payload estreito) ✓"]
F1 --> F2["Fase 2 — Geografia por cityCode TSE ✓"]
F1 --> F3["Fase 3 — Alert confirmed + Progress<br/>+ gapVs2022 helpers"]
F1 --> F4["Fase 4 — Fetch 2022 único<br/>lista overview + coroplético"]
F1 --> F5["Fase 5 — Batch federal lista<br/>+ index union (A5 flip/leverage)"]
F1 -.mesmo loader.-> A5["A5 Insights ✓"]
F1 -.métricas.-> B3["B3 Leaflet coroplético"]
F4 -.evita 2ª query.-> B3
F5 -.evita N queries flip.-> A5
Fase 1 — Agregação do ranking federal no detalhe ✓ (2026-07-19)
src/utilities/federalCandidateTotalsAggregate.ts: drizzleSUM(votes) GROUP BY candidate_number+assertCanReadElectionData.src/utilities/nucleusElectoralBaseline.ts:loadTicketOfficeVotes(Local API magro) +aggregateNucleusElectoralBaselinerecebefederalTotalsjá agregados;getNucleusElectoralBaselinewidened paraPick<Payload, 'find' | 'db'>.- Testes: unit
federalCandidateTotalsAggregate.unit.spec.ts; int spy empayload.findgarante que queries federal T1 no Local API sempre filtramcandidateNumber.
Fase 2 — Filtro por cityCode TSE ✓ (2026-07-20)
src/lib/bahiaTseCityCodes.ts+pnpm build:tse-city-codes(TSE 2022detalhe_votacao_munzonaBA).src/utilities/nucleusElectionGeography.ts: tipos/helpers extraídos;cityZonePairscomcityCode;geographyWhere+zonesByCityCode.federalCandidateTotalsAggregate.ts: SQL emcity_code.- Fail-closed quando município não mapeia; sem migration.
Fase 3 — Alert confirmed + Progress + helpers Gap vs 2022
src/components/ui/Alert.tsx: adicionar variantconfirmedespelhandopendingcom tokens--estimate-confirmed/ foreground.NucleusInsights: trocar ocn(...)ad-hoc econfirmedInsightAlertClass(Gap "acima" e conversão reduto/consolidado desde A5-1 e leverage ≥100% / tendência increase) porvariant={… ? 'confirmed' : 'pending'}(oudefaultsó se neutro — manter pending para noBaseline/noEstimate).src/lib/electionInsights.ts:gapVs2022AlertVariant(status)esupportLineemcomputeGapVs2022(ougapVs2022SupportLine) — simetria comconversionRateAlertVariant/ticketLeverageAlertVariant; remove ternários e string montada no componente.NucleusElectoralBaseline: substituir a barra custom porProgress(já usado no overview), preservandoaria-label.
Fase 4 — Fetch único de votos 2022 (lista overview + coroplético B3)
loadNucleusListElectionOverviewjá obtémvotes2022vialoadCandidateSeriesByGeographyna união do conjunto filtrado — expor helper ou retorno auxiliar comMap<cityName, votes>(ou reutilizarsumCandidateVotesForGeographypor cidade) sem segunda query.loadNucleusChoroplethBundle/nucleusListOverviewPageData.ts: aceitarbaselineVotesByCitypré-carregado do overview (ou funçãoloadNucleusListOverviewElectionAndChoroplethque faz uma passagem de série + monta gap/trend/conversão + bundle).- Manter
loadBaseline2022VotesByCityNamespara call sites isolados (dashboard geral, detalhe) até um follow-up opcional. - Testes: int ou unit garantindo que overview com coroplético não chama
loadCandidateSeriesByGeographyduas vezes para 2022 (spy estrutural como Fase 1 federal). - Verificar: paridade numérica do coroplético
baseline2022Votesantes/depois.
Fase 5 — Batch federal na lista + indexação union (A5 flip/leverage)
- Uma query drizzle na união do conjunto filtrado:
GROUP BY city_name, zone_number, candidate_number(oucity_codeapós F2), materializando mapacityZoneKey → federalTotals[]reutilizável por núcleo — mesmo espírito da F1 no detalhe, estendido ao pathloadNucleusListElectionOverview. - Substituir o
Promise.allde N×loadFederalCandidateTotalsAggregatedno flip por slice in-memory por geografia de cada núcleo. - Pré-indexar
ticketVotesemajoritarianTalliesunion porcityZoneKey(ou fingerprint de geografia) antes do loopcomparableIndexes;majoritarianTicketVoteTotals/resolveMajoritarianWinnersleem do índice em O(pares do núcleo) em vez de re-filtrar todas as linhas union. - Helper compartilhado
sumElectorateFromTalliesunificandoaggregateElectorateesumElectorateForGeography(overview vs detalhe). - Testes: int spy garantindo uma execução drizzle federal na lista com N núcleos (espelhar guard da F1); unit de particionamento por geografia; paridade numérica flip/leverage antes/depois.
- Cortável se filtros típicos permanecerem em poucos núcleos (≤10); não cortável quando TI-wide ou dezenas+ núcleos forem rotina.
Migration: nenhuma nas Fases 1, 3, 4 e 5. Fase 2 preferencialmente só artefato estático versionado; índice Postgres extra só se o EXPLAIN ainda doer após cityCode (aí pnpm migrate:create cortável).
Dependências
- Dura: A4 Baseline no produto + Gap vs 2022 — implementado (loader + UI).
- Suave: A5 Insights e B3 coroplético — consomem/amplificam o mesmo helper; se A5 chegar antes da F1, herda o custo.
- Reusa:
BASELINE_TICKET_2022,ba2022Scope/geographyWhere, collectionselectionCandidateVote/electionTally, padrão drizzle doscripts/seed-tse-results.mjs, tokensestimate-confirmed/estimate-pending,Progress.
Não escopo
- Novos insights de produto → A5 (planos
insight-*.md). - Dobradinha 2026 → A6.
- Lazy load de geometrias / cache CLI / factory mun-TI → B5 (F1 ✓, F2–F3 pendentes).
BahiaMapsetStyleincremental → B6 (escala-dry-pos-b3.md).- Escala de apoiadores / agenda → C6/C7/C8.
- Remover rank/winner da UI por decisão de produto sem agregação — fora; se produto cortar, fechar F1 como "só chapa + tallies".
- Tipagem genérica dos
selectPayload do overview B1.
Referências
docs/roadmap.md(Trilha A, item A7; A4/A5/B3)docs/plans/baseline-eleitoral-tse.md— plano pai do A3/A4docs/plans/escala-dry-pos-b2.md/escala-dry-pos-c3.md/escala-dry-pos-c6.md— precedente pós-/simplifysrc/utilities/federalCandidateTotalsAggregate.ts— drizzle aggregate federal T1 (Fase 1)src/utilities/nucleusElectoralBaseline.ts—loadTicketOfficeVotes,aggregateNucleusElectoralBaseline, série E2 leansrc/utilities/campaignAccess.ts—assertCanReadElectionData/ElectionDataReader(gate ACL do path drizzle)src/utilities/drizzleBulk.ts—requirePostgresDrizzle(read-only aggregates)docs/plans/insight-taxa-conversao.md— A5-1 usaconfirmedInsightAlertClassaté F3docs/plans/insight-alavancagem-chapa.md— A5 flip/leverage (motiva F5)src/lib/electionInsights.ts/src/components/campaign/NucleusInsights.tsx— Gap + conversão UIsrc/components/campaign/NucleusElectoralBaseline.tsx— barra do candidatosrc/components/ui/Alert.tsx— variantpendinga espelharsrc/components/ui/Progress.tsx— reuso no cardsrc/utilities/nucleusListOverviewPageData.ts—Promise.alloverview + choroplethsrc/utilities/nucleusChoroplethPageData.ts—loadBaseline2022VotesByCityNames- AGENTS.md — Election baseline data; Local API
overrideAccess: false; naming inglês