Surface brancos/nulos/abstenções as mobilization opportunity on nucleus detail and list overview, with loader aggregate and unit tests; register pending int coverage in escala-dry-pos-e2.
16 KiB
Escala e DRY pós-E2 (série TSE 2014/2018 + tendência)
Status: Fase 2 entregue (2026-07-19); extensão F2 pós-A5 / pós-A5-2 / pós-A5-mobilização pendente; Fases 1, 3 e 4 pendentes
Atualizado em: 2026-07-19 (extensão int mobilization absorvida via capture-review-debts pós-A5 mobilização; leverage/flip/classification já registrados; E4 import planilha cortado)
Item do roadmap: docs/roadmap.md (Trilha E, item E7)
Responsável: —
Contexto
O E2 (mapa-projecao-municipios.md) entrega seed pnpm db:seed:tse -- --year=2014|2018|2022 (federal T1 BA nos anos históricos), computeVoteTrend ±10% (increase|stable|decline|noBaseline), série no card NucleusElectoralBaseline, alerta em NucleusInsights e card "Tendência histórica" no overview B1 — loader unificado loadNucleusListElectionOverview (gap + tendência numa query union) em nucleusElectoralBaseline.ts. O slice A5-1 taxa de conversão (insight-taxa-conversao.md, 2026-07-19) estende o mesmo loader com agregado conversion (weightedRate + distribution por faixa) e fetch condicional de electionTally — ver Fase 2. O slice A5 mobilização (insight-mobilizacao-brancos-nulos.md, 2026-07-19) acrescenta mobilization (absoluteTotal, relativePercent, subtotais) via aggregateMobilizationOverview — int pendente nesta extensão F2.
Duas passagens /simplify no mesmo branch já limparam o que cabia em cleanup: HISTORICAL_PRIOR_SERIES_YEARS, HISTORICAL_SERIES_YEARS, buildUnionGeography, aggregateVoteTrend / comparableTrendCount, formatVoteTrendSeries / formatVoteTrendSeriesCompact, voteTrendAlertVariant, merge de labels, FEDERAL_DEPUTY_OFFICE no build, remoção de seriesFromYearTotals e alias NucleusListOverviewBaseline2022.
Os revisores (performance / reuse / quality) marcaram como importantes e maiores que simplify os follow-ups abaixo. Débitos que já têm dono em outro item ficam fora deste plano (ver Não escopo).
- Tendência calculada e exibida duas vezes na aba Visão geral do núcleo.
NucleusActiveTab.tsxrenderizaNucleusElectoralBaselineeNucleusInsightsem sequência; cada um chamacomputeVoteTrend(baseline.series)e ambos mostram a série 2014→2018→2022 (badge + mensagem no card; alerta + mensagem + série no insights). Duplicação de trabalho no cliente e de informação para o usuário. - Sem testes de integração do loader combinado.
Resolvido na Fase 2 (2026-07-19):loadNucleusListElectionOverviewsubstitui loaders separados de gap e tendência; só há cobertura unitária…tests/int/nucleusListElectionOverview.int.spec.ts+seedMultiYearFederalCandidateFixtureemtests/helpers/tseFixtures.ts. - Tipos de distribuição duplicados.
NucleusTrendOverviewAggregateemnucleusElectoralBaseline.tséRecord<VoteTrendStatus, number>enquantoelectionInsights.tsjá expõeVoteTrendDistribution+emptyVoteTrendDistribution— mesmo shape, dois nomes. VoteTrendResult.ratioexposto na API pública mas usado sobretudo em asserts de teste; o produto só consomestatus+messagena UI.- Helpers de teste eleitoral duplicados e wipe lento.
deleteAllElectionDataestá copiado em três int specs (electionResultsImport,nucleusElectoralBaseline,nucleusListElectionOverview); cada um fazfind+deletepor documento nas três collectionselection*. O import de produção já usa delete por escopo via Drizzle (electionResultsImport.ts).
Já resolvido no simplify A5-1 / capture-review-debts (não reabrir): tallies só quando há núcleos comparáveis; union de tallies restrita a comparableIndexes; sumElectorateForGeography indexado por cityZoneKey (mesmo padrão de votes2022); conversão desacoplada do gate candidateVotes > 0; gap no overview via computeGapVs2022; isComparableConversionBand / formatElectionNumber DRY no overview.
Já resolvido no simplify pós-E7 F2 (não reabrir): loadOverview sem findByID por teste (generalUser em closure); assert único de conversion.distribution; TSE_FIXTURE_ZONE_EXPECTED com abstencoes/confirmedVoteEstimate; remoção de feiraZ10 (zone 10 do CSV ≠ zonas TSE oficiais).
Pendente pós-A5 (capture-review-debts): int de leverage/flipOpportunity no loader combinado; spy estrutural de que loadFederalCandidateTotalsAggregated não é chamado N vezes na lista (documenta débito até A7 F5 fechar o batch).
Pendente pós-A5-2 (capture-review-debts): int de classification.distribution no mesmo loadNucleusListElectionOverview (fixture com validos > 0 e bandas comparáveis; assert classification: null quando só semBaseline).
Pendente pós-A5-mobilização (capture-review-debts, ~0,25 dia eng): int de mobilization no mesmo loader — asserts de absoluteTotal, relativePercent, brancosNulosTotal, abstençõesTotal sobre o conjunto filtrado (fixture Salvador Z1+Z2 já traz QT_ABSTENCOES / brancos / nulos no detalhe_votacao_munzona_fixture.csv); caso negativo mobilization: null quando geografia vazia (já assertado no int existente) ou potencial zero/semAptos.
Já resolvido no simplify pós-A5 mobilização (não reabrir): loops de classificação + mobilização unificados; mobilizationInput acumulador único; federalTallyByIndex elimina segunda sumFederalTallyForGeography no loop de conversão; aggregateMobilizationOverview + unit tests; formatPercent hoisted; remoção de mobilizationAlertVariant (variant pending inline).
Explicitamente fora (revisores pediram skip no simplify, capture-review-debts ou já têm item): ranking federal completo no detalhe e filtro por cityCode → A7; query duplicada lista+overview e re-resolve de geografia no baseline do overview → E6; DRY de formatElectionNumber nos cards não-tendência do overview → E6 F3; remover a query TSE do overview quando não há estimativas comparáveis (o card de tendência exige distribuição mesmo sem gap — comportamento intencional de produto); chip de faixa no Alert de conversão (produto adia); DRY do stack de 3 Alerts em NucleusInsights até mais insights A5; ranking/sort da lista por mobilização → gatilho em insight-mobilizacao-brancos-nulos.md (produto pedir sort); helpers cosméticos mobilizationUnavailable / guard supportLine / remover isComparableMobilization (paridade com conversão/leverage); imports sequenciais 2014/2018/2022 em seedMultiYearFederalCandidateFixture (fixture pequena; paralelizar só se CI atrasar); documentação de isolamento entre suites int que apagam election* na mesma DB (mitigado por teqo_test por worker).
Objetivos
- Abrir a aba Visão geral de um núcleo calcula a tendência uma vez e não repete a série histórica em dois blocos adjacentes.
loadNucleusListElectionOverviewtem pelo menos um teste int com fixture TSE que cobre gap agregado + distribuição de tendência + agregado de conversão (weightedRate,distributionreduto/consolidado/oportunidade) no mesmo path; extensão pós-A5: agregadosleverage/flipOpportunity+ spy de query count federal (guardrail contra regressão N+1 até A7 F5); extensão pós-A5-2: agregadoclassification.distribution(defesa/indecisa/ataque/perdida) + casoclassification: nullsem válidos; extensão pós-A5-mobilização: agregadomobilization(Σ brancos/nulos/abstenções +%ponderado) espelhandoaggregateMobilizationOverview.- Tipos de distribuição de tendência têm uma única fonte (
VoteTrendDistribution); loaders e VMs importam deelectionInsights.ts. - Helpers compartilhados para wipe/seed de dados eleitorais nos int tests (sem triplicar
deleteAllElectionData). - Guardrails: sem migration, sem Consent (dado público TSE). Access continua
overrideAccess: false. Identificadores em inglês; strings visíveis em pt-BR. - Impeccable: N/A nas fases 2 e 4 (só testes/helpers); Fase 1 é classe B (encaixe em
NucleusActiveTab/ cards existentes).
Decisões travadas
- Um item E7, quatro fases ordenadas. Mesmo racional de A7/C8/E6: um ID no roadmap, PRs por fase. Ordem: UX do detalhe → int do loader → higiene de tipos/API → helpers de teste eleitoral.
- Dependência dura de E2 mergeado. Só faz sentido com seed multi-ano,
computeVoteTrendeloadNucleusListElectionOverviewjá no código. - Fase 1 não remove o insight de tendência nem o badge no baseline — apenas unifica o cálculo e evita repetir a série literal; o alerta pode mostrar só
trend.messagese o card acima já exibirformatVoteTrendSeriesCompact. - Cortável se poucos núcleos e time de campo não reclamar da duplicação visual — Fase 2 (int) é a mais valiosa antes de E4 import em massa amplificar o conjunto filtrado.
- i18n e naming (AGENTS.md):
VoteTrendDistributioncanônico;NucleusTrendOverviewAggregatevira alias deprecado ou some na Fase 3.
Questões em aberto
- Fase 1: tendência só no Insights ou só no Baseline? Recomendação: manter badge + série compacta no
NucleusElectoralBaseline(paridade com designBaseline-Eleitoral-2022);NucleusInsightsrecebetrend: VoteTrendResultpor prop e mostra só o alerta comtrend.message(sem repetir a série).computeVoteTrendroda uma vez emNucleusActiveTab. - Fase 2: fixture int separada ou estender
tseFixtures.ts? Recomendação: estendertests/helpers/tseFixtures.tscom série 2014/2018/2022 mínima para dois núcleos sobrepostos na geografia union; reutilizar padrão deelectionResultsImport.int.spec.ts. - Fase 3: remover
ratiodeVoteTrendResult? Recomendação: sim, se nenhum caller de produção ler — ajustar testes para assertar sóstatus/messageou testarratiovia função interna de par se ainda for útil para regressão.
Abordagem proposta
flowchart TD
E2["E2 Série + tendência ✓"] --> F1
F1["Fase 1 — Trend VM único no detalhe<br/>(NucleusActiveTab → filhos)"]
F1 --> F2["Fase 2 — Int loadNucleusListElectionOverview ✓"]
F1 --> F3["Fase 3 — VoteTrendDistribution único + API ratio"]
F2 --> F4["Fase 4 — Helpers teste eleitoral<br/>(clearElectionFixtureData)"]
F2 -.confiança antes.-> E4["E4 Import planilha"]
A7["A7 Loader baseline"] -.mesmo módulo.-> F2
E6["E6 Lista/overview"] -.geo cache.-> F2
Fase 1 — Tendência única na aba Visão geral
src/components/campaign/NucleusActiveTab.tsx:const trend = baseline ? computeVoteTrend(baseline.series) : null; passartrendopcional paraNucleusElectoralBaselineeNucleusInsights.NucleusElectoralBaseline: aceitartrend?: VoteTrendResult | null— se presente, não chamarcomputeVoteTrendinternamente.NucleusInsights: aceitartrendpor prop; remover segunda cópia da série noAlertDescription(manter só mensagem classificada).- Testes: ajustar
nucleusElectoralBaselineUi.unit.spec.tsse asserts dependerem do texto duplicado da série.
Fase 2 — Testes int do loader combinado ✓ (2026-07-19)
tests/helpers/tseFixtures.ts—seedMultiYearFederalCandidateFixture,TSE_FIXTURE_ZONE_EXPECTED.tests/int/nucleusListElectionOverview.int.spec.ts— gap+trend+conversão, tendência sem estimate, geografia vazia, conversão ponderada e aptos sem votos 2022 do candidato.
Extensão Fase 2 (pós-A5 / pós-A5-2 / pós-A5-mobilização — pendente)
Appetite do lote F2 ext.: ~0,75–1 dia eng (leverage/flip spy + classification + mobilization int; sem UI).
- Estender fixture TSE com votos de chapa majoritária + federal proporcional por partido suficientes para
leveragecomparável eflipOpportunitycomstatus: 'opportunity'. - Asserts em
loadNucleusListElectionOverview:leverage.weightedPercent/unconvertedCounteflipOpportunity.count/bothAlignedCount. - Spy estrutural: com N núcleos no filtro,
loadFederalCandidateTotalsAggregated(ou execute drizzle federal) não deve ser invocado N vezes — falha documenta regressão até A7 F5; após F5, o spy valida uma única batch query. - Pós-A5-2: asserts de
classification.distributionsobre o conjunto filtrado com geografia; caso negativo com tallies semvotosValidos→classification: nulle overview sem linhaClassificação:. - Pós-A5-mobilização: em
tests/int/nucleusListElectionOverview.int.spec.ts, assertresult.mobilizationpara[salvadorZ1, salvadorZ2]com totais derivados deTSE_FIXTURE_ZONE_EXPECTED+ brancos/nulos do fixture (detalhe_votacao_munzona_fixture.csvcargo federal T1); espelharaggregateMobilizationOverview(não duplicar fórmula inline). Casoaptos: 0ou geografia vazia →mobilization: null(vazio já coberto).
Fase 3 — Tipos e API
- Substituir
NucleusTrendOverviewAggregatepor import deVoteTrendDistributionnos VMs (nucleusListOverviewViewModels.ts,nucleusElectoralBaseline.tsexports). - Avaliar remoção de
VoteTrendResult.ratioou torná-lo@internaldocumentado; atualizarelectionInsights.unit.spec.ts.
Fase 4 — Helpers de teste eleitoral
tests/helpers/electionTestHelpers.ts(nome final a confirmar na implementação):clearElectionFixtureData(payload)que apagaelectionCandidateVote,electionTallyeelectionCandidateem lote (espelhardeleteScope/drizzleBulkdesrc/utilities/electionResultsImport.ts, não N×payload.delete).- Migrar os três int specs para o helper:
tests/int/electionResultsImport.int.spec.ts,tests/int/nucleusElectoralBaseline.int.spec.ts,tests/int/nucleusListElectionOverview.int.spec.ts. - Manter
seedMultiYearFederalCandidateFixtureemtseFixtures.ts; Fase 4 só consome o wipe compartilhado. - Verificar:
pnpm test:intverde; sem tocar Neon (teqo_test).
Migration: nenhuma nas quatro fases.
Dependências
- Dura: E2 Série TSE 2014/2018 + tendência — implementado no branch (aguardando merge).
- Suave: E4 import planilha — amplifica valor da Fase 2; A7 F1 compartilha
nucleusElectoralBaseline.ts(não misturar PRs sem necessidade); E6 F2 cache de geografia reduz custo do mesmo path do overview. - Reusa:
electionInsights.ts,nucleusElectoralBaseline.ts,nucleusListOverviewPageData.ts,tests/helpers/tseFixtures.ts,src/utilities/electionResultsImport.ts(deleteScope),src/utilities/drizzleBulk.ts, padrão int deelectionResultsImport.int.spec.ts.
Não escopo
- Agregar ranking federal no detalhe → escala-dry-pos-a4.md (A7 F1).
- Filtro geográfico por
cityCodeTSE → A7 F2. - Query duplicada
/campanha/nucleoslista + overview → escala-dry-pos-e1.md (E6 F1). - Re-resolve de geografia no overview baseline → E6 F2.
formatElectionNumbernos cards de estimativa/cobertura/metas do overview → E6 F3.- Seed 2014 se formato TSE incompatível (spike já validou 2018; 2014 é risco separado no runbook do seed).
- Persistir tendência no banco (permanece derivada em leitura).
Referências
docs/roadmap.md(Trilha E, E7; E2 implementado)docs/plans/mapa-projecao-municipios.md— plano pai E2docs/plans/escala-dry-pos-a4.md/escala-dry-pos-e1.md— débitos adjacentes (A7 / E6)src/components/campaign/NucleusActiveTab.tsx— render duplo baseline + insightssrc/components/campaign/NucleusElectoralBaseline.tsx/NucleusInsights.tsxdocs/plans/insight-taxa-conversao.md— slice A5-1 (conversão no loader/overview)docs/plans/insight-mobilizacao-brancos-nulos.md— slice A5 mobilização (loader/overview; ranking adiado)src/lib/electionInsights.ts—computeVoteTrend,computeConversionRate,aggregateConversionBand,VoteTrendDistribution,aggregateVoteTrend,computeMobilizationOpportunity,aggregateMobilizationOverviewsrc/utilities/nucleusElectoralBaseline.ts—loadNucleusListElectionOverview,getNucleusElectoralBaselinesrc/utilities/nucleusListOverviewPageData.tstests/unit/electionInsights.unit.spec.ts/tests/unit/nucleusElectoralBaselineUi.unit.spec.tstests/helpers/tseFixtures.ts/tests/int/electionResultsImport.int.spec.ts/tests/int/nucleusListElectionOverview.int.spec.tstests/helpers/electionTestHelpers.ts— alvo da Fase 4 (a criar)- AGENTS.md — naming,
overrideAccess: false, seed guards