teqo-backup/docs/plans/escala-dry-pos-e2.md
Francisco Ignácio Fontoura Solla ea579d2173 Deliver A5 mobilization insight and absorb E7 F2 int follow-up.
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.
2026-07-19 22:34:35 -03:00

16 KiB
Raw Permalink Blame History

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).

  1. Tendência calculada e exibida duas vezes na aba Visão geral do núcleo. NucleusActiveTab.tsx renderiza NucleusElectoralBaseline e NucleusInsights em sequência; cada um chama computeVoteTrend(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.
  2. Sem testes de integração do loader combinado. loadNucleusListElectionOverview substitui loaders separados de gap e tendência; só há cobertura unitária… Resolvido na Fase 2 (2026-07-19): tests/int/nucleusListElectionOverview.int.spec.ts + seedMultiYearFederalCandidateFixture em tests/helpers/tseFixtures.ts.
  3. Tipos de distribuição duplicados. NucleusTrendOverviewAggregate em nucleusElectoralBaseline.ts é Record<VoteTrendStatus, number> enquanto electionInsights.ts já expõe VoteTrendDistribution + emptyVoteTrendDistribution — mesmo shape, dois nomes.
  4. VoteTrendResult.ratio exposto na API pública mas usado sobretudo em asserts de teste; o produto só consome status + message na UI.
  5. Helpers de teste eleitoral duplicados e wipe lento. deleteAllElectionData está copiado em três int specs (electionResultsImport, nucleusElectoralBaseline, nucleusListElectionOverview); cada um faz find + delete por documento nas três collections election*. 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.
  • loadNucleusListElectionOverview tem pelo menos um teste int com fixture TSE que cobre gap agregado + distribuição de tendência + agregado de conversão (weightedRate, distribution reduto/consolidado/oportunidade) no mesmo path; extensão pós-A5: agregados leverage / flipOpportunity + spy de query count federal (guardrail contra regressão N+1 até A7 F5); extensão pós-A5-2: agregado classification.distribution (defesa/indecisa/ataque/perdida) + caso classification: null sem válidos; extensão pós-A5-mobilização: agregado mobilization (Σ brancos/nulos/abstenções + % ponderado) espelhando aggregateMobilizationOverview.
  • Tipos de distribuição de tendência têm uma única fonte (VoteTrendDistribution); loaders e VMs importam de electionInsights.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, computeVoteTrend e loadNucleusListElectionOverview já 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.message se o card acima já exibir formatVoteTrendSeriesCompact.
  • 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): VoteTrendDistribution canônico; NucleusTrendOverviewAggregate vira 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 design Baseline-Eleitoral-2022); NucleusInsights recebe trend: VoteTrendResult por prop e mostra só o alerta com trend.message (sem repetir a série). computeVoteTrend roda uma vez em NucleusActiveTab.
  • Fase 2: fixture int separada ou estender tseFixtures.ts? Recomendação: estender tests/helpers/tseFixtures.ts com série 2014/2018/2022 mínima para dois núcleos sobrepostos na geografia union; reutilizar padrão de electionResultsImport.int.spec.ts.
  • Fase 3: remover ratio de VoteTrendResult? Recomendação: sim, se nenhum caller de produção ler — ajustar testes para assertar só status/message ou testar ratio via 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; passar trend opcional para NucleusElectoralBaseline e NucleusInsights.
  • NucleusElectoralBaseline: aceitar trend?: VoteTrendResult | null — se presente, não chamar computeVoteTrend internamente.
  • NucleusInsights: aceitar trend por prop; remover segunda cópia da série no AlertDescription (manter só mensagem classificada).
  • Testes: ajustar nucleusElectoralBaselineUi.unit.spec.ts se 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 leverage comparável e flipOpportunity com status: 'opportunity'.
  • Asserts em loadNucleusListElectionOverview: leverage.weightedPercent / unconvertedCount e flipOpportunity.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.distribution sobre o conjunto filtrado com geografia; caso negativo com tallies sem votosValidos → classification: null e overview sem linha Classificação:.
  • Pós-A5-mobilização: em tests/int/nucleusListElectionOverview.int.spec.ts, assert result.mobilization para [salvadorZ1, salvadorZ2] com totais derivados de TSE_FIXTURE_ZONE_EXPECTED + brancos/nulos do fixture (detalhe_votacao_munzona_fixture.csv cargo federal T1); espelhar aggregateMobilizationOverview (não duplicar fórmula inline). Caso aptos: 0 ou geografia vazia → mobilization: null (vazio já coberto).

Fase 3 — Tipos e API

  • Substituir NucleusTrendOverviewAggregate por import de VoteTrendDistribution nos VMs (nucleusListOverviewViewModels.ts, nucleusElectoralBaseline.ts exports).
  • Avaliar remoção de VoteTrendResult.ratio ou torná-lo @internal documentado; atualizar electionInsights.unit.spec.ts.

Fase 4 — Helpers de teste eleitoral

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 de electionResultsImport.int.spec.ts.

Não escopo

  • Agregar ranking federal no detalhe → escala-dry-pos-a4.md (A7 F1).
  • Filtro geográfico por cityCode TSE → A7 F2.
  • Query duplicada /campanha/nucleos lista + overview → escala-dry-pos-e1.md (E6 F1).
  • Re-resolve de geografia no overview baseline → E6 F2.
  • formatElectionNumber nos 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 E2
  • docs/plans/escala-dry-pos-a4.md / escala-dry-pos-e1.md — débitos adjacentes (A7 / E6)
  • src/components/campaign/NucleusActiveTab.tsx — render duplo baseline + insights
  • src/components/campaign/NucleusElectoralBaseline.tsx / NucleusInsights.tsx
  • docs/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, aggregateMobilizationOverview
  • src/utilities/nucleusElectoralBaseline.ts — loadNucleusListElectionOverview, getNucleusElectoralBaseline
  • src/utilities/nucleusListOverviewPageData.ts
  • tests/unit/electionInsights.unit.spec.ts / tests/unit/nucleusElectoralBaselineUi.unit.spec.ts
  • tests/helpers/tseFixtures.ts / tests/int/electionResultsImport.int.spec.ts / tests/int/nucleusListElectionOverview.int.spec.ts
  • tests/helpers/electionTestHelpers.ts — alvo da Fase 4 (a criar)
  • AGENTS.md — naming, overrideAccess: false, seed guards