O /simplify da escala relativa do mapa achou um bug semantico real em
computeAggregateTerritorialClass — somava o headroom dos slugs e comparava
contra a mediana de UMA unidade, classificando um grupo grande como expansao
quando ele e marginal. A mediana agora escala por slugs.length, com teste.
As bolhas sairam do centro do bounding box para o centroide real e ganharam
pane proprio (z-index 450), porque o bringToFront do hover as escondia atras
do poligono. Legenda e pintura passaram a compartilhar o mesmo predicado,
a copy dos quantis parou de prometer faixas de igual largura, e o subtitulo
do painel parou de dizer "em 2026" com 2022 selecionado (defeito do B11).
Fonte unica onde havia duas: NO_DATA_FILL, o raio das bolhas e
formatPlacementOrdinal (ex-formatMunicipalityVoteRank, agora client-safe em
electionFormat.ts).
Os quatro debitos maiores que o cleanup viraram o fill-in B13+
(docs/plans/escala-dry-pos-b13.md); a flakiness dos e2e, reproduzida em main
limpo, foi para o ledger com a causa afinada.
O coropleto do Inicio era quase monocromatico porque 0-100% dos validos comprime
uma disputa cujo topo estadual e 4,71%. Tres escalas relativas ao proprio
candidato, todas em classes discretas: quantis (novo default), padrao proprio
(LQ) e posicao no municipio, ao lado de "% dos validos" (B11) e "Total (votos)".
Artefato v3: federalRankByIbgeCode com a colocacao entre todos os candidatos com
votos > 0 na geografia (2014/2018/2022), chaveado por ibgeCode porque e o que o
mapa desenha. Fica no artefato commitado, nao em query de request, que varreria
~100 mil linhas por ano para um dado imutavel. 534 -> 623 KB (orcamento 700 KB).
LQ virou 5 classes nas ancoras do E10, nao diverging: o plano pedia as duas
coisas e se contradizia. Vermelho x azul continua significando "Solla x
adversario" e segue exclusivo do compare.
computeAggregateTerritorialClass classifica um grupo de slugs como um territorio
so, somando insumos e rodando o mesmo classificador (LQ e razao; a media de
razoes nao e a razao das somas) - o E12 herda o helper. TERRITORIAL_CLASS_ANCHORS
mudou para src/lib/territorialClassAnchors.ts para nao arrastar o artefato TSE
ao bundle do cliente.
Quantis sobre o escopo de acesso do usuario, degradando para 3 classes abaixo de
10 municipios com a legenda declarando a contagem efetiva. Rank indisponivel em
2026 (dado TSE); LQ em 2026 reusa o padrao estadual de 2022.
Bolhas opcionais por validos projetados (area proporcional ao valor, cor da
classe E10, interactive: false). 417 marcadores medidos antes de otimizar - nao
derrubaram o frame, entao o corte para top-K nao foi feito.
Sem migration, sem collection, sem server action; bundle client de /campanha
byte a byte igual.
- MunicipalityListAdvisorsControl: stop advancing lastPropsIDsRef from a
successful delta response — it exists only to detect a genuine external
prop change, and writing it there made a later stale re-render of the
pre-edit content look external and clobber optimistic state back to it.
Also reconcile with the latest confirmed server set on the error path
once every in-flight delta settles (previously only the success path did),
and consolidate chip/tooltip name resolution onto the shared
advisorEntriesFromIds helper instead of a bespoke lookup with an
'#id' placeholder.
- Extract campaignJsonMutationErrorResponse/parseCampaignJsonRequestBody
(src/utilities/campaignJsonMutationRoute.ts) and adopt them in both
municipios/advisors/route.ts and municipios/expected-votes/route.ts,
removing the duplicated body-parse + error-mapping ladder.
- Municipality.ts's validateMunicipalityAdvisors now uses
uniqueRelationshipIds instead of re-inlining the Set+map+filter dance
the helper was extracted for.
Verified: tsc --noEmit, lint, format:check, check:cycles all clean;
full unit suite (450 tests) and the municipality/advisor int suites pass.
knip's pre-existing payload.config.ts load error is unrelated (P3).
Playwright's spawned webServer failed to boot in this sandbox (unrelated
module-resolution environment issue, reproduced even before this diff);
manual pnpm dev boots fine and the change is covered by unit/int tests.
Replaces the checkbox-list + "Salvar assessores" button in the
Assessors popover (/campanha/municipios) with removable chips + a
Command combobox, delta-based auto-save (one advisor add/remove per
request, never the whole array), and optimistic UI with request
sequencing to resolve out-of-order responses correctly.
- New POST /campanha/municipios/advisors JSON route (mirrors
expected-votes/), backed by setMunicipalityAdvisorMembership(Record)
in actions/municipality.ts (text advisory lock + transaction).
- nextAdvisorIdsAfterMembership moved to
src/lib/municipalityAdvisorMembership.ts with unit tests.
- Shared isSameOriginRequest (src/utilities/sameOriginRequest.ts) and
uniqueRelationshipIds (src/utilities/relationship.ts) helpers,
de-duplicating the same-origin CSRF check and the
Set+map(relationshipId)+filter pattern across advisor call sites.
- MunicipalityListAdvisorsControl.tsx rewritten: chips, Command
combobox, per-item pending affordance, rollback on error, and a
request-sequence guard so only the most recently sent delta's
response is adopted when concurrent optimistic updates settle.
- e2e coverage for the combobox auto-save flow; setup prewarms the new
POST-only route to avoid Playwright dev-server HMR aborting the
first request.
- Plan and roadmap marked as delivered (B27).
- openOnTouch/disabled em CampaignHoverTooltip para conviver com Popover
sem roubar o tap nem sobreviver atras dele; falsy content agora renderiza
o trigger sem wrapper (colapsa o ternario nos dois consumidores)
- formatAdvisorNamesTooltip + advisorEntriesFromIds compartilhado
(MunicipalityAdvisorAvatarStack) para dedupe entre lista e controle
- MunicipalityListAdvisorsControl: hover mostra nomes completos (coordenador)
- MunicipalityListTrendControl: hover mostra a justificativa (politicalTrend.note,
E4R), com whitespace-pre-wrap preservado; leitura tambem dentro do Popover em toque
- testes unit dos wrappers/no-op e do helper
/simplify (pos-rebase): corrigido HEADER_TOOLTIP_COUNT (4->10) no teste da
celula de assessores, que ficou obsoleto depois do rebase absorver o B22
(10 tooltips de header no thead staff) - o teste passava a comparar 5 vs 11
sem essa correcao; formatAdvisorNamesTooltip agora tipa ReactElement | null
em vez de ReactNode, removendo os casts 'as React.ReactElement' no teste;
comentarios de baixa informacao removidos/enxugados em CampaignHoverTooltip
e nos dois controles; docs: plano, roadmap e notebook do projeto marcados
como entregues.
Gate verde: tsc, lint (0 warnings), format, 446 unit, build anterior verde.
Aikido: 0 findings.
/simplify: extrai campaignHoverExplanationClassName/campaignHoverTooltipAlign para CampaignHoverTooltip.tsx (elimina duplicacao entre CampaignTableHead, MunicipalitySortableHead e MunicipalityGoalAccountCard), troca non-null assertion de campaignConceptOneLiner por lookup em Record, corrige copy da coluna Assessores (coordenador, nao candidato) e restaura comentario de invariante do sort de goalCoverage
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).
- campoVotes (sem leitor) e classification.fieldHeadroom (duplicava o fator field) removidos
- formatTerritorialClassWhy passa a ser mesmo a fonte unica do slice(0,2) e do fallback
"sem serie", eliminando o sentinela de string vazia nas duas superficies de detalhe
- classe territorial vira um so componente com layout table|card, como os vizinhos
- parseExhaustiveEnumParam em campaignListUrl cobre trend e class (o includes era morto:
allParamValues ja deduplica)
- bloco central caminha o computeVoteRankByYear memoizado do A11 em vez de reordenar
os 435 slugs por conta propria
- comentarios corrigidos onde afirmavam o contrario do codigo (CampaignCellTooltip e a
regra do sem_base por superficie)
O rename mecanico deixou um defeito silencioso e alguns fosseis: o fixture de
recentVisits perdeu o ${index} (todas as entradas com o mesmo label, invisivel
porque o label nao era asserido) e access/shared.ts guardava "action plan" em
prosa, forma que o guard de vocabulario nao cobria. O padrao agora tolera espaco
e a allowlist encolheu: o comentario de campaignMigrationReconciliation cita
activity e o arquivo saiu da excecao.
Na migration, a entrada action_plan_municipality_idx era inalcancavel (nenhuma
migration jamais criou esse nome) e saiu, deixando up() e down() com os mesmos
48 pares; as duas buscas de FK passam a usar to_regclass, que retorna NULL em vez
de lancar -- a unica consulta que contradizia a invariante do proprio cabecalho.
O detalhe de organizacao para de derivar o rotulo de status a mao e reusa
ActivityStatusBadge, com status tipado como ActivityStatus no loader.
Debitos do /simplify registrados: C8 F4b (o guard de formActions so inspeciona
arquivos chamados exatamente formActions.ts) no plano do C8; defers e descartes
com gatilho no plano do C13.
Collection actionPlan -> activity (labels "Atividade"/"Atividades"), campo
campaignDemand.actionPlan -> activity, rota /campanha/planos ->
/campanha/atividades (criacao em atividades/nova), sidebar "Atividades" e
rotulos alinhados a entidade ("Tipo de atividade", "Origem da atividade",
"Resultado da atividade"). Valores de enum kind/status/origin inalterados.
Schema movido por migration data-preserving escrita a mao
(20260725_213000_rename_action_plan_to_activity): 4 tabelas, 3 enums, 2
colunas, 39 indices (4 pkeys inclusos), 14 FKs e 2 sequences, cada statement
guardado na existencia do objeto de origem, com down() simetrico. O gerador do
Payload emitiria DROP+CREATE e nao ve o indice parcial escrito a mao
action_plan_upcoming_start_at_idx, entao a lista veio de
pg_indexes/pg_constraint/pg_type/pg_sequences e o snapshot .json foi derivado
do anterior por transformacao textual -- validado por migrate:create __probe
sem mudanca de schema. A mesma migration alinha os 8 indices fosseis %plaza%
que a remodelagem de municipios deixou (renomeou as colunas plaza_id, nao os
indices).
Guard: o describe "banned campaign terminology" de
codebaseConventions.unit.spec.ts virou tabela de termos varrendo src+tests+
scripts (migrations fora -- historia congelada), banindo os compostos
actionPlan, a copy "plano de acao" e a rota antiga, mas nao plan/plano isolado
(colidiria com os planos de assinatura da Fase 2). Allowlist: o proprio spec,
scripts/generate-remodel-municipalities-migration.mjs e o comentario historico
em campaignMigrationReconciliation.int.spec.ts.
Plano: docs/plans/renomear-plano-acao-para-atividade.md
A seção deixa de truncar seis prioritárias sem critério; amostra oito por cobertura central, mostra o total e linka ?priority=alta alinhado à fila de alocação.
- AGENTS.md: Known Gap #2 resolvido (D3), arvore de estrutura (+AllocationDecision, PrivacyPolicy, CampaignGoals; components por dominio), URLs novas (/campanha/assessores|conceitos|perfil), entrada 'Recently resolved' do Pass 2
- tracker: todos os workstreams done; secao de achados de drift de docs de produto (PRODUCT/DESIGN/CUSTOMER - reportados, nao aplicados)
- TESTING.md: 46/48/7 + linhas do safety net (formActions wrapper, voteTrend, submitWhatsapp)
- TECH-DEBT: smells fechados (municipalityUi split, detail RSCs, formActions, components subpastas, electionInsights)
- planos: status entregue nos 5 do Pass 2; escala-dry-pos-b16 absorvido; C8 F3/F4 fechados em escala-dry-pos-c6; notas de costura em B17/B18/cenario-junto; nota de conflito E4R em mapa-projecao
- roadmap: entrada Pass 2 no 'Ja entregue', B17/B18 marcados como triviais, B16+ riscado, header atualizado
- notebook do projeto: entrada Pass 2
- DESIGN.md: secao do sistema de listas (unico drift aplicado, conforme plano)
- submitWhatsapp resolve Consent.key = 'whatsapp-inscricao' via requireConsentByKey e FALHA FECHADO com mensagem clara enquanto o documento nao existir
- migration 20260725_170000 (data-only, idempotente, guardada): tagueia o documento vivo id 2 (key vazia/'') com a chave estavel; no-op se ja houver documento com a chave ou se id 2 tiver outra chave; NAO inventa texto juridico (politica Onda 0)
- chave em lib/campaignConsentKeys + lease key nas fixtures; int spec pina fail-closed + caminho feliz (contact + subscription -> consent.id)
- ATENCAO DEPLOY: revisar o SQL antes do push (aplica em producao no build da Vercel)
- ARCHITECTURE.md: contexto de sistema (3 route groups, 2 barreiras de auth), regra de dependencia como aplicada, fluxo de dados collections->access->loaders->view models->RSC->ilhas, escada de caching, RBAC, bounded contexts + linguagem ubiqua, log de decisoes estruturais (incl. D1/D2/D5 do Pass 2)
- codebase-map.mdc: mapa navegacional compacto always-applied (onde vive o que, sistema de listas, como achar o dono de uma feature, invariantes); convencao: atualizado no mesmo PR que mover modulos
- ~70 strings visiveis + labels de admin (headers de coluna, APIErrors, empty states, dashboard, mapa, formularios, filtros) com concordancia de genero corrigida
- apoiadores desambiguado: cidade do contato vira 'Cidade'; a unidade operacional assume 'Municipio' (filtro, coluna, ficha)
- comentarios de codigo atualizados; payload-types regenerado
- fora por decisao D6: textos provisorios de consent do Onda 0 (lote juridico) e migrations congeladas
- e2e do plano de acao acompanha o label ('Municipio *')
- triplo de cenarios: municipalityMapData usa VoteEstimateScenarioFields (2 grafias inline eliminadas)
- MAX_LEADERSHIP_MUNICIPALITIES: Leadership.ts importa de lib/schemas/leadership (const local removida)
- campaignUserInitials: copia divergente do AvatarStack removida; fonte unica em campaignUserProfile (com fallback defensivo)
- tendencia politica: options do select da collection derivam de politicalTrendStatuses + politicalTrendLabels; ProjectionTrendStatus vira alias de PoliticalTrendStatusValue
- MunicipalityStrategyViewModel nomeado e exportado (consumidores param de indexar NonNullable<...['strategy']>); nota de por que NAO deriva do zod
- DynamicFind documentado como a unica grafia aprovada do cast
- stub<T> nos doubles de teste onde a checagem por propriedade e real (campaignUserPhone, campaignAuth, campaignConsent); deep fakes mantem o cast localizado
- formatElectionNumber -> src/lib/electionFormat.ts (13 importers); computeVoteTrend + tipos -> src/lib/voteTrend.ts
- ~52 exports sem consumidor deletados (limiares 35/20/10 da era nucleos, gap vs 2022, conversao, mobilizacao, ticket flip/leverage) - decisao D2; formulas no git history; E10 nasce em modulo novo (nota no plano classificacao-territorial-relativa.md)
- specs: voteTrend.unit.spec.ts (pins vivos + pin do defeito E18 de fracao), electionPartySpectrum.unit.spec.ts sem os blocos de ticket; electionInsights.unit.spec.ts deletado
- fix de regressao W2 pega pelo knip: scripts/import-projecao.mjs importava voteEstimate/slug nos paths antigos (knip agora roda sem pipe mascarando exit code)
- knip unused exports: 157 -> 140
106 arquivos flat -> 15 dominios (actionPlan, advisor, auth, dashboard, demand, invite, leadership, map, municipality, organization, stateDeputy, supporter, votePledge, shared, shell) via git mv; imports absolutos reescritos em 98 arquivos; excecao de lint do campaign-logo e paths dos specs de contrato (campaignInviteUi, campaignComponents) atualizados. Sem mudanca de conteudo nos componentes.
- CampaignTable/CampaignTableHead: colunas como dado (id, head island, cell, seams B17 mandatory/defaultVisible); colSpan derivado
- CampaignListFooter: contagem + paginacao compartilhadas
- MunicipalityList reescrita sobre column defs; cards mobile inalterados
- municipalityUi.ts (765/59 exports) dividido: municipalityListUrl / municipalityListFilters / municipalityLabels / municipalitySignal; contrato de URL byte-identico (pins verdes)
- B16+ absorvido: useOptimistic no HeaderFilter (onNavigate dentro da transition), hrefs de opcao via serializador canonico confiavel (1 serializacao por opcao, pin de byte-igualdade), facet de municipio como slugs rotulados no cliente (-20KB), facets no mesmo Promise.all (seed por promise), catalogo DRY, clearMunicipalityListFilters
- timeout explicito no int test de facets (2 bundles full-scope sob suite 47-way)
A revisão paralela do /simplify achou três defeitos funcionais na entrega
dos filtros no header, todos corrigidos aqui:
- as facetas omitiam só o param multi, então "Prioritária" escondia os
municípios não-prioritários do popover onde o próprio check mora e
"Sem assessor" esvaziava o popover de Assessores; cada faceta passa a
omitir o grupo de params do seu popover (int test cobre os dois casos);
- "todas as tendências" tinha duas codificações — as três marcadas
deixavam trend= inútil na URL com o funil se declarando inativo; o
parse agora canonicaliza o conjunto completo para ausente;
- o filtro de Território havia sumido do mobile ao virar multi.
Também devolve o "Limpar" aos popovers curtos (estava preso ao limiar da
busca) e registra o que sobrou como B16+ (escala/DRY), com o
CampaignListEmptyState compartilhado e o firstValue duplicado absorvidos
no C8, e a fragilidade do build sem o global metadata no site público.
Popover de filtro por coluna no desktop (multi-seleção OR em município,
território, assessores e tendência; busca acento-insensível e "Limpar"),
mantendo os selects empilhados no mobile. Header sticky com a página como
scroller e empty state que substitui só as linhas de conteúdo, preservando
overview e filtros. Remove a coluna "Assessoria": a ordenação foi para
"Assessores" e o com/sem assessor virou opção do mesmo popover.
Achado do /simplify pós-entrega (findByID por município ao atribuir um
território de identidade ou zona eleitoral inteira). Sem precedente para
o fix natural (payload.find com id: { in }) e mudaria semântica de erro
para IDs inexistentes; fica como gatilho no plano em vez de item de
roadmap.
Lista+detalhe única de edição para coordinator/candidate: nome, e-mail e
celular com auto-save debounced (edit-where-you-see-it); carteira de
municípios por chips com agregação de território de identidade,
adição/remoção com UI otimista; criação inline no topo da tabela; ação de
WhatsApp por assessor; reset de senha via e-mail (bloqueado para
placeholders @planilha.invalid). Alinha access de update/phone/atribuição
de município a isCampaignUnrestricted (coordinator + candidate).
Sem migration. Testes int (campaignAdvisorManagement) + unit (schema e
portfólio de municípios) cobrindo access, conflito de e-mail único, cap de
assessores por município e agregação por território/ZE.