From 520613f862fbe8e2272fde8803a3c5a66131d3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Sat, 25 Jul 2026 22:38:28 -0300 Subject: [PATCH] E10+ registra os debitos do /simplify do E10 como fill-in de engenharia MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quatro fases num plano so (escala-dry-pos-e10): leitura unica da tabela na lista, memo de processo dos potenciais por municipio, TooltipProvider unico no layout e multi-filtros do mobile saindo das definitions. A F1 nao e achado novo: o A11 ja tinha adiado a "hidratacao unica do sort derivado" com gatilho "3o path derivado no loader" — com classe o loader chegou ao 4o (votos, deficit, frescor, classe), entao o adiado do A11 fica riscado e apontando para o item novo. Reforcada tambem a nota de gatilho do disclosure em conta-da-cadeira.md (a classe entrou acima da dl, nao como 5a metrica). Descartados no triage, com racional no plano: formatador de percentual proprio do TerritoryListColumns, os tres caches do classificador e o ruido do knip com payload.config.ts (ja ledgerado P3, e a evidencia da sessao contradiz). --- docs/plans/conta-da-cadeira.md | 2 +- docs/plans/escala-dry-pos-e10.md | 97 +++++++++++++++++++++++++++ docs/plans/ranking-votos-municipio.md | 2 +- docs/roadmap.md | 3 +- 4 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 docs/plans/escala-dry-pos-e10.md diff --git a/docs/plans/conta-da-cadeira.md b/docs/plans/conta-da-cadeira.md index 9369a551..5c0a1d5f 100644 --- a/docs/plans/conta-da-cadeira.md +++ b/docs/plans/conta-da-cadeira.md @@ -147,7 +147,7 @@ Componentes: - **Delta semanal persistido.** Revisitar quando C12 entregar trajetória (gatilho: primeira semana com 2 snapshots). - **Meta por cenário (pessimista/central/otimista) × cobertura tripla.** Resolvido nesta sessão como escopo do próprio E8 (não mais adiado): cobertura é computada para os três cenários no servidor, seletor de cenário fica no cliente (`MunicipalityEstimateScenarioProvider`, default `central`). -- **Modelo de disclosure do grid de diagnóstico (`GoalAccountMetric`) ainda não "assentado" para mais métricas.** O `/impeccable critique` pós-hardening (26/40; ver Follow-up #3) levantou, em "Questions to Consider", se hover/foco continua o mecanismo certo de explicação conforme **E9/E10/B13/E11/E12/E13/E14** adicionam métricas a este mesmo card, e se "Captura"/"Share intracampo" deveriam virar um único número com toggle de denominador em vez de dois números cada um explicando o outro por tooltip. Nenhum achado adicional confirmado nesta sessão (é pergunta de design, não bug) — **não registrado como item**; gatilho: revisitar quando a próxima entrega de E9–E14 for adicionar uma 5ª métrica a esta `dl`/grid (nesse ponto, decidir layout/idioma de disclosure antes de replicar `GoalAccountMetric` de novo). Largura tablet/foldable intermediária também não testada — mesmo gatilho. +- **Modelo de disclosure do grid de diagnóstico (`GoalAccountMetric`) ainda não "assentado" para mais métricas.** O `/impeccable critique` pós-hardening (26/40; ver Follow-up #3) levantou, em "Questions to Consider", se hover/foco continua o mecanismo certo de explicação conforme **E9/E10/B13/E11/E12/E13/E14** adicionam métricas a este mesmo card, e se "Captura"/"Share intracampo" deveriam virar um único número com toggle de denominador em vez de dois números cada um explicando o outro por tooltip. Nenhum achado adicional confirmado nesta sessão (é pergunta de design, não bug) — **não registrado como item**; gatilho: revisitar quando a próxima entrega de E9–E14 for adicionar uma 5ª métrica a esta `dl`/grid (nesse ponto, decidir layout/idioma de disclosure antes de replicar `GoalAccountMetric` de novo). Largura tablet/foldable intermediária também não testada — mesmo gatilho. **Reforço 2026-07-25 (E10):** a classe territorial entrou neste card como uma linha **acima** da `dl` (`TerritorialClassRow`), não como 5ª métrica dentro dela — o gatilho continua **não** disparado, mas o `/simplify` do E10 registrou que essa linha já reimplementa o estilo do trigger de `GoalAccountMetric` (o comportamento de toque/dismiss é compartilhado via `CampaignHoverTooltip`, então só o botão duplica). Quando o gatilho disparar, decidir o idioma antes de replicar o componente pela terceira vez — ver "Adiado com gatilho" de [escala-dry-pos-e10.md](escala-dry-pos-e10.md). ## Referências diff --git a/docs/plans/escala-dry-pos-e10.md b/docs/plans/escala-dry-pos-e10.md new file mode 100644 index 00000000..de08b340 --- /dev/null +++ b/docs/plans/escala-dry-pos-e10.md @@ -0,0 +1,97 @@ +# Escala e DRY pós-E10 (classificação territorial relativa) + +Status: rascunho +Atualizado em: 2026-07-25 +Item do roadmap: [docs/roadmap.md](../roadmap.md) (Fill-ins abertos, **E10+**, fill-in de engenharia) +Impeccable: A — N/A (sem superfície UI nova; as quatro fases preservam o comportamento visível de E10) +Appetite: ~0,75–1 dia eng; quatro fases independentes, nenhuma com migration, collection ou `Consent` +Responsável: — + +## Dados → decisão → apresentação + +- **Vou apresentar dados?** Não (N/A) — o lote é custo de leitura/render de métricas já entregues (A11, E8, E9, E10); nenhuma métrica, série ou escala nova. +- **Anti-goals de dado:** N/A. + +## Contexto + +**E10** ([classificacao-territorial-relativa.md](classificacao-territorial-relativa.md), entregue 2026-07-25) classificou os 435 municípios em Reduto/Expansão/Manutenção/Marginal/Sem base sobre o artefato TSE commitado, com coluna+sort+filtro na lista, badge no card do E8 e na capa do dossiê, mais o seam genérico `cellTooltip` na `CampaignTable`. O `/simplify` da entrega (três revisores em paralelo — quality / performance / reuse, 2026-07-25) aplicou o cleanup barato na hora e deixou quatro achados **maiores que cleanup**, registrados aqui. + +O achado mais importante não é novo: o A11 já havia registrado, em "Adiado com gatilho", a _hidratação única do sort derivado_ com o gatilho **"3º path derivado no loader"**. E10 é o **quarto** (`votos` A11, `deficit` e `frescor` E9, `classe` E10) — **o gatilho disparou**, e é por isso que este lote existe em vez de mais uma linha adiada. + +**Já resolvido no simplify (não reabrir):** `campoVotes` (somado em todo rollup estadual, sem leitor) e `classification.fieldHeadroom` (duplicava o fator `field`) removidos; `formatTerritorialClassWhy` virou de fato a fonte única do `slice(0, 2)` **e** do fallback "sem série do TSE", eliminando o sentinela de string vazia (`why || TERRITORIAL_CLASS_NO_DATA`) nas duas superfícies de detalhe; a classe territorial virou um componente só com prop `layout: 'table' | 'card'`, como `VotePositionReadout`/`SignalAgeReadout` vizinhos; `parseExhaustiveEnumParam` em `campaignListUrl.ts` absorveu `trend` e `class` (o guard `includes` era morto — `allParamValues` já deduplica por `Set`); o bloco central passou a caminhar o `computeVoteRankByYear` memoizado do A11 em vez de reordenar os 435 slugs; o filtro de classe entrou no memo de `facetRows` (era aplicado 3× a arrays que costumam ser o mesmo objeto); `Intl.NumberFormat` hoisted; e dois comentários que **afirmavam o contrário do código** foram corrigidos (o de `CampaignCellTooltip` alegava que um elemento criado no servidor não sobreviveria ao clone de ref — `MunicipalityBaselineCard` o desmente desde o E8; e a regra do `sem_base`, que o `AGENTS.md` dava como "sempre travessão" enquanto card e dossiê entregam pílula). + +## Objetivos + +- Uma leitura da tabela `municipality` por request na lista de municípios, em vez de duas com o mesmo `where`. +- O que é puro sobre o artefato commitado é memoizado **por processo**, não recomputado por request. +- Um `TooltipProvider` por árvore `/campanha`, para o agrupamento de delay do Radix voltar a funcionar. +- Os quatro filtros multi do mobile saem da mesma definição de dado que os single-select vizinhos já usam. +- Guardrails: sem migration, sem collection, sem `Consent`, sem server action; contrato de URL da lista (`?sort=`/`?class=`/`?trend=`/…) e comportamento visível de E10/E9/A11 inalterados; `overrideAccess: false` preservado em toda leitura com `user`. + +## Decisões travadas + +- **F1 resolve o gatilho do A11 em vez de criar helper genérico de hidratação.** O A11 pediu "hidratação única do sort derivado"; o caminho barato é fazer `loadMunicipalityScope` aceitar docs já carregados quando `isPagedByPayload === false`, não desenhar uma camada de repositório. Fonte: `/simplify` E10 (2026-07-25) + gatilho do [ranking-votos-municipio.md](ranking-votos-municipio.md). **Rejeitado:** materializar a classe/deficit em coluna do Postgres (volta a ser schema e desfaz a pureza que é o ponto do E10); cache cross-request da lista (dado vivo de 2026 — proibido pela escada de cache do `engineering-standards.mdc`). +- **F2 separa o que é puro do que depende do banco.** `computeAllMunicipalityPotentials()` é puro sobre o artefato; só `deriveSuggestedGoalsByScenario` depende de `campaignGoals`. O memo de processo cobre o primeiro e o `cache()` de request continua cobrindo o segundo. **Rejeitado:** memoizar `computeStatewideSuggestedGoals` inteiro por processo (erraria na hora em que o CG editasse a meta estadual no admin). +- **F3 não muda o idioma de disclosure, só onde o Provider mora.** Hoist do `TooltipProvider` para o layout de `(app)`; `CampaignHoverTooltip` mantém estado controlado, o `onPointerUp` de toque e o dismiss por `pointerdown` conquistados no critique do E8 — nada disso vive no Provider. **Rejeitado:** trocar Tooltip por Popover nas células (muda o gesto em toda a lista; é decisão de produto do B22/B23, não deste lote). +- **i18n e naming** seguem o AGENTS.md: identificadores em inglês (`loadMunicipalityScopeFromDocs`, `municipalityPotentialBySlug`, `mobileMultiFilterDefinitions`), strings visíveis em pt-BR. + +## Questões em aberto + +- **F3 deve virar um `TooltipProvider` global ou um por superfície de lista?** **Opções:** (a) um no layout de `(app)`; (b) um por página de lista; (c) manter como está. **Recomendação:** (a) — é o padrão do Radix, e o `delayDuration={300}` atual já é uniforme em todos os call sites, então não há política por superfície a preservar. +- **F4 vale a pena com quatro cópias, ou espera a quinta?** **Opções:** (a) fazer agora; (b) adiar até B21/B33 trazerem outra lista com multi no mobile. **Recomendação:** (a) — os single-select do mesmo arquivo já saem de `municipalityFilterDefinitions` num `.map` (linha 220), então a assimetria é interna ao arquivo e barata de remover; não é abstração nova, é usar a que já existe. + +## Abordagem proposta + +```mermaid +flowchart LR + F1["F1 leitura única
municipalityPageData"] --> Lista["/campanha/municipios"] + F2["F2 memo de processo
municipalityPotential"] --> Lista + F2 --> Card["Card conta da cadeira"] + F3["F3 TooltipProvider
no layout (app)"] --> Chrome["Tooltips /campanha"] + F4["F4 multi do mobile
via definitions"] --> Filtros["MunicipalityFilters"] +``` + +Componentes: + +- **`loadMunicipalityScope`** (`src/utilities/campaignMunicipalityScope.ts:77`): hoje sempre consulta. Ganha uma variante que recebe os docs já lidos (`municipalityListSelect` é **superset estrito** do select do escopo — `city`, `zoneNumber`, `lastUpdateAt`, `politicalTrend` a mais), mantendo o `cache()` por request e executando só a metade de `aggregatePledgesByMunicipality`. Consumido por `municipalityPageData.ts:342` quando `isPagedByPayload === false` (`:317`). +- **`computeAllMunicipalityPotentials`** (`src/utilities/municipalityPotential.ts:152`): passa a preencher um `Map` de módulo, no mesmo padrão de `statewideTotalsCache` (`bahiaElectionAggregates.ts:90`), `rankByYearCache` (`municipalityVoteRank.ts:18`) e `catalogContext` (`municipalityTerritorialClass.ts`). `computeStatewideSuggestedGoals` (`:296`) continua dentro do `cache()` de request porque lê `campaignGoals`. +- **`CampaignHoverTooltip`** (`src/components/campaign/shared/CampaignHoverTooltip.tsx:75`): remove o `` interno; o provider passa a ser renderizado uma vez em `src/app/(campaign)/campanha/(app)/layout.tsx`. Hoje a lista monta ~30 (5 headers + 25 células de Classe via `CampaignCellTooltip`), e `skipDelayDuration` do Radix só agrupa dentro de um mesmo Provider. +- **`MunicipalityFilters`** (`src/components/campaign/municipality/MunicipalityFilters.tsx:203,261,279,297`): os quatro blocos `CampaignMobileMultiFilterField` (região, tendência, classe, assessor) viram um `.map` sobre as definitions multi, espelhando o `.map` dos single-select em `:220`. `municipalityFilterDefinitions` (`municipalityListFilters.ts:113`) já carrega `param`/`label`/`options`/`staffOnly`; o que falta é `selected` + `emptyLabel` por param. +- **Migration**: sem migration, sem collection, sem server action. + +## Dependências + +- Nenhuma dura de outro item aberto. F1 fecha o "Adiado com gatilho" de [ranking-votos-municipio.md](ranking-votos-municipio.md) e trata junto o full-scan de `expectedVotes`/`coverage` que o B15 documenta. F4 é suave com **B21**/**B33** (se a extração do head compartilhado chegar antes, herda a peça). +- Reusa: `campaignMunicipalityScope.ts`, `municipalityPageData.ts`, `municipalityPotential.ts`, `municipalityListFilters.ts`, `CampaignMobileMultiFilterField`, `CampaignHoverTooltip`, padrão de memo de `bahiaElectionAggregates.ts`. + +## Não escopo + +- **Seletor de colunas / filtros salvos** — **B17** / **B18**. +- **Tooltip por célula como feature de produto** (nomes de assessores, justificativa de tendência) — **B23**; este lote só move o Provider. +- **Coluna de rank, cenário na URL, símbolo proporcional** — A11 / "Cenário junto aos filtros" / **B13**. +- **`TerritoryListColumns` com formatador de percentual próprio** — descartado no triage: outra superfície, difere só no zero à direita, e unificar exige decisão de produto sobre a casa decimal. + +## Rabbit holes + +- **"Já que estou no loader, unifico todos os sorts derivados numa camada."** Se alguém "só completar": nasce um repositório de municípios e o `where` do Payload vira DSL própria. **Mitigação neste item:** F1 toca **um** branch (`isPagedByPayload === false`) e não muda assinatura pública de `loadMunicipalityScope`. +- **"Já que estou no Provider, revejo o disclosure do card."** Se alguém "só completar": cai no debate Tooltip×Popover que o critique do E8 deixou explicitamente adiado. **Mitigação:** F3 é hoist puro; qualquer mudança de gesto fica com o gatilho já registrado em [conta-da-cadeira.md](conta-da-cadeira.md). +- **"Memoizo tudo que é puro por processo."** Se alguém "só completar": memoiza algo que lê `campaignGoals`/pledges e a mesa vê número velho. **Mitigação:** F2 memoiza só o que não toca o banco; o teste do lote pina que editar `campaignGoals` muda a meta sugerida na hora. + +## Adiado com gatilho + +- **`TERRITORIAL_CLASSES` sem consumidor e as três derivações `Object.keys(...) as ...`.** Mover a tupla + tipos para módulo de contrato client-safe removeria o cast, mas `trend` usa o **mesmo** idioma no mesmo arquivo — corrigir só o novo deixa o arquivo menos consistente. Revisitar quando: 3º enum multi entrar na lista (o **B33** traz Partido em `/campanha/dobradinhas`), tratando trend+class+partido de uma vez. +- **`TerritorialClassRow` reimplementa o trigger do `GoalAccountMetric`.** Só o estilo do botão é duplicado — o comportamento de toque/dismiss vive no `CampaignHoverTooltip` compartilhado, então o risco de regressão assimétrica é menor do que parece. Revisitar sob o gatilho **já registrado** em [conta-da-cadeira.md](conta-da-cadeira.md): quando E9–E14 adicionarem uma 5ª métrica a essa `dl`/grid, decidir layout/idioma antes de replicar o componente de novo. +- **Três caches de módulo no classificador viram um só.** Legibilidade, não custo — o sort redundante já morreu no simplify. Revisitar quando **E15** recalibrar as âncoras (é quando alguém vai reler as três lifetimes de uma vez). + +## Referências + +- `docs/roadmap.md` (Fill-ins abertos — **E10+**) +- [classificacao-territorial-relativa.md](classificacao-territorial-relativa.md) — o pai do lote (E10 ✓) +- [ranking-votos-municipio.md](ranking-votos-municipio.md) — "Adiado com gatilho" que F1 fecha (3º path derivado) +- [conta-da-cadeira.md](conta-da-cadeira.md) — gatilho do disclosure do `GoalAccountMetric` (E8) +- `src/utilities/municipalityPageData.ts` (`:317` `isPagedByPayload`, `:319` `listQuery`, `:342` escopo) — F1 +- `src/utilities/campaignMunicipalityScope.ts` (`:77`) — F1 +- `src/utilities/municipalityPotential.ts` (`:152`, `:296`) — F2 +- `src/components/campaign/shared/CampaignHoverTooltip.tsx` (`:75`) + `src/app/(campaign)/campanha/(app)/layout.tsx` — F3 +- `src/components/campaign/municipality/MunicipalityFilters.tsx` (`:203`–`:313`) + `src/utilities/municipalityListFilters.ts` (`:113`) — F4 +- AGENTS.md — `overrideAccess: false` com `user`, escada de cache (React `cache()` vs memo de processo vs artefato), gate por entrega diff --git a/docs/plans/ranking-votos-municipio.md b/docs/plans/ranking-votos-municipio.md index 0d60375b..d451ab9d 100644 --- a/docs/plans/ranking-votos-municipio.md +++ b/docs/plans/ranking-votos-municipio.md @@ -90,7 +90,7 @@ Componentes tocados: `src/lib/municipalityVoteRank.ts` (novo, puro + unit tests) - **Seletor de ano na ordenação da lista.** Gatilho: pedido da mesa por leitura de série na lista. - **Coluna de rank na fila do E9.** Entra junto com E9 (a fila referencia este helper). -- **Hidratação única do sort derivado `votos` (evitar find filtrado + `loadMunicipalityScope` redundante).** Gatilho: lista lenta no recorte amplo ou 3º path derivado no loader; B15 já documenta full-scan de `expectedVotes`/`coverage` — tratar junto. +- ~~**Hidratação única do sort derivado `votos` (evitar find filtrado + `loadMunicipalityScope` redundante).** Gatilho: lista lenta no recorte amplo ou 3º path derivado no loader; B15 já documenta full-scan de `expectedVotes`/`coverage` — tratar junto.~~ — **gatilho disparou em 2026-07-25**: com `classe` (E10) o loader chegou ao 4º path derivado (`votos`, `deficit`, `frescor`, `classe`). Registrado como **E10+ F1** · [plano](escala-dry-pos-e10.md). ## Já resolvido no simplify/critique (não reabrir) diff --git a/docs/roadmap.md b/docs/roadmap.md index f1db7f16..e320146c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap — Teqo -Atualizado em: 2026-07-25 (**E10 ✓** classificação territorial relativa entregue — Reduto/Expansão/Manutenção/Marginal/Sem base sobre o artefato TSE, coluna+sort+filtro na lista, badge no card do E8 e na capa do dossiê, e o seam genérico `cellTooltip` da `CampaignTable` (núcleo do B23) com `CampaignHoverTooltip` promovido para `shared/`; **B13** liberado; **C8 F4b** novo fill-in — o guard de `formActions` só vê arquivos chamados exatamente `formActions.ts`, achado do `/simplify` do C13; **B26 ✓** registrar sinal na célula "Último sinal" — [plano](plans/registrar-sinal-lista-municipios.md); **C13 ✓** "Plano de Ação" → **"Atividade"** renomeado em toda a vertical — collection `activity`, rota `/campanha/atividades`, sidebar "Atividades", migration data-preserving escrita à mão (4 tabelas, 3 enums, 2 colunas, 39 índices, 14 FKs, 2 sequences + os 8 índices fósseis `%plaza%` da remodelagem) e guard de vocabulário table-driven varrendo `src`+`tests`+`scripts`; [plano](plans/renomear-plano-acao-para-atividade.md); **B37** chips editáveis de municípios na lista de dobradinhas — coluna "Municípios" de `/campanha/dobradinhas` troca o número morto (`municipalityCount`) por chips com link (município) / território (`MapPinIcon`) e o mesmo Popover+combobox do **B34** (busca por município/TI/ZE, lote de território/zona), mas escrevendo do lado invertido (`municipality.stateDeputies`, sem piso mínimo, lock por município em vez de por dobradinha, sem `assertMunicipalitiesWithinScope` porque `canUpdateMunicipality` já escopa o assessor); reaproveita/promove a mesma lib do B19/B34 (quem chegar primeiro faz a renomeação); mesmo conflito registrado com o pedido literal (toggle "Editar" de tabela inteira) que B28/B31/B32/B34/B36 já resolveram na direção do Popover por célula — decisão marcada como _proposta, a validar com produto_; sem migration, [plano](plans/chips-municipios-lista-dobradinhas.md); **B36** coluna "Lideranças" em `/campanha/dobradinhas` com a mesma UI/mecânica da coluna "Municípios" de `/campanha/assessores` (B19 ✓) — chips + Popover + `Command` + auto-save por delta + "Ver mais…" (clamp de 3 linhas por `ResizeObserver`, necessário aqui porque um deputado popular pode acumular muito mais lideranças do que uma liderança acumula dobradinhas); pedido explícito de não duplicar lógica — reusa **sem alteração** a mutação/schema/lock que o **B31** desenha para o lado inverso da mesma relação `leadership.stateDeputies`, promovendo a célula desse item para um componente compartilhado bidirecional (`LeadershipStateDeputyRelationCell`) em vez de um segundo componente-irmão; corrida de extração com o B31 (quem entrar primeiro paga a peça compartilhada); sem migration, [plano](plans/chips-liderancas-lista-dobradinhas.md); **B34** chips editáveis de municípios na lista de lideranças — coluna "Municípios" de `/campanha/liderancas` ganha chips com link (município) / território (`MapPinIcon`), edição por **Popover disparado por célula** com chips removíveis + combobox `Command` (busca por município/TI/ZE, lote de território/zona), gravação **por delta** direto em `leadership.municipalities` com lock `leadership-municipalities:{id}` (piso de 1 e teto de 30 respeitados) e `revalidatePath` (`liderancas`, `liderancas/[id]`, `municipios/[slug]` dos tocados); promove a lógica pura de `advisorMunicipalityPortfolio.ts`/`loadAdvisorMunicipalityIndex` (**B19 ✓**) para nomes neutros, sem portar a UI com `ResizeObserver`; **conflito registrado com o pedido literal** (pede um toggle "Editar" de tabela inteira, que B28/B31/B32 já travaram como anti-goal nesta mesma tabela — este plano segue o precedente e marca a decisão como _proposta, a validar com produto_); sem migration, [plano](plans/chips-municipios-lista-liderancas.md); **B33** ordenação e filtro no header da lista de dobradinhas — leva B15 ✓/B16 ✓ para `/campanha/dobradinhas` (sort por Nome/Partido, filtro por Partido); consome o header rico compartilhado que **B29** extrai (ou extrai o dele mesmo, se chegar primeiro — mesma corrida de ordem de chegada de B21/B29) — Não escopo do B29 já nomeava esta entrada; contagens derivadas (`municipalityCount`/`leadershipCount`) ficam fora do sort v1, [plano](plans/ordenacao-filtros-lista-dobradinhas.md); **B32** editar Status de apoio na célula da lista de lideranças — clicar na célula abre um Popover com select de `supportStatus`, gravando sozinho por endpoint JSON dedicado (`POST /campanha/liderancas/support-status`, espelho do `expected-votes/`), **sem** o toggle "Editar" de tabela inteira (pedido explícito de produto, mesmo anti-goal do B28/B31); fecha o "Adiado com gatilho" que o próprio B29 já havia registrado para este caso; extrai `isSameOriginRequest` (2º endpoint) e as mensagens seguras do form action da ficha para módulos compartilhados; sem migration, [plano](plans/autosave-status-lista-liderancas.md); **B31** coluna de dobradinhas na lista de lideranças — chips com link + adicionar/remover na própria linha, com busca e gravação automática, na paridade pedida com a coluna "Municípios" de `/campanha/assessores` (B19 ✓) — paridade **da coluna**, não do modo "Editar" de tabela inteira (anti-goal do princípio 3 e do B28); escrita **por delta** com lock (`leadership-state-deputies:{id}`) em vez de replace do array, e `revalidatePath` porque `/campanha/dobradinhas` tem contagens derivadas; sem migration, [plano](plans/dobradinhas-lista-liderancas.md); **B30** convite de completar cadastro por WhatsApp na lista de lideranças — ação ao fim de cada linha de `/campanha/liderancas` reusando `createCampaignInvite`/`kind: 'autopreenchimento'` (mesmo fluxo do detalhe), disabled quando `contact.phone` não normaliza (`normalizeBrazilianPhone`); ícone distinto do WhatsApp de contato do B28 (soft, mesma linha); sem migration/action nova, [plano](plans/convite-whatsapp-lista-liderancas.md); **B29** ordenação e filtro no header da lista de lideranças — leva B15 ✓/B16 ✓ para `/campanha/liderancas` (sort/filtro na URL, popover no header, colunas "Setor" e "Última atualização"), ordenação por nome via join `contact.name` (sem desnormalizar), e é o **3º call site** que dispara a extração do par head/filtro para `shared/` (municípios migrado junto; B21 fecha seu adiado), [plano](plans/ordenacao-filtros-lista-liderancas.md); **B28** e-mail + celular na lista de `/campanha/liderancas` — colunas com copy-on-click + WhatsApp como em `/campanha/assessores` (B19 ✓); edição in-list adiada; sem migration, [plano](plans/email-celular-lista-liderancas.md); **B27** combobox + chips + auto-save no popover de Assessores de `/campanha/municipios` — a lista de checkboxes com botão "Salvar assessores" vira chips removíveis sobre um combobox com busca, gravando **por delta** (endpoint JSON espelho do `expected-votes/`, lock do B19 ✓), [plano](plans/combobox-assessores-lista-municipios.md); ~~**B26**~~ ✓ registrar sinal na célula "Último sinal" (entregue 2026-07-25), [plano](plans/registrar-sinal-lista-municipios.md); **fill-in** renomear coluna "Cobertura da meta" → "Cobertura" em `/campanha/municipios` — header curto (padrão A11); glossário/KPI strips intactos; B16 ✓ liberou o nome ao remover "Assessoria", [plano](plans/renomear-coluna-cobertura-meta.md); **B25** link do território na lista de municípios — a célula da coluna "Território" vira link para a linha daquele TI em `/campanha/territorios` (âncora, sem rota de detalhe); dep dura B21, [plano](plans/link-territorio-lista-municipios.md); **B24** auto-save + justificativa editável no popover de Tendência de `/campanha/municipios` — tira o botão "Salvar" que o princípio 4 do `PRODUCT.md` proíbe e fecha o adiado "nota no popover" do B9, [plano](plans/autosave-tendencia-lista-municipios.md); **fill-in** remover coluna "Tipo" de `/campanha/municipios` — município e ZE tratados iguais na coordenação; filtro por Salvador cobre as zonas; drop também do filtro/sort/`?kind=` na URL da lista, schema `kind` intacto, [plano](plans/remover-coluna-tipo-municipios.md); **B23** tooltip de conteúdo nas células das listas — `cellTooltip` em `CampaignTableColumn`, consumidores as colunas "Assessores" e "Tendência" (justificativa do E4R no hover; sem nota, sem tooltip) de `/campanha/municipios`, [plano](plans/tooltip-celulas-listas.md); **fill-in** opções selecionadas no topo nos Popovers de filtro do header — helper puro reutilizável pelas demais listas, [plano](plans/selecionados-no-topo-filtros-header.md); **B22** explicação por coluna no header das listas — tooltip de hover/foco/tap promovido para o sistema de listas compartilhado, [plano](plans/explicacao-colunas-header-listas.md); **B21** página própria dos Territórios de Identidade — rota `/campanha/territorios` com overview + tabela no sistema de listas (sort/filtro no header), sem migration, [plano](plans/pagina-territorios-identidade.md); **B20 ✓** Municípios prioritários no Início entregue 2026-07-25 — atalho ranqueado por déficit + markup sem bullets, [plano](plans/municipios-prioritarios-inicio.md); **Pass 2 ✓** consolidação de engenharia — listas genéricas/B16+ absorvido/B17-B18 triviais/E10 limpo/C8 F4 ✓/D3 consent WhatsApp; **B16 ✓**; **E16 ✓**; **E9 ✓**; **E18 ✓**; **B19 ✓**; **E8 ✓**; C12 ✓; B18; B17; A11 ✓; B15 ✓; E4R ✓; E17 ✓; B14; janela 1: smoke, R6) +Atualizado em: 2026-07-25 (**E10 ✓** classificação territorial relativa entregue — Reduto/Expansão/Manutenção/Marginal/Sem base sobre o artefato TSE, coluna+sort+filtro na lista, badge no card do E8 e na capa do dossiê, e o seam genérico `cellTooltip` da `CampaignTable` (núcleo do B23) com `CampaignHoverTooltip` promovido para `shared/`; **B13** liberado; **E10+** novo fill-in de engenharia com os quatro débitos do `/simplify` do E10 — a lista lendo `municipality` duas vezes por request (o que **dispara o "Adiado com gatilho" do A11**: `classe` é o 4º path derivado no loader), memo de processo dos potenciais por município, `TooltipProvider` único no layout e os multi-filtros do mobile saindo das definitions · [plano](plans/escala-dry-pos-e10.md); **C8 F4b** novo fill-in — o guard de `formActions` só vê arquivos chamados exatamente `formActions.ts`, achado do `/simplify` do C13; **B26 ✓** registrar sinal na célula "Último sinal" — [plano](plans/registrar-sinal-lista-municipios.md); **C13 ✓** "Plano de Ação" → **"Atividade"** renomeado em toda a vertical — collection `activity`, rota `/campanha/atividades`, sidebar "Atividades", migration data-preserving escrita à mão (4 tabelas, 3 enums, 2 colunas, 39 índices, 14 FKs, 2 sequences + os 8 índices fósseis `%plaza%` da remodelagem) e guard de vocabulário table-driven varrendo `src`+`tests`+`scripts`; [plano](plans/renomear-plano-acao-para-atividade.md); **B37** chips editáveis de municípios na lista de dobradinhas — coluna "Municípios" de `/campanha/dobradinhas` troca o número morto (`municipalityCount`) por chips com link (município) / território (`MapPinIcon`) e o mesmo Popover+combobox do **B34** (busca por município/TI/ZE, lote de território/zona), mas escrevendo do lado invertido (`municipality.stateDeputies`, sem piso mínimo, lock por município em vez de por dobradinha, sem `assertMunicipalitiesWithinScope` porque `canUpdateMunicipality` já escopa o assessor); reaproveita/promove a mesma lib do B19/B34 (quem chegar primeiro faz a renomeação); mesmo conflito registrado com o pedido literal (toggle "Editar" de tabela inteira) que B28/B31/B32/B34/B36 já resolveram na direção do Popover por célula — decisão marcada como _proposta, a validar com produto_; sem migration, [plano](plans/chips-municipios-lista-dobradinhas.md); **B36** coluna "Lideranças" em `/campanha/dobradinhas` com a mesma UI/mecânica da coluna "Municípios" de `/campanha/assessores` (B19 ✓) — chips + Popover + `Command` + auto-save por delta + "Ver mais…" (clamp de 3 linhas por `ResizeObserver`, necessário aqui porque um deputado popular pode acumular muito mais lideranças do que uma liderança acumula dobradinhas); pedido explícito de não duplicar lógica — reusa **sem alteração** a mutação/schema/lock que o **B31** desenha para o lado inverso da mesma relação `leadership.stateDeputies`, promovendo a célula desse item para um componente compartilhado bidirecional (`LeadershipStateDeputyRelationCell`) em vez de um segundo componente-irmão; corrida de extração com o B31 (quem entrar primeiro paga a peça compartilhada); sem migration, [plano](plans/chips-liderancas-lista-dobradinhas.md); **B34** chips editáveis de municípios na lista de lideranças — coluna "Municípios" de `/campanha/liderancas` ganha chips com link (município) / território (`MapPinIcon`), edição por **Popover disparado por célula** com chips removíveis + combobox `Command` (busca por município/TI/ZE, lote de território/zona), gravação **por delta** direto em `leadership.municipalities` com lock `leadership-municipalities:{id}` (piso de 1 e teto de 30 respeitados) e `revalidatePath` (`liderancas`, `liderancas/[id]`, `municipios/[slug]` dos tocados); promove a lógica pura de `advisorMunicipalityPortfolio.ts`/`loadAdvisorMunicipalityIndex` (**B19 ✓**) para nomes neutros, sem portar a UI com `ResizeObserver`; **conflito registrado com o pedido literal** (pede um toggle "Editar" de tabela inteira, que B28/B31/B32 já travaram como anti-goal nesta mesma tabela — este plano segue o precedente e marca a decisão como _proposta, a validar com produto_); sem migration, [plano](plans/chips-municipios-lista-liderancas.md); **B33** ordenação e filtro no header da lista de dobradinhas — leva B15 ✓/B16 ✓ para `/campanha/dobradinhas` (sort por Nome/Partido, filtro por Partido); consome o header rico compartilhado que **B29** extrai (ou extrai o dele mesmo, se chegar primeiro — mesma corrida de ordem de chegada de B21/B29) — Não escopo do B29 já nomeava esta entrada; contagens derivadas (`municipalityCount`/`leadershipCount`) ficam fora do sort v1, [plano](plans/ordenacao-filtros-lista-dobradinhas.md); **B32** editar Status de apoio na célula da lista de lideranças — clicar na célula abre um Popover com select de `supportStatus`, gravando sozinho por endpoint JSON dedicado (`POST /campanha/liderancas/support-status`, espelho do `expected-votes/`), **sem** o toggle "Editar" de tabela inteira (pedido explícito de produto, mesmo anti-goal do B28/B31); fecha o "Adiado com gatilho" que o próprio B29 já havia registrado para este caso; extrai `isSameOriginRequest` (2º endpoint) e as mensagens seguras do form action da ficha para módulos compartilhados; sem migration, [plano](plans/autosave-status-lista-liderancas.md); **B31** coluna de dobradinhas na lista de lideranças — chips com link + adicionar/remover na própria linha, com busca e gravação automática, na paridade pedida com a coluna "Municípios" de `/campanha/assessores` (B19 ✓) — paridade **da coluna**, não do modo "Editar" de tabela inteira (anti-goal do princípio 3 e do B28); escrita **por delta** com lock (`leadership-state-deputies:{id}`) em vez de replace do array, e `revalidatePath` porque `/campanha/dobradinhas` tem contagens derivadas; sem migration, [plano](plans/dobradinhas-lista-liderancas.md); **B30** convite de completar cadastro por WhatsApp na lista de lideranças — ação ao fim de cada linha de `/campanha/liderancas` reusando `createCampaignInvite`/`kind: 'autopreenchimento'` (mesmo fluxo do detalhe), disabled quando `contact.phone` não normaliza (`normalizeBrazilianPhone`); ícone distinto do WhatsApp de contato do B28 (soft, mesma linha); sem migration/action nova, [plano](plans/convite-whatsapp-lista-liderancas.md); **B29** ordenação e filtro no header da lista de lideranças — leva B15 ✓/B16 ✓ para `/campanha/liderancas` (sort/filtro na URL, popover no header, colunas "Setor" e "Última atualização"), ordenação por nome via join `contact.name` (sem desnormalizar), e é o **3º call site** que dispara a extração do par head/filtro para `shared/` (municípios migrado junto; B21 fecha seu adiado), [plano](plans/ordenacao-filtros-lista-liderancas.md); **B28** e-mail + celular na lista de `/campanha/liderancas` — colunas com copy-on-click + WhatsApp como em `/campanha/assessores` (B19 ✓); edição in-list adiada; sem migration, [plano](plans/email-celular-lista-liderancas.md); **B27** combobox + chips + auto-save no popover de Assessores de `/campanha/municipios` — a lista de checkboxes com botão "Salvar assessores" vira chips removíveis sobre um combobox com busca, gravando **por delta** (endpoint JSON espelho do `expected-votes/`, lock do B19 ✓), [plano](plans/combobox-assessores-lista-municipios.md); ~~**B26**~~ ✓ registrar sinal na célula "Último sinal" (entregue 2026-07-25), [plano](plans/registrar-sinal-lista-municipios.md); **fill-in** renomear coluna "Cobertura da meta" → "Cobertura" em `/campanha/municipios` — header curto (padrão A11); glossário/KPI strips intactos; B16 ✓ liberou o nome ao remover "Assessoria", [plano](plans/renomear-coluna-cobertura-meta.md); **B25** link do território na lista de municípios — a célula da coluna "Território" vira link para a linha daquele TI em `/campanha/territorios` (âncora, sem rota de detalhe); dep dura B21, [plano](plans/link-territorio-lista-municipios.md); **B24** auto-save + justificativa editável no popover de Tendência de `/campanha/municipios` — tira o botão "Salvar" que o princípio 4 do `PRODUCT.md` proíbe e fecha o adiado "nota no popover" do B9, [plano](plans/autosave-tendencia-lista-municipios.md); **fill-in** remover coluna "Tipo" de `/campanha/municipios` — município e ZE tratados iguais na coordenação; filtro por Salvador cobre as zonas; drop também do filtro/sort/`?kind=` na URL da lista, schema `kind` intacto, [plano](plans/remover-coluna-tipo-municipios.md); **B23** tooltip de conteúdo nas células das listas — `cellTooltip` em `CampaignTableColumn`, consumidores as colunas "Assessores" e "Tendência" (justificativa do E4R no hover; sem nota, sem tooltip) de `/campanha/municipios`, [plano](plans/tooltip-celulas-listas.md); **fill-in** opções selecionadas no topo nos Popovers de filtro do header — helper puro reutilizável pelas demais listas, [plano](plans/selecionados-no-topo-filtros-header.md); **B22** explicação por coluna no header das listas — tooltip de hover/foco/tap promovido para o sistema de listas compartilhado, [plano](plans/explicacao-colunas-header-listas.md); **B21** página própria dos Territórios de Identidade — rota `/campanha/territorios` com overview + tabela no sistema de listas (sort/filtro no header), sem migration, [plano](plans/pagina-territorios-identidade.md); **B20 ✓** Municípios prioritários no Início entregue 2026-07-25 — atalho ranqueado por déficit + markup sem bullets, [plano](plans/municipios-prioritarios-inicio.md); **Pass 2 ✓** consolidação de engenharia — listas genéricas/B16+ absorvido/B17-B18 triviais/E10 limpo/C8 F4 ✓/D3 consent WhatsApp; **B16 ✓**; **E16 ✓**; **E9 ✓**; **E18 ✓**; **B19 ✓**; **E8 ✓**; C12 ✓; B18; B17; A11 ✓; B15 ✓; E4R ✓; E17 ✓; B14; janela 1: smoke, R6) Registro canônico dos **próximos** planos e débitos. Histórico de entregas: resumo abaixo + planos em [`docs/plans/`](plans/) + notebook [`.cursor/rules/projects/nucleos-eleitorais.mdc`](../.cursor/rules/projects/nucleos-eleitorais.mdc). ## Âncoras do calendário eleitoral 2026 (Res. TSE 23.760/2026) @@ -124,6 +124,7 @@ O discovery literatura→persona→entrevista ([relatório aprovado](research/re - **Selecionados no topo nos filtros do header** em `/campanha/municipios` — nos Popovers de filtro (B16 ✓) as opções já marcadas sobem para o topo (ordem congelada enquanto o Popover está aberto; divisor de hairline), para desmarcar sem caçar a checkbox entre 27 territórios / 435 municípios; a regra sai como **helper puro em `src/lib/`** (`orderFilterOptionsSelectedFirst`), consumido pelo Popover desktop e pelo select mobile e pronto para **B21**/E12 — sem extrair o head genérico (gatilho do B21 segue sendo o 3º call site; **B22** promove o tooltip pelo mesmo critério de "peça compartilhada sem head genérico") · ~0,25–0,5d · Impeccable B · soft: B16 ✓ / B21 / B22 · [plano](plans/selecionados-no-topo-filtros-header.md) - **Cenário junto aos filtros** em `/campanha/municipios` — mover `VoteEstimateScenarioField` para a fileira/barra slim de `MunicipalityFilters` (com **B16**, a barra deixa de carregar os selects de coluna); overview só consome o contexto · ~0,25–0,5d · Impeccable B · soft: B16 · [plano](plans/cenario-junto-filtros-municipios.md) - ~~**B16+** escala/DRY pós-B16~~ — **absorvido pelo Pass 2 W1-D1 (2026-07-25)**: `useOptimistic`, facet por slugs, hrefs via serializador canônico, facets no mesmo `Promise.all` · [plano](plans/escala-dry-pos-b16.md) +- **E10+** escala/DRY pós-E10 — quatro fases do `/simplify` da classificação territorial: **F1** a lista lê `municipality` 2× por request (`listQuery` + `loadMunicipalityScope`, mesmo `where`, select superset) — **fecha o "Adiado com gatilho" do A11**, cujo gatilho ("3º path derivado no loader") disparou com `classe` sendo o 4º (`votos`, `deficit`, `frescor`); **F2** `computeAllMunicipalityPotentials()` recomputa 435 linhas puras do artefato por request (memo de processo, como `bahiaElectionAggregates`/`municipalityVoteRank`); **F3** um `TooltipProvider` por tooltip (~30 na lista) anula o agrupamento de delay do Radix — hoist para o layout de `(app)`; **F4** os 4 multi-filtros do mobile saem da mesma `municipalityFilterDefinitions` que os single-select vizinhos já usam · ~0,75–1d eng · Impeccable A · sem migration · cortável · [plano](plans/escala-dry-pos-e10.md) - **O0+** escala/DRY pós-Onda 0 · [plano](plans/escala-dry-pos-onda0.md) - **RS+** auth read leve + shells de senha · [plano](plans/escala-dry-pos-reset-senha-perfil.md) - **C10 / C11** escala apoiadores/planos · C11 absorveu a extensão pós-C12 para picker de planos e paginação das demandas vinculadas (Fase 6 condicional; ≥100 planos ou ≥50 demandas/plano) · gatilhos: base nominal crescendo ([plano](plans/escala-dry-pos-c9.md)) / volume de planos medido ([plano](plans/escala-dry-pos-c7.md))