Product decision: strategic nucleus data will be entered manually via E1+E3 UI instead of a one-shot pnpm db:seed:mapa import.
11 KiB
Escala e DRY pós-E1+E3 (metas, prioridade, estratégia manual)
Status: registrado no roadmap (fases pendentes)
Atualizado em: 2026-07-19 (capture-review-debts pós-A5: labels conversão/tendência no overview → F3; E4 import planilha cortado — gatilho E6 F1 permanece volume manual de núcleos / lista lenta)
Item do roadmap: docs/roadmap.md (Trilha E, item E6)
Responsável: —
Contexto
O E1+E3 (mapa-projecao-municipios.md) entrega voteGoals (good/regular/minimum), priority (alta|normal), dobradinhaNotes e nextSteps em electoralNucleus (migration 20260719_054522_add_nucleus_goals_strategy), com UI no detalhe (NucleusVoteGoals), formulário, inteligência, filtro ?priority=alta, somas no overview B1 e KPI "Meta regular (soma)" no dashboard geral. Domínio centralizado em src/utilities/voteGoals.ts (getVoteGoalsOrderViolation, aggregateVoteGoals, sumVoteGoals, voteGoalProgressPercent); select compartilhado nucleusVoteGoalsSelect em nucleusViewModels.ts.
Duas passagens /simplify no mesmo branch já limparam o que cabia em cleanup: select fragment único, remoção de voteGoals do nucleusListSelect (lista só usa priority), remoção de priority do select do dashboard geral, aggregateVoteGoals em um passe no overview VM, labels E3 em nucleusUi.ts (client-safe), step={1} nos inputs de meta, testes esparsos de ordem.
Os revisores (performance / reuse / quality) marcaram como importantes e maiores que simplify os follow-ups abaixo. Sem registro, E2 e B3 (coroplético com metas) herdam o mesmo custo de I/O na lista de núcleos. (E4 import planilha cortado 2026-07-19.)
- Query duplicada na página
/campanha/nucleos.page.tsxchama em paraleloloadNucleusListPageData(paginado,depth: 1) eloadNucleusListOverviewData(pagination: false, conjunto filtrado inteiro). Overview precisa do agregado; a lista precisa da página — mas hoje são dois round-trips sobre linhas sobrepostas. Em filtros amplos, o fetch unpaginado do overview domina. - Geografia resolvida duas vezes para baseline no overview.
nucleusListOverviewPageData.tsmapeia comtoNucleusElectionGeographyInput;loadNucleusBaseline2022OverviewchamaresolveNucleusElectionGeographyde novo por núcleo (mais pesado:tseZonesForCity, interseções). - Agregados de metas só em memória.
buildNucleusListOverviewViewModelebuildGeneralDashboardViewModelcarregam todos os núcleos do escopo e somam comaggregateVoteGoals/sumVoteGoals. O precedente C6 (supporterListOverviewAggregate.ts,COUNT(*) FILTER) ainda não foi aplicado a metas/prioridade. - DRY de formatação e KPIs.
formatElectionNumberjá existe emelectionInsights.tse é usado emNucleusVoteGoals, masNucleusCard,NucleusList,NucleusListOvervieweCampaignDashboardainda instanciamIntl.NumberFormat('pt-BR')local.percentage(part, total)está duplicado emnucleusListOverviewViewModels.tsecampaignDashboardViewModels.ts. Rótulos de cenário (Bom/Regular/Mínimo) estão hardcoded em quatro superfícies. DashboardNucleusRecordenganoso para não-geral.toDashboardNucleusRecordsempre preenchevoteGoals/priority, mas selects de coordenador/liderança omitem esses campos → nulls/'normal'sintéticos.
Explicitamente fora (revisores pediram skip no simplify): NucleusPriorityBadge como componente (só 3 call sites), merge Zod parcial de voteGoals no patch (Payload beforeValidate já faz merge), mover voteGoals.ts para src/lib/ por layering, tipos VoteGoalsFields vs VoteGoalsViewModel (cerimônia aceitável), bug hasError em LeadershipPrimaryContactAction.tsx (pré-existente em main, C9).
Objetivos
- Abrir
/campanha/nucleoscom filtro amplo não escala com duas queries full-scan equivalentes; agregados de metas/prioridade preferem uma fonte de verdade (loader compartilhado ou SQL aggregate). - Card "Baseline 2022" no overview não re-resolve geografia que o mapper de núcleos já calculou.
- Formatação de votos/metas e percentuais de KPI reutilizam helpers únicos (
formatElectionNumber,campaignPercentage,voteGoalScenarioLabels). - Guardrails: Fases 1–2 podem ser sem migration (loader/SQL read-only); sem Consent (metas não são PII). Access continua
overrideAccess: false+ selects/VMs por papel. Valores enumalta|normalpermanecem dados em português (AGENTS.md).
Decisões travadas
- Um item E6, quatro fases ordenadas. Mesmo racional de A7/C8/C10: um ID no roadmap, PRs por fase. Ordem: loader/aggregate (custo real) → cache de geografia baseline → DRY UI/helpers → tipos do dashboard por papel (qualidade).
- Dependência dura de E1+E3 mergeados em
main. Só faz sentido com campos e UI já no produto; não reabre escopo de E2/E4. - Fase 1 não remove o painel overview nem o KPI do dashboard. Otimiza como os dados chegam; paridade com a planilha permanece.
- Cortável se o número de núcleos ativos permanecer pequeno (dezenas). Vira não-cortável quando B3 coroplético ou E4 import multiplicarem o conjunto filtrado.
- i18n e naming (AGENTS.md): identificadores em inglês (
loadNucleusListPageBundle,nucleusListOverviewAggregate,voteGoalScenarioLabels,formatNullableElectionNumber); strings visíveis e valores enumalta|normalem pt-BR.
Questões em aberto
- Fase 1: unificar loaders vs SQL aggregate puro? Recomendação: espelhar C6 —
nucleusListOverviewAggregate.tscomSUM/COUNT(*) FILTERespelhandobuildNucleusListWhere+ access; lista paginada mantém query própria; overview consome só aggregate + preview de updates (não re-lê todos os núcleos para somar metas). Unificar lista+overview num únicofindunpaginado só se a paginação puder ser in-memory sem regressão dedepth/coordinadores — validar com teste int. - Fase 2: passar geografia resolvida ou memoizar no contexto da request? Recomendação: estender
NucleusListOverviewNucleusRecordcom o objeto já resolvido (ou mapaid → NucleusElectionGeography) e fazerloadNucleusBaseline2022Overviewaceitar entrada pré-resolvida; fallback para resolve atual em outros call sites. - Fase 4: tipos separados por role no dashboard? Recomendação:
DashboardGeralNucleusRecordcomvoteGoals+DashboardScopedNucleusRecordsem —buildScopedDashboardViewModeldeixa de receber campos fantasma.
Abordagem proposta
flowchart TD
E1["E1+E3 Metas/prioridade/E3 ✓"] --> F1
F1["Fase 1 — Aggregate SQL ou loader único<br/>(metas + prioritários + KPIs)"]
F1 --> F2["Fase 2 — Geografia baseline sem re-resolve"]
F1 --> F3["Fase 3 — formatElectionNumber + percentage + scenario labels"]
F1 --> F4["Fase 4 — Dashboard records por role"]
F1 -.menos I/O.-> E4["E4 Import planilha"]
F1 -.menos I/O.-> B3["B3 Coroplético com metas"]
F2 -.mesmo path.-> A7["A7 Baseline loader"]
Fase 1 — Aggregate / loader da lista de núcleos
- Novo
src/utilities/nucleusListOverviewAggregate.ts(ou extensão denucleusListOverviewPageData.ts): uma query drizzle/SQL comSUM(vote_goals_regular)etc. eCOUNT(*) FILTER (WHERE priority = 'alta'), espelhandobuildNucleusListWhere+ constraint de access do usuário (mesmo padrão desupporterListOverviewAggregate.ts). - Dashboard geral: reutilizar o mesmo aggregate para
regularVoteGoalTotalem vez desumVoteGoalssobre todos os docs — ou query dedicadastatus = ativose o escopo for global. - Opcional na mesma fase: fundir
loadNucleusListOverviewDatacom parte do list loader se o teste de perf mostrar ganho claro; caso contrário, manter duas queries mas overview não materializa array completo só para somar metas. - Testes: unit do SQL/where mirror; int com filtros
priority=alta+ múltiplos núcleos assertando totais.
Fase 2 — Cache de geografia no overview → baseline
- Em
nucleusListOverviewPageData.ts, persistir geografia resolvida no record passado aloadNucleusBaseline2022Overview. - Ajustar
nucleusElectoralBaseline.tspara aceitar geografia pré-computada (overload ou tipoPreresolvedNucleusGeography). - Critério: uma passagem
resolveNucleusElectionGeographypor núcleo no path do overview.
Fase 3 — DRY de formatação e rótulos
src/lib/electionInsights.ts:formatNullableElectionNumber(value, empty = '—').- Substituir
Intl.NumberFormatlocal emNucleusCard,NucleusList,NucleusListOverview,CampaignDashboard. src/utilities/voteGoals.ts:voteGoalScenarioLabels(good/regular/minimum→ Bom/Regular/Mínimo); usar emNucleusForm,NucleusVoteGoals,NucleusListOverview.electionInsights.ts:conversionBandLabel(reduto/consolidado/oportunidade) e reuso devoteTrendStatusLabelnas linhas de overview eleitoral emNucleusListOverview(substituir strings hardcoded "reduto", "aumento", etc. — absorvido do triage pós-A5).- Extrair
campaignPercentage(ex.src/utilities/campaignMath.ts) usado por overview e dashboard. - Opcional:
NucleusPriorityBadgese os três call sites incomodarem no próximo touch.
Fase 4 — Dashboard nucleus records por papel
- Separar tipos/loader: geral carrega
voteGoals; coordenador/liderança não. toDashboardNucleusRecordvira duas funções ou genérico com select tipado.
Migration: nenhuma nas quatro fases (só leitura/agregação e tipos).
Dependências
- Dura: E1 Metas + prioridade e E3 campos manuais — merge em
maincom migration20260719_054522. - Suave: E4 import planilha e B3 coroplético — amplificam custo se Fase 1 atrasar; A7 compartilha loader de baseline com Fase 2.
- Reusa:
voteGoals.ts,nucleusVoteGoalsSelect,buildNucleusListWhere,supporterListOverviewAggregate.ts(padrão),formatElectionNumber,loadNucleusBaseline2022Overview.
Não escopo
- E2 série 2014/2018 → permanece no mapa-projecao-municipios.md.
- E4 seed da planilha → idem.
- Dobradinha estruturada A6 / import nominal de pessoas da planilha.
- Renomear enum
alta→high(migration + backfill). - Zod merge de
voteGoalsparcial no patch. LeadershipPrimaryContactActionhasError(C9).
Referências
docs/roadmap.md(Trilha E, E6; E1–E5)docs/plans/mapa-projecao-municipios.md— plano pai E1–E5docs/plans/escala-dry-pos-a4.md/escala-dry-pos-c6.md— precedente pós-/simplifysrc/app/(campaign)/campanha/(app)/nucleos/page.tsx— parallel loaderssrc/utilities/nucleusPageData.ts/nucleusListOverviewPageData.tssrc/utilities/nucleusListOverviewViewModels.ts/campaignDashboardViewModels.tssrc/utilities/voteGoals.ts/nucleusViewModels.tssrc/utilities/nucleusElectoralBaseline.ts—resolveNucleusElectionGeography,loadNucleusBaseline2022Overviewsrc/utilities/supporterListOverviewAggregate.ts— padrão SQL aggregatesrc/lib/electionInsights.ts—formatElectionNumber- AGENTS.md — naming,
overrideAccess: false, enum values em português como dado