diff --git a/docs/plans/escala-dry-pos-a4.md b/docs/plans/escala-dry-pos-a4.md index deaaf4ba..68d10646 100644 --- a/docs/plans/escala-dry-pos-a4.md +++ b/docs/plans/escala-dry-pos-a4.md @@ -1,7 +1,7 @@ # Escala e DRY pós-A4 (baseline TSE no produto) Status: Fase 1 implementada (Fases 2–5 pendentes; merge pendente) -Atualizado em: 2026-07-19 (Fase 5 lista flip/leverage + extensão F3 gap registradas via `capture-review-debts` pós-A5 `/simplify`) +Atualizado em: 2026-07-19 (Fase 5 lista flip/leverage + extensão F3 gap registradas via `capture-review-debts` pós-A5 `/simplify`; **pós-A5-2 classificação territorial** — absorções no mesmo plano via `capture-review-debts`) Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha A, item A7) Responsável: — @@ -22,7 +22,9 @@ Os revisores (performance / reuse / quality) marcaram como **importantes e maior **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. -**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), `BahiaMap` `setStyle` incremental → **B6** ([escala-dry-pos-b3.md](escala-dry-pos-b3.md)), factory geometrias mun/TI → **B5 F3** ([escala-dry-pos-b2.md](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). +**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. + +**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), `BahiaMap` `setStyle` incremental → **B6** ([escala-dry-pos-b3.md](escala-dry-pos-b3.md)), factory geometrias mun/TI → **B5 F3** ([escala-dry-pos-b2.md](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**. ## Objetivos diff --git a/docs/plans/escala-dry-pos-e2.md b/docs/plans/escala-dry-pos-e2.md index c36bdb17..22e50864 100644 --- a/docs/plans/escala-dry-pos-e2.md +++ b/docs/plans/escala-dry-pos-e2.md @@ -1,7 +1,7 @@ # 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 pendente; Fases 1, 3 e 4 pendentes -Atualizado em: 2026-07-19 (extensão int leverage/flip registrada via `capture-review-debts` pós-A5 `/simplify`) +Atualizado em: 2026-07-19 (extensão int leverage/flip/**classification** registrada via `capture-review-debts` pós-A5 `/simplify` + pós-A5-2) Item do roadmap: [docs/roadmap.md](../roadmap.md) (Trilha E, item E7) Responsável: — @@ -25,12 +25,14 @@ Os revisores (performance / reuse / quality) marcaram como **importantes e maior **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`). + **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 `Alert`s em `NucleusInsights` até mais insights A5; **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). +- `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. - 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. @@ -76,11 +78,12 @@ flowchart TD - `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 — pendente) +#### Extensão Fase 2 (pós-A5 / pós-A5-2 — pendente) - 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:`. ### Fase 3 — Tipos e API diff --git a/docs/plans/insight-classificacao-territorial.md b/docs/plans/insight-classificacao-territorial.md index 1393d858..fdbd8a7b 100644 --- a/docs/plans/insight-classificacao-territorial.md +++ b/docs/plans/insight-classificacao-territorial.md @@ -1,71 +1,78 @@ # Insight: classificação territorial (defesa/ataque/indecisa/perdida) -Status: rascunho -Atualizado em: 2026-07-17 -Item do roadmap: [docs/roadmap.md](../roadmap.md) (seção "Campanha → Próximos ciclos") +Status: entregue (slice A5-2, 2026-07-19) +Atualizado em: 2026-07-19 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Janela 3, A5 — segundo dos cinco insights) Responsável: — +**Revisão 2026-07-19:** as-built espelha A5-1 conversão — Alert no stack `NucleusInsights.tsx` (sem `NucleusTerritorialClassification.tsx`); overview via `loadNucleusListElectionOverview` + linha em `NucleusListOverview`; limiares 35%/20%/10% versionados em `electionInsights.ts` _(assumido — validar com produto)_; classificação sobre todos os núcleos filtrados com geografia + válidos > 0 (não exige estimativa confirmada). + ## Referência visual (UX Pilot) -Design: [`Baseline-Eleitoral-2022.png`](../design-refs/latest/Baseline-Eleitoral-2022.png) — card "Insights do território", linha "Território de defesa · Base sólida — prioridade: manter engajamento" com chip de classificação à direita (`Defesa` em verde). Os quatro estados usam pares de badge do tema `campaign`: defesa = verde, ataque = contorno vermelho, indecisa = âmbar, perdida = cinza. Implementar como um card do stack `NucleusInsights.tsx` ([baseline-eleitoral-tse.md](baseline-eleitoral-tse.md)), ajustando a paleta antiga do HTML/PNG para os tokens claros. +Design: [`Baseline-Eleitoral-2022.png`](../design-refs/latest/Baseline-Eleitoral-2022.png) — card "Insights do território", linha "Território de defesa · Base sólida — prioridade: manter engajamento" com chip de classificação à direita (`Defesa` em verde). Os quatro estados usam pares de badge do tema `campaign`: defesa = verde, ataque = contorno vermelho, indecisa = âmbar, perdida = cinza. Implementar como um card do stack `NucleusInsights.tsx` ([baseline-eleitoral-tse.md](baseline-eleitoral-tse.md)), com os tokens claros do tema `campaign` em vez da paleta antiga do HTML/PNG. ## Contexto -A literatura de territorialização eleitoral (Politipédia AVM, OPUS, Seja Eleito) classifica cada território em quatro zonas operacionais — **defesa** (voto histórico favorável, meta: manter), **ataque** (desfavorável mas relevante, meta: virar/reduzir margem), **indecisa** (pulverizada, baixa rejeição, meta: consolidar) e **perdida** (desfavorável e elástica, meta: minimizar perda) — para alocar esforço de forma proporcional ao retorno. Hoje o `/campanha` não classifica núcleos; com o baseline TSE 2022 (ver [baseline-eleitoral-tse.md](baseline-eleitoral-tse.md)) podemos derivar uma primeira classificação a partir do histórico. +A literatura de territorialização eleitoral (Politipédia AVM, OPUS, Seja Eleito) classifica cada território em quatro zonas operacionais — **defesa** (voto histórico favorável, meta: manter), **ataque** (desfavorável mas relevante, meta: virar/reduzir margem), **indecisa** (pulverizada, meta: consolidar) e **perdida** (baixa base histórica, meta: minimizar perda) — para alocar esforço de forma proporcional ao retorno. Com o baseline TSE 2022 (ver [baseline-eleitoral-tse.md](baseline-eleitoral-tse.md)) derivamos a primeira classificação a partir de `sollaVotes2022 / votosValidosFederal2022`. ## Objetivos -- Computar `sollaPercentValid = sollaVotes2022 / votosValidosFederal2022` por núcleo (soma sobre cidades∩zonas). -- Classificar o núcleo em defesa/ataque/indecisa/perdida por `sollaPercentValid` vs. limiares configuráveis. -- Exibir badge no detalhe do núcleo e distribuição por classe no overview da lista (sobre o conjunto filtrado). -- (Opcional, quando o domínio existir) refinar a classe cruzando com pesquisa de intenção de voto atual. +- Computar `percentValid = sollaVotes2022 / votosValidosFederal2022` por núcleo (soma sobre cidades∩zonas). +- Classificar o núcleo em defesa/ataque/indecisa/perdida por `percentValid` vs. limiares configuráveis. +- Exibir Alert com badge no detalhe do núcleo e distribuição por classe no overview da lista (sobre o conjunto filtrado com geografia resolvível). ## Decisões travadas -- **Leitura derivada** — sem escrita, sem `Consent`, sem migration. -- **Limiares versionados** em `src/lib/electionInsights.ts` (constantes); decisão de produto por limiar. A classe é **sugestão automática**, não rótulo definitivo — coordenador pode discordar. -- **Reusa** `getNucleusElectoralBaseline` (Solla + válidos federal) do plano baseline. +- **Leitura derivada** — sem escrita, sem `Consent`, sem migration. Reusa `getNucleusElectoralBaseline` / `loadNucleusListElectionOverview`. +- **Limiares versionados** em `src/lib/electionInsights.ts`: `TERRITORIAL_DEFESA_MIN = 0.35`, `TERRITORIAL_INDECISA_MIN = 0.20`, `TERRITORIAL_ATAQUE_MIN = 0.10` _(assumido — validar com produto)_. +- A classe é **sugestão automática**, não rótulo definitivo — coordenador pode discordar. +- **Sem rejeição** por geografia (domínio inexistente); refinamento futuro com pesquisas. +- **`lideranca` vê** o insight (mesmo stack da Visão geral). +- **UI:** `Alert` em `NucleusInsights.tsx` (`data-insight="territorial-class"`), não componente separado; chip de classe no Alert (design-ref). +- **Overview:** distribuição sobre todos os núcleos filtrados com geografia + `validos > 0` (não exige estimativa confirmada). ## Questões em aberto -- Limiares exatos por classe — definir com produto (referência: alta % válidos → defesa; baixa + relevante → ataque; pulverizada → indecisa; baixa + alta rejeição → perdida). -- Como incorporar rejeição (não temos dado de rejeição por geografia hoje) — adiar até domínio de pesquisas. -- `lideranca` vê a classe do próprio núcleo? Recomendação: sim. +- Limiares exatos — **assumidos 35%/20%/10%** até validação de produto. +- Refinar com pesquisa de intenção/rejeição — adiado. -## Abordagem proposta +## Abordagem (as-built) ```mermaid flowchart LR - Base["getNucleusElectoralBaseline
sollaVotes + votosValidosFederal"] - Calc["computeTerritorialClass(sollaPercentValid)
classe + confiança"] - Detail["Detalhe: badge da classe
+ % válidos de Solla"] - Overview["Overview: distribuição
defesa/ataque/indecisa/perdida
sobre o filtro"] + Base["getNucleusElectoralBaseline
candidate.votes + electorate.validos"] + Calc["computeTerritorialClass(...)"] + Detail["NucleusInsights Alert + Badge"] + Overview["loadNucleusListElectionOverview
+ NucleusListOverview"] Base --> Calc Calc --> Detail Calc --> Overview ``` -- **Helper** `src/lib/electionInsights.ts`: `computeTerritorialClass(sollaVotes, votosValidosFederal)` → `{ percentValid, class: 'defesa'|'ataque'|'indecisa'|'perdida'|'semBaseline' }`. -- **Componente** `src/components/campaign/NucleusTerritorialClassification.tsx` (server). Overview: bloco com contagem por classe. -- **Teste int** cenários limítrofes e `votosValidosFederal=0`. +- **Helper** [`src/lib/electionInsights.ts`](../../src/lib/electionInsights.ts): `computeTerritorialClass`, `aggregateTerritorialClass`, `territorialClassBadgeVariant`, `territorialClassLabel`. +- **Detalhe** [`src/components/campaign/NucleusInsights.tsx`](../../src/components/campaign/NucleusInsights.tsx). +- **Overview** [`src/utilities/nucleusElectoralBaseline.ts`](../../src/utilities/nucleusElectoralBaseline.ts) (`classification.distribution`) + [`NucleusListOverview.tsx`](../../src/components/campaign/NucleusListOverview.tsx). +- **Testes** unitários em `tests/unit/electionInsights.unit.spec.ts`; UI em `tests/unit/nucleusElectoralBaselineUi.unit.spec.ts` e `tests/unit/campaignNucleusListOverview.unit.spec.ts`. -## Arquivos a criar/alterar +**Já resolvido no simplify / capture-review-debts (não reabrir):** `TERRITORIAL_CLASS_UI` + accessors; `sumFederalTallyForGeography` único; reuso de tallies union na conversão (sem 2º fetch); filtro `isComparableTerritorialClass` no loader; `TerritorialClassIcon`; branches `semBaseline` unificados em `computeTerritorialClass`. -- Criar: `src/components/campaign/NucleusTerritorialClassification.tsx`. -- Alterar: `src/lib/electionInsights.ts`, `nucleos/[slug]/page.tsx` + `nucleusDetailPageData.ts`, overview da lista. +**Débitos maiores (planos existentes):** batch federal flip/leverage na lista → [escala-dry-pos-a4.md](escala-dry-pos-a4.md) **A7 F5**; int `classification` + leverage/flip no loader → [escala-dry-pos-e2.md](escala-dry-pos-e2.md) **E7 F2 ext.**; fetch único 2022 lista+coroplético → **A7 F4**; DRY linha de distribuição no overview → [escala-dry-pos-e1.md](escala-dry-pos-e1.md) **E6 F3**; shell `NucleusInsightAlert` → **E7 F1** (adiado até estabilizar stack A5). ## Dependências - [baseline-eleitoral-tse.md](baseline-eleitoral-tse.md) — **única dependência dura**. -- [zonas-por-municipio.md](zonas-por-municipio.md) — dependência suave herdada do baseline (melhora a qualidade de `tseZones`; ver revisão 2026-07-17 no plano baseline). +- [zonas-por-municipio.md](zonas-por-municipio.md) — dependência suave herdada do baseline. ## Não escopo -- Mapa/PostGIS com a classe pintada por território (roadmap linha 52). -- Pesquisa de intenção/rejeição (domínio inexistente; refinamento futuro). +- Mapa/PostGIS com a classe pintada por território (B3). +- Pesquisa de intenção/rejeição; override manual da classe; demais insights A5. ## Referências - Politipédia AVM — "Territorialização eleitoral em campanha" - [baseline-eleitoral-tse.md](baseline-eleitoral-tse.md) +- [insight-taxa-conversao.md](insight-taxa-conversao.md) — padrão as-built A5-1 +- [escala-dry-pos-a4.md](escala-dry-pos-a4.md) — A7 F5 (batch flip/leverage) +- [escala-dry-pos-e2.md](escala-dry-pos-e2.md) — E7 F2 ext. (int loader) - AGENTS.md diff --git a/docs/roadmap.md b/docs/roadmap.md index bbd8e7a6..74ce8402 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -65,7 +65,7 @@ MVP de território + reporte implementado e enviado (ondas 1–8 + refactors de ### Ciclo 2+ — A4 implementado e mesclado em `main` (2026-07-18) - **A4 Baseline no produto + Gap vs 2022** — agregação `getNucleusElectoralBaseline` / `loadNucleusBaseline2022Overview` + `computeGapVs2022`; bloco "Baseline eleitoral 2022" e insight Gap na aba Visão geral do núcleo; card "Baseline 2022" no overview da lista filtrada. Chapa tipada via `BASELINE_TICKET_2022` (papéis `candidate` / `president` / `governor`). Sem migration, sem Consent (dado público TSE). Três passagens `/simplify` no PR; débitos maiores → A7. [Plano](plans/baseline-eleitoral-tse.md). -- **A7 Escala e DRY pós-A4 — Fase 1 entregue (2026-07-19; merge pendente)** — agregação drizzle do ranking federal no detalhe (`loadFederalCandidateTotalsAggregated`); Fases 2–5 pendentes (`cityCode` TSE, Alert `confirmed` + helpers gap + `Progress`, fetch único 2022 lista+coroplético B3, batch federal flip/leverage na lista pós-A5). Passagens `/simplify` + `capture-review-debts` pós-B3 e pós-A5. [Plano](plans/escala-dry-pos-a4.md). +- **A7 Escala e DRY pós-A4 — Fase 1 entregue (2026-07-19; merge pendente)** — agregação drizzle do ranking federal no detalhe (`loadFederalCandidateTotalsAggregated`); Fases 2–5 pendentes (`cityCode` TSE, Alert `confirmed` + helpers gap + `Progress`, fetch único 2022 lista+coroplético B3, batch federal flip/leverage na lista pós-A5). Passagens `/simplify` + `capture-review-debts` pós-B3, pós-A5 e **pós-A5-2 classificação territorial** (tally único, reuso tallies conversão, ordem classificação antes do flip N+1). [Plano](plans/escala-dry-pos-a4.md). - **A8 Perfis do eleitorado IBGE registrado (2026-07-19)** — perfil médio/comum do território derivado de indicadores municipais do Censo IBGE (artefato estático + ponderação por `cities[]`), exibido na aba Eleitorado junto com `voterProfiles` manuais já existentes; sem migration, sem Consent. [Plano](plans/perfil-eleitorado-ibge.md). ### Ciclo 2+ — B2 fundação de geometrias do mapa (2026-07-18) @@ -92,7 +92,7 @@ Objetivo: equiparar e superar as planilhas "Mapa de projeção de votos Solla 20 - **E1 Metas em cenários + prioridade** — grupo `voteGoals` (`good`/`regular`/`minimum`) + `priority` (`alta|normal`) em `electoralNucleus` (migration compartilhada com E3); card "Metas 2026" no detalhe, filtro na lista, somas no overview B1 e no dashboard (meta estadual derivada). **Implementado e mesclado em `main` 2026-07-19** (migration `20260719_054522_add_nucleus_goals_strategy`; `/simplify` → E6). - **E6 Escala e DRY pós-E1+E3 registrado (2026-07-19)** — débitos do `/simplify` que não entraram no cleanup: query duplicada lista+overview, re-resolve de geografia no baseline do overview, aggregate SQL de metas/prioridade (padrão C6), DRY `formatElectionNumber`/`campaignPercentage`/`voteGoalScenarioLabels`, tipos do dashboard por papel. [Plano](plans/escala-dry-pos-e1.md). - **E2 Série histórica TSE 2014/2018 + tendência** — seed `pnpm db:seed:tse -- --year=2014|2018` (federal T1 BA only); `computeVoteTrend` ±10% (`increase|stable|decline|noBaseline`); série + badge no baseline card, insight e card de distribuição no overview; query candidate-only multi-ano (sem migration). **Implementado 2026-07-19.** Passagem `/simplify` no branch; débitos maiores → E7. -- **E7 Escala e DRY pós-E2 registrado (2026-07-19)** — débitos do `/simplify` que não entraram no cleanup: trend VM único no detalhe (sem série duplicada baseline+insights), testes int de `loadNucleusListElectionOverview` (gap + tendência **+ conversão A5-1**; **extensão pós-A5:** leverage/flip + spy N+1 federal), tipo único `VoteTrendDistribution`, helpers compartilhados de wipe eleitoral nos int tests. **Fase 2 int loader entregue (2026-07-19)** — `tests/int/nucleusListElectionOverview.int.spec.ts` + `seedMultiYearFederalCandidateFixture`; extensão F2 pós-A5 pendente; Fases 1, 3 e 4 pendentes. [Plano](plans/escala-dry-pos-e2.md). +- **E7 Escala e DRY pós-E2 registrado (2026-07-19)** — débitos do `/simplify` que não entraram no cleanup: trend VM único no detalhe (sem série duplicada baseline+insights), testes int de `loadNucleusListElectionOverview` (gap + tendência **+ conversão A5-1**; **extensão pós-A5:** leverage/flip + spy N+1 federal; **extensão pós-A5-2:** `classification.distribution`), tipo único `VoteTrendDistribution`, helpers compartilhados de wipe eleitoral nos int tests. **Fase 2 int loader entregue (2026-07-19)** — `tests/int/nucleusListElectionOverview.int.spec.ts` + `seedMultiYearFederalCandidateFixture`; extensões F2 pós-A5 e pós-A5-2 pendentes; Fases 1, 3 e 4 pendentes. [Plano](plans/escala-dry-pos-e2.md). - **E3 Dobradinhas + encaminhamentos manuais** — `dobradinhaNotes` e `nextSteps` staff-only na inteligência do núcleo (excluídos dos view models de `lideranca`); versão manual imediata que A6/C4 complementam depois. **Implementado e mesclado em `main` 2026-07-19** (mesma migration E1+E3; UI em `NucleusIntelligenceDialog` + cards staff na Visão geral). - **E4 Import único da planilha** — `pnpm db:seed:mapa` idempotente por slug, município canônico via `canonicalizeMunicipalityName`; lideranças/assessores da planilha entram como **texto staff-only, nunca `Contact`/`leadership`** (sem telefone/consent). - **E5 Salvador por bairro** — registrado como futuro (exigiria `votacao_secao` + DE-PARA local↔bairro versionado); a visão por zona já é coberta pelo baseline município×zona. diff --git a/src/components/campaign/NucleusInsights.tsx b/src/components/campaign/NucleusInsights.tsx index 7c804f0a..6ef004de 100644 --- a/src/components/campaign/NucleusInsights.tsx +++ b/src/components/campaign/NucleusInsights.tsx @@ -1,27 +1,36 @@ +import type { LucideIcon } from 'lucide-react' import { CheckCircle2Icon, Link2Icon, MegaphoneIcon, MinusIcon, PercentIcon, + ShieldIcon, TrendingDownIcon, TrendingUpIcon, TriangleAlertIcon, } from 'lucide-react' import { Alert, AlertDescription, AlertTitle } from '@/components/ui/Alert' +import { Badge } from '@/components/ui/Badge' import { computeConversionRate, computeGapVs2022, + computeTerritorialClass, computeTicketLeverage, computeVoteTrend, conversionRateAlertVariant, formatElectionNumber, formatVoteTrendSeries, isComparableConversionBand, + isComparableTerritorialClass, isComparableTicketLeverage, + territorialClassAlertVariant, + territorialClassBadgeVariant, + territorialClassLabel, ticketLeverageAlertVariant, voteTrendAlertVariant, + type ComparableTerritorialClass, } from '@/lib/electionInsights' import { cn } from '@/lib/utils' import type { NucleusElectoralBaselineViewModel } from '@/utilities/nucleusViewModels' @@ -29,6 +38,18 @@ import type { NucleusElectoralBaselineViewModel } from '@/utilities/nucleusViewM const confirmedInsightAlertClass = 'border-[color:var(--estimate-confirmed)] bg-[color:var(--estimate-confirmed)] text-[color:var(--estimate-confirmed-foreground)] *:data-[slot=alert-description]:text-[color:var(--estimate-confirmed-foreground)]' +const TERRITORIAL_CLASS_ICON: Record = { + defesa: ShieldIcon, + ataque: TriangleAlertIcon, + indecisa: MinusIcon, + perdida: TrendingDownIcon, +} + +const TerritorialClassIcon = ({ band }: { band: ComparableTerritorialClass }) => { + const Icon = TERRITORIAL_CLASS_ICON[band] + return