teqo-backup/docs/roadmap.md
Francisco Ignácio Fontoura Solla da4ca2c713 Deliver B37: editable município chips on the dobradinhas list.
The "Municípios" column of /campanha/dobradinhas reuses
MunicipalityPortfolioCell unchanged, writing the inverted side of the
relation (municipality.stateDeputies) through a single transactional
batch action (setStateDeputyMunicipalitiesBatch): per-município advisory
lock, all-or-nothing on a partial-scope batch instead of a silent partial
apply, no floor (a dobradinha can hold zero municípios). stateDeputiesArraySchema's
cap moved 20 → 435 to fit a whole território batch from the município side.

The post-implementation critique found two P1s, and both were fixed instead
of deferred. First: MunicipalityPortfolioCell and LeadershipStateDeputyRelationCell
had grown the same chip-editing machine twice — optimistic delta, undo, ARIA
1.2 combobox, floor/ceiling, pointer-fine/coarse split — with incompatible
contracts. This was the 3rd call site B34+ was waiting for, so RelationChipCell
is now the single engine and both cells shrank to thin domain wrappers (chip/
search building, FormData, copy — nothing interaction-specific left in either).
LeadershipStateDeputyRelationCell also gained an ownerName prop and its first
dedicated test suite, which it shipped without before this.

Second: an advisor could see, and attempt to remove, a município chip outside
the portfolio they administer, failing silently at the document level.
MunicipalityPortfolioCell now filters the visible chips by addableIds instead
of only gating what can be added, so an advisor sees and can only touch
municípios they administer.

No migration, collection, or Consent. Docs updated: plan as-built, roadmap,
AGENTS.md, project notebook.
2026-07-28 09:34:39 -03:00

142 KiB
Raw Blame History

Roadmap — Teqo

Atualizado em: 2026-07-28 (B37 ✓ chips editáveis de municípios na lista de dobradinhas — a coluna "Municípios" de /campanha/dobradinhas troca o número morto (row.municipalityCount) pela mesma peça do B34 ✓ (shared/MunicipalityPortfolioCell, sem cópia, sem provider novo), escrevendo do lado invertido (municipality.stateDeputies) por uma única action de lote (setStateDeputyMunicipalitiesBatch, transacional, lock por município, tudo-ou-nada — sem "aplica o que pode"), sem piso mínimo e sem assertMunicipalitiesWithinScope (canUpdateMunicipality já escopa o assessor); o cap de stateDeputiesArraySchema subiu de 20 para 435 para caber um lote de território inteiro do lado do município. O critique pós-implementação achou dois P1 e a resposta foi generalizar na hora, não adiar: MunicipalityPortfolioCell e LeadershipStateDeputyRelationCell (B31 ✓/B36 ✓) tinham a mesma máquina de interação (delta otimista, undo, combobox ARIA, piso/teto, split pointer-fine/coarse) implementada duas vezes com contratos incompatíveis — em vez de esperar um "3º call site" hipotético, este item era o 3º call site, então src/components/campaign/shared/RelationChipCell.tsx nasceu como o motor único e as duas células viraram wrappers finos (chips/busca/FormData/copy por domínio); isso fecha adiantado o gatilho useChipRowOverflow que o B34+ havia registrado. O segundo P1 — um assessor via e tentava remover chips de município fora do escopo, falhando em silêncio no nível do documento — fechou escondendo o chip em vez de só explicar: MunicipalityPortfolioCell agora filtra por addableIds os municípios que renderiza, não só os que oferece para adicionar. LeadershipStateDeputyRelationCell ganhou ownerName e sua primeira suíte de testes própria (zero cobertura antes); gate completo (tsc/lint/format/cycles limpos, unit 651/651, int 452/452, Aikido 0 findings) · plano; B17+ novo fill-in de engenharia registrado no fechamento do B17 — esconder uma coluna não desliga a query que existe só para ela, e o botão "Colunas" some com a tabela no estado vazio de três rotas; B17 ✓ seletor de colunas — entregue como capacidade do CampaignTable, nas sete superfícies do sistema de listas em vez de só em municípios; persistência em cookie (campaign_columns) com filtragem no servidor, porque CampaignTableColumn.cell é função e o array de colunas não atravessa a fronteira server/client — a coluna oculta some também do payload RSC; label: string virou obrigatório e o seam defaultVisible morreu (era true em 10 colunas e false em nenhuma: guardar só o conjunto de ocultas é o que faz coluna nova nascer visível, contrato que E9 ✓/E14 ✓ assumiram); uma coluna mandatory por superfície, refiltrada no servidor; o bug real pego na revisão foi o payload de um toggle chegando dentro da janela de debounce do seguinte e commitando o conjunto sem ele; a segunda rodada de revisão mudou a cadência por medição (fechar o menu é o commit, timer de 3 s como rede que grava só o cookie, sem refresh — os 400 ms agrupavam rajada, não ritmo de leitura, e qualquer timer que repintasse recriaria o mesmo custo mais devagar, re-pagando os ~229 ms / 371 statements de /campanha/liderancas, P1 no TECH-DEBT; sessão que termina onde começou não repinta, e desmontar com o menu aberto grava pelo cleanup) e apagou duplicação com dono: contentKey virou o 4º chamador de sameIdSet (generalizado p/ string | number), e os label de municípios/dobradinhas passaram a citar os módulos de rótulo que o header já usa, o que fez o id das colunas de municípios ser tipado contra MunicipalityListColumnId (id, descrição do B22 ✓ e rótulo do picker viraram a mesma chave; membro nivel → level, alinhado ao id e ao ?level=) · plano); B32+ ✓ escala e DRY pós-B32 — a máquina de auto-save dos controles de célula virou useCampaignCellAutosave (um valor só: a auditoria mostrou que display/draft eram o mesmo valor com dois nomes) e as cinco rotas JSON de /campanha passaram a nascer de campaignJsonMutationRoute, com guard de convenção — a verificação de mesma origem deixou de depender de disciplina de cópia; a região aria-live saiu de dentro do overlay (fechar é o que dispara a gravação, então ela desmontava junto) e o variant="sheet" da casca ganhou slot footer, o que fez o controle de Sinal (B26 ✓) largar o Popover/Drawer próprios · plano; 2026-07-27 (E14 ✓ níveis de envolvimento N0–N4 — municipality.engagementLevel staff-only (sem default: nulo é "sem nível", e filtrar por ele é a fila de triagem), motivo e sinais de reversão obrigatórios, e o primeiro escritor de produção do allocationDecision (C12 ✓), gravado na mesma transação sob lock porque o "nível anterior" do snapshot precisa ser verdade; enum de outcome estendido com movimento; histerese do §6.8 (salto duplo só com choque triangulado, 3 semanas de proteção, um movimento por mês) num módulo puro que roda no cliente e no servidor, com a rota respondendo blocked (409) com as violações tipadas e o override registrado com as regras que contrariou; submit explícito em vez de auto-save (exceção prevista pela regra, e o controle não vira o 4º call site do B32+); coluna + filtro nativo (?level=) e sort em memória na lista (o Postgres põe NULL primeiro no DESC, e sem backfill isso entregaria "N4 primeiro" cheio de "Sem nível") + verbete no E18 · plano; a triage do fechamento não abriu ID novo — os três débitos caíram no B32+: o 5º controle/rota JSON tirou F2/F3 da condicionalidade (a política de mesma origem que isSameOriginRequest diz possuir está copiada em cinco rotas, e a que esquecer a linha falha aberta) e o slot footer que o B42 ✓ havia adiado virou F5, porque na folha "Fechar" está pinado e "Registrar movimento" fica abaixo da dobra; fill-in dimir o resultado no compositor de giro — a troca de TI em /campanha/atividades/giros sinaliza só no seletor; a triage do fechamento do E13 mediu que as peças de pending não são acopladas a lista e o /simplify as havia dispensado por isso · plano; E13 ✓ planejador de presença/giros — elegibilidade de visita em 5 condições ✓/— (volume, headroom, rede, janela, encaixe) no detalhe do município, fase do calendário (construcao|consolidacao|ativacao, âncoras ilustrativas em src/lib/visitPlannerAnchors.ts) e compositor em /campanha/atividades/giros gerando activity em rascunho com deputyPresent numa transação; duas premissas do plano caíram na auditoria — kind: 'visita' nunca entrou na migration do C12 (e não entra: deputyPresent já É o marcador de presença) e allocationDecision não tem write path em src/ (o "não vá" é aviso textual; gravar o override vira gatilho do E11); "encaixe em giro" = mesmo TI, porque não existe grafo de contiguidade em runtime; o teto-do-campo-não-capturado e as medianas do catálogo saíram do classificador do E10 para municipalityPotential.ts para que card e coluna "Classe" não pudessem divergir; a action relê os ids das paradas com overrideAccess: false dentro da transação (o create de activity é booleano de staff — sem isso um assessor semeava o estado inteiro), buraco achado por int test; sem migration, sem collection, sem Consent, sem entidade tour · plano; B42 ✓ polimento mobile da lista de municípios — Drawer em Tendência/Assessores/Votos estimados pela casca CampaignCellEditOverlay, card compacto e clicável por link esticado; a triage do fechamento não abriu ID novo: F4 entrou no B32+ (região aria-live que desmonta com o overlay) e o débito de bundle do catálogo no B33+ deixou de ser condicional (gatilho disparado por B41 ✓ e B42 ✓) · plano; B34+ fill-in de engenharia pós-B34 registrado — a entrega custou +58 kB de First Load JS em /campanha/liderancas e +42 kB em /campanha/assessores (medido contra main limpo; a maior parte é a célula, não o catálogo), o gatilho de extração do useChipRowOverflow disparou (~105 linhas idênticas entre as duas células de chips da mesma tabela) e os três módulos de delta de membership colapsam num só; o passe localizado que fechou o B34 já derrubou o custo de CPU medido (chips 4,2 → 0,043 ms quente; busca 4,59 → 0,077 ms por tecla), e a segunda rodada somou três fases que mudaram o maior lever — revalidatePath por toggle (374 statements / 229 ms por clique), índice de municípios sem cache no degrau 2 da escada, e o filter por linha que derrota o próprio WeakMap — mais o N+1 de access pré-existente promovido a P1 no TECH-DEBT (367 dos 371 statements da rota), que é quem precisa cair primeiro · plano; B34 ✓ chips editáveis de municípios na lista de lideranças — célula sempre editável em pointer-fine + Drawer em pointer-coarse, AdvisorMunicipalityCell promovido a shared/MunicipalityPortfolioCell com Assessores migrado no mesmo PR, action única de lote (a de um id só, inclusive a de assessor, foi deletada), anti-goal de input sempre montado emendado para relações por chips; corrigiu de passagem o seam description de CampaignTableHead (B22 ✓), que quebrava qualquer chamador RSC · plano; B41 ✓ scroll horizontal + sticky Município · plano; B32+ novo fill-in de engenharia registrado no fechamento do B32 — MunicipalityListExpectedVotesControl/MunicipalityListTrendControl/LeadershipListSupportStatusControl são o 3º call site da mesma máquina de auto-save por debounce (estado/refs/save/schedule/flush quase idênticos), gatilho que os três planos B24/B27/B32 já haviam registrado; auditoria primeiro, extração condicional (precedente B33+) — plano; B32 ✓ editar Status de apoio na célula da lista de lideranças — LeadershipListSupportStatusControl (Popover + NativeSelect, sempre montado, sem toggle "Editar" de tabela inteira) grava por POST /campanha/liderancas/support-status (auto-save ~150 ms, sem revalidatePath), reusando isSameOriginRequest/campaignJsonMutationRoute.ts já extraídos pelo B24 ✓/B27 ✓; fecha o "Adiado com gatilho" do B29 — plano; B33+ ✓ useCampaignListFilterNavigation — o scaffold de navegação/debounce sai dos três filtros de lista para um hook em shared/ (281 linhas removidas contra 75 em src); a auditoria derrubou a premissa do plano de que os três domínios teriam assinaturas de canonicalização diferentes — buildStateDeputyListHref/buildTerritoryListHref/buildMunicipalityListHref já canonicalizam internamente, então o parse(...) explícito de dobradinhas/territórios era trabalho redundante e um único callback toHref cobre os três; a entrega achou dois bugs de comportamento, não de estrutura: tocar um filtro no meio da digitação descartava a busca pendente em dobradinhas e territórios (unificado na semântica de munícipios por navigateWithSearch), e o setTimeout do debounce capturava o state do render que o agendou, então um sort clicado dentro da janela de 1s era revertido um segundo depois — munícipios escapava por acidente, pelo key= da página que remonta e mata o timer (corrigido com stateRef, com caso de spec verificado vermelho nas três listas sem o fix); o /simplify ainda achou o fallback useCampaignListPending() ?? useTransition() copiado em 5 lugares, com o hook novo como 6º → useCampaignListTransition, e um aria-live que fazia o resumo de filtros de munícipios ser a terceira região viva da mesma página; shouldUpdateMunicipalitySearchUrl orfanou e foi deletado; tests/unit/campaignListFilterNavigation.unit.spec.ts (jsdom + router mockado + fake timers, 8 casos × 3 listas) é a primeira cobertura desses componentes — plano; C8 F4b ✓ guard de formActions ampliado para irmãos *FormActions.ts + taskActions.ts; 6 ladders migradas; taskActions.ts allowlisted · plano; Build resiliente ao global metadata ausente ✓ — resolveSiteMetadata em src/utilities/seo.ts; fallback NEXT_PUBLIC_SITE_URL e omit canonical/OG quando não há URL; 4 rotas + layout; plano; B8+ ✓ escala e DRY pós-B8 F2 — a política de escala de chão das malhas commitadas virou código nomeado (scripts/lib/topology.mjs, tolerância de Visvalingam em m² e dígitos de quantização derivados do bbox de cada topologia), e com ela a malha das ZE caiu de 480 para 30,2 pontos/feature (72.310 → 7.687 bytes) sem que bahia-municipalities mudasse um byte; a auditoria corrigiu a alavanca do plano (quantização sozinha quase não corta — o peso absoluto manda) e rejeitou um item do F4 por medição (city/zoneNumber de MunicipalityZoneNeighborhoodEntry são evidência de proveniência hasheada, não código morto); MAX_POINTS_PER_FEATURE = 60 é o novo tripwire, featureMapKey passou a ser exercido sobre features reais das três malhas (417 codarea, 27 code, 19 slug), o mapa degrada em vez de morrer quando o chunk das zonas falha, e a ressalva de aproximação saiu do gate de comparação · plano; users.roles ✓ — admin|editor em users, migration 20260727_032523_add_users_roles, editor publica post/tag/media; unificação com campaignUser rejeitada · plano; 2026-07-26 (fill-in remover lista "Municípios por zona eleitoral" do Início — pós-B8 F2 ✓ os polígonos tornam a lista textual redundante; dropar UI + zoneBreakdown do bundle; ressalva de aproximação fica na malha · plano; B8 F2 ✓ polígonos das 19 ZE de Salvador — artefato bahia-municipality-zones.topo.json (72 KB) por dissolve bairro→zona do BA_bairros_CD2022 do IBGE contra a RA 02/2017 do TRE-BA, com curadoria explícita e um passe de maior fronteira compartilhada validado; o coroplético passou a ser keyed por map key (slug na zona, ibgeCode no resto) e o polígono municipal de Salvador ficou como base sem interação por baixo, porque duas malhas simplificadas independentemente não fecham na mesma costa; competitiveRank segue por cidade (o artefato TSE só tem federalRankByIbgeCode) com int pinando o fato; desambiguar a ZE no card "Onde estou" do B14 ✓ virou débito com gatilho, não dependência — plano; B8+ novo fill-in de engenharia com os quatro débitos do /simplify do B8 F2, cujos relatórios chegaram depois do merge — a malha de 19 zonas ficou com a mesma contagem de vértices da malha dos 417 municípios (as duas constantes de simplificação são relativas ao bbox e foram copiadas verbatim, o que vale ~3,9 m de grade em Salvador contra ~103 m no estado), a regra da map key está em três formas sem nenhum teste ligando artefato → featureMapKey → catálogo, uma falha de chunk nas zonas passou a derrubar o mapa inteiro, a ressalva de aproximação desaparece no modo comparação, e getTerritoryFeature perdeu o último consumidor de produção neste commit · plano; B39–B42 registrados: sessão "Lembrar de mim" + login biométrico WebAuthn + scroll horizontal/sticky na lista de municípios + polimento mobile da mesma lista — B39, B40, B41, B42; E12 ✓ camada TI — rollups MAUP, colunas Cobertura/Captura/Classe em /campanha/territorios, benchmark intra-TI no MunicipalityGoalAccountCard; plano; B36 ✓ coluna "Lideranças" em /campanha/dobradinhas — chips + Popover+Command + delta reuse da mutação B31; LeadershipStateDeputyRelationCell compartilhada (direction/measureOverflow); columns factory (não Context); casca dobradinhas/formActions.ts; loadLeadershipOptions + leaderships[] no row; plano; B30 ✓ convite de completar cadastro por WhatsApp na lista de lideranças — LeadershipInviteRowAction (UserPlusIcon) na coluna actions ao lado do contato do B28 ✓; Popover two-step reusa createCampaignInvite/autopreenchimento; disable por normalizeBrazilianPhone; plano; B31 ✓ coluna de dobradinhas na lista de lideranças entregue — chips clicáveis → /campanha/dobradinhas/<slug> em /campanha/liderancas, edição por Popover+Command (mecânica do B27 ✓) com escrita por server action direta do cliente (useTransition, padrão do B19 ✓) em vez do endpoint JSON do B27, porque aqui há derivados a revalidar (leadershipCount, a ficha do deputado); setLeadershipStateDeputyMembership(Record) em transação + lock leadership-state-deputies:{id} + revalidatePath; helper puro nextStateDeputyIdsAfterMembership espelha nextAdvisorIdsAfterMembership; catálogo entregue uma vez por tabela via leadershipColumns(stateDeputyOptions) (columns factory, mesmo precedente de municipalityListColumns) em vez de Context; é o 2º call site do padrão Popover+Command+chips — extração para shared/ continua esperando o 3º (B34/B36/B37, que agora consomem a mutação/lock em vez de correr para desenhá-los); plano; fill-in ✓ limitar pan/zoom do mapa à Bahia — BahiaMap com maxBounds/maxBoundsViscosity: 1 sobre BAHIA_BOUNDS.pad(0.1) e minZoom via getBoundsZoom com o mesmo padding 16px do fitBounds, recalculado no ResizeObserver existente; único arquivo; plano; (B14 ✓ município mais próximo entregue — card cliente "Onde estou" no Início staff resolvendo point-in-polygon sobre o chunk de geometria que o mapa do dashboard já carrega (a premissa "polígonos explodem o bundle" do plano de 24/07 caiu na medição: 41 KB gzip já estão no browser, então o artefato de centroides e seu script saíram de escopo), com fallback nomeando onde o assessor está e o município mais próximo da carteira dentro de 150 km; a posição nunca sai do aparelho (sem action, sem Consent); First Load JS de /campanha 265 → 268 kB depois de mover o href das zonas de Salvador para o servidor (zoneCityHrefs) — importar buildMunicipalityListHref no cliente custava 21 kB por um link, porque arrasta bahiaTerritories + municipalityCatalog; plano; B38 ✓ sidebar recolhido em tablet entregue — offcanvas + trigger md+ + cookie/matchMedia; plano; B33+ novo fill-in de engenharia registrado no fechamento do B33 — StateDeputyFilters é o 3º call site do scaffold de navegação/debounce que TerritoryFilters/MunicipalityFilters já tinham, achado do /simplify rodada 2, plano; B28 ✓ e-mail + celular na lista de lideranças entregue — colunas E-mail/Celular com copy-on-click + coluna de Ações com WhatsApp, no padrão de /campanha/assessores (B19 ✓); shared/CampaignCopyableCell nasce compartilhado já no 2º call site (AdvisorsTable migrado no mesmo PR), whatsAppHrefForPhone promovido a lib/phone.ts, loadLeadershipDetail deduplicado; plano; B24 ✓ auto-save + justificativa no popover de Tendência de /campanha/municipios — sem botão "Salvar", POST /campanha/municipios/political-trend, plano; B13 ✓ escala relativa no mapa do Início entregue — quantis como novo default, padrão próprio/LQ nas âncoras do E10 e posição no município a partir do artefato TSE v3 (federalRankByIbgeCode), mais bolhas opcionais por válidos projetados coloridas pela classe; computeAggregateTerritorialClass nasce aqui e o E12 deve herdá-lo em vez de escrever um rollup próprio; TERRITORIAL_CLASS_ANCHORS mudou de casa para src/lib/territorialClassAnchors.ts; plano; B13+ novo fill-in de engenharia com os quatro débitos do /simplify do B13 — teste de concordância entre as duas escritas do LQ (mapa cliente × classificador server-only, a rede de segurança do E15), escopo geográfico declarado no readout (posição é da cidade inteira, classe é do escopo do usuário), uma varredura da fatia TSE por ano no script de agregados em vez de três, e a tupla no competitiveRankByYear que corta ~metade dos ~63,6 KB somados ao payload RSC · plano; débito de teste afinado no ledger (TECH-DEBT.md, P3 → P2): campaignMunicipalities/campaign-pwa são flaky com 2 workers em main também — clique no checkbox de consentimento antes da hidratação e regex de fixture em que "Conde" casa com "Condeúba"; B27 ✓ combobox + chips + auto-save no popover de Assessores de /campanha/municipios entregue — a checkbox list + botão "Salvar assessores" virou chips removíveis + Command com gravação por delta, nextAdvisorIdsAfterMembership movido para src/lib/, endpoint POST /campanha/municipios/advisors espelho do expected-votes/, plano; B23 ✓ tooltip de conteúdo nas células das listas entregue — a mecânica genérica (cellTooltip, CampaignCellTooltip, CampaignHoverTooltip em shared/) já tinha saído de graça no E10 ✓; este item fechou openOnTouch/disabled no primitivo (mais um terceiro comportamento do /simplify: content falsy renderiza o trigger sem wrapper) e os dois consumidores de /campanha/municipios (nomes de assessores + justificativa da tendência), plano; B22 ✓ explicação por coluna no header das listas entregue — description?: ReactNode em CampaignTableHead/MunicipalitySortableHead (rename de tooltip, sublinhado pontilhado), campaignConceptOneLiner como fonte única para colunas cobertas pelo E18 ✓, municipalityColumnDescriptions cobrindo as 11 colunas de /campanha/municipios; sem migration/action, plano; 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; C8 F4b ✓ (entregue 2026-07-27); B26 ✓ registrar sinal na célula "Último sinal" — plano; 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; 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; 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; B33 ✓ ordenação e filtro no header da lista de dobradinhas (entregue 2026-07-26) — ?sort=/?dir= por Nome/Partido + Popover multi-seleção de Partido com "Sem partido", consumindo o chrome compartilhado do B21 ✓ (a corrida de ordem de chegada com B29 foi encerrada); paridade mobile (StateDeputyFilters) entrou por decisão desta sessão; ordem dos nulos de party pinada por int test; contagens derivadas (municipalityCount/leadershipCount) seguem sem sort/filtro, como travado, plano; 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; 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; B30 ✓ convite de completar cadastro por WhatsApp na lista de lideranças — LeadershipInviteRowAction na coluna actions (entregue 2026-07-26), plano; 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; B28 ✓ e-mail + celular na lista de /campanha/liderancas (entregue 2026-07-26) — colunas E-mail/Celular com copy-on-click + coluna de Ações com WhatsApp como em /campanha/assessores (B19 ✓); CampaignCopyableCell nasce compartilhado já no 2º call site (AdvisorsTable migrado no mesmo PR), whatsAppHrefForPhone promovido a lib/phone.ts; edição in-list adiada; sem migration, plano; B27 ✓ combobox + chips + auto-save no popover de Assessores de /campanha/municipios (entregue 2026-07-26), plano; B26 ✓ registrar sinal na célula "Último sinal" (entregue 2026-07-25), plano; 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; B25 ✓ link do território na lista de municípios (entregue 2026-07-26) — coluna "Território" + card mobile → /campanha/territorios#ti-<slug>; territoryAnchor.ts + TerritoryLink; seam rowId em CampaignTable; plano; B24 ✓ auto-save + justificativa editável no popover de Tendência de /campanha/municipios (entregue 2026-07-26), plano; 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; B23 ✓ tooltip de conteúdo nas células das listas (entregue 2026-07-26), plano; fill-in opções selecionadas no topo nos Popovers de filtro do header — helper puro reutilizável pelas demais listas, plano; B22 ✓ explicação por coluna no header das listas (entregue 2026-07-26), plano; 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; B20 ✓ Municípios prioritários no Início entregue 2026-07-25 — atalho ranqueado por déficit + markup sem bullets, plano; 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/ + notebook .cursor/rules/projects/nucleos-eleitorais.mdc.

Âncoras do calendário eleitoral 2026 (Res. TSE 23.760/2026)

Data Marco Consequência para o produto
20/07–05/08 Convenções partidárias Vertical remodelada já em produção; smoke + onboarding do time no modelo novo
15/08 Prazo final de registro de candidaturas TSE publica candidaturas 2026 → destrava A6 (dobradinha)
16/08 Início da propaganda eleitoral Base nominal com dados reais + inteligência mínima (E8–E9, C12) operando antes desta data
09–23/10 Propaganda gratuita rádio/TV Congelar mudanças arriscadas (~20/09)
04/10 1º turno GOTV (C5) — confirmação de comparecimento da base nominal
25/10 Eventual 2º turno Chapa majoritária; Solla decidido no 1º turno

Princípios e decisões travadas

  • Ownership de audiência e portabilidade de dados; módulos reutilizáveis em outros contextos políticos.
  • Um único app Next.js: site (frontend), admin (payload), /campanha (campaign). Sem Rust separado. Vercel por enquanto.
  • Doações só via CTA QueroApoiar (apoiar.me/jorgesolla). Pessoa = Contact + joins — nunca cadastro paralelo.
  • Município = unidade operacional pré-definida (remodelagem 2026-07-23, em produção): 435 entradas — um por município da Bahia, exceto Salvador (19 Municípios-zona, ZE 1–19); Camaçari é município inteiro (ZE 170/171 agregadas); geografia seedada e read-only. Substitui Praça/Núcleo Eleitoral — plano-mestre remodelagem-municipios.md.
  • Roles: coordinator ("Coordenador Geral"), advisor ("Assessor"), leader ("Liderança") e candidate ("Candidato", visão irrestrita); assessor vê só os municípios que administra; liderança em lockdown (só a ferramenta de contatos de apoiadores). Assimetria de votos: staff registra declaredVotes e estima em 3 cenários (A10); liderança nunca vê estimativas. Sem disparo em massa (Res. TSE 23.610 art. 33 / Meta). Comunicação 1:1 interna staff↔lideranças via bridge WhatsApp não oficial = programa D3–D5 (não é Business API nem blast).
  • Padrões de engenharia vigentes (hardening 2026-07-23): access explícito em toda collection, zero lint warnings, ban as never, knip no CI, caching ladder — regra .cursor/rules/engineering-standards.mdc; débitos no ledger TECH-DEBT.md.

Onda 0 — caminho crítico para dados reais

Textos provisórios de Consent + /privacidade auto-provisionados (onda-0.md); hold de PII real até o lote jurídico final. Inalterada pela remodelagem (mesmas chaves Consent).

  1. Lote jurídico único (externo) — textos finais (substituem provisórios) + base LGPD art. 11: lideranca-autopreenchimento, apoiador-cadastro, apoiador-intencao-voto, campanha-notificacoes-push, campanha-whatsapp-canal (D3 — armazenamento/processamento do canal interno staff↔lideranças; se o lote já tiver fechado sem esta chave, entra no próximo passe com a assessoria — não abrir rodada jurídica só por ela), Aviso de Privacidade, avaliação RIPD.
  2. Smoke pós-deploy (desbloqueado — deploy da remodelagem aplicado em produção em 2026-07-23) — NEXT_PUBLIC_SITE_URL HTTPS, login /campanha, município de teste; checklist no AGENTS.md.
  3. Ativação com dados reais assim que (1) liberar — lideranças/apoiadores reais e import em massa.
  4. Onboarding do time — usuários coordinator/advisor, primeiros municípios assumidos, treino de campo; B19 /campanha/assessores (entregue 2026-07-24 — CG/candidato sem /admin); seed da planilha de prioridades via E4R (engenharia pronta — pnpm db:seed:projecao; aplicar em produção após smoke — plano); rede/lideranças (nomes) só após o lote jurídico.

O0+ (escala/DRY pós-Onda 0) não bloqueia jurídico nem smoke fictício — plano.

Já entregue (resumo)

  • Era Núcleos (2026-07-15 → 2026-07-20) — MVP + Ciclo 2 (auth campaignUser, território A1/A2, baseline TSE A3/A4, overview B1, share C1, PWA D1, geometrias B2, Leaflet B3), C2 apoiadores (eng.), C3 agenda, C6–C11 escala, E1+E3 metas/estratégia, E2 série TSE 2014/2018/2022, A5 conversão/classificação/alavancagem/mobilização, A7 F1–F2, A8 perfis IBGE, fill-ins (reset senha/perfil, visitados recentes, Field Desk polish). Infra e padrões (locks, transações, consent por chave, shells, mapa, dados eleitorais) reaproveitados pela remodelagem; superfícies e modelo de Núcleo substituídos.

  • Plataforma — local Postgres + guards, migrations baselined, posts/tags do site público com cache posts, Onda 0 textos provisórios + /privacidade; toolchain TS6 (tooling) + TS7 (typecheck).

  • Site público (2026-07-21) — Pixel do Meta nos abaixo-assinados (plano).

  • Remodelagem Praças → Municípios (R0–R5 2026-07-21; M1–M5 2026-07-23; em produção 2026-07-23) — modelo Município (435 entradas; Salvador 19 zonas; Camaçari inteira), role candidate, demandas staff-only, lockdown da liderança (ferramenta de contatos, source: lideranca), dobradinhas stateDeputy + /campanha/dobradinhas, mapa no Início; migrações destrutivas da vertical aplicadas em produção via build Vercel. Planos: remodelagem-pracas.md → remodelagem-municipios.md.

  • Mapa e lista (2026-07-21, na era Praças; identificadores renomeados na M1) — A9 total esperado (plano) + A9+ loader compartilhado (plano); B6 setStyle incremental (plano); B7 mapa filtrado (plano); B9 edição rápida (plano); B10 hover/tap (plano); B11 escala % dos válidos (plano); B12 fit ao footprint filtrado (plano); B8 F1 catálogo bairros das zonas (plano — hoje Salvador-only, entradas de Camaçari removidas na M1); fill-ins filtros-auto (plano) e C8 F4 (plano).

  • A10 (2026-07-23) — cenários pessimista/média/otimista em votePledge.estimatedVotes e municipality.expectedVotes; agregação por cenário (default média); seletor no mapa/overview; liderança segue com um declaredVotes (plano). Desbloqueou E8.

  • Admin Payload (2026-07-23) — export CSV de assinaturas e contatos (plano).

  • Hardening de engenharia (2026-07-23, Fases 0–6) — access lockdown (PII/CMS admin-only + gates de rota do leader), tooling gate (knip no CI, --max-warnings=0, ban as never), artefato TSE commitado + cache (bahiaElectionAggregates, election-tse, loadMunicipalityScope), pending honesto + Suspense streaming, state scoping, splits (src/utilities/access/*, supporterImport.ts, shells DRY). Tracker: IMPROVE-CODE-QUALITY-PLAN.md · ledger: TECH-DEBT.md · mapa de testes: TESTING.md.

  • Consolidação de engenharia Pass 2 (2026-07-25, W0–W5 + D3) — sistema de listas da campanha (colunas como dado em CampaignTable, URL state compartilhado, 8 superfícies migradas, B16+ absorvido; B17/B18 ficaram triviais — só falta a UI de toggle/salvar), split do municipalityUi, fronteiras lib/utilities zeradas + server-only em 21 loaders + components/campaign em 15 domínios (decisão: sem src/domains/), electionInsights deletado (E10 nasce limpo), knip exports em ERROR no CI (0 mortos), tabs de detalhe extraídas, runCampaignFormAction fecha C8 F4, copy Praça→Município varrida, consent do WhatsApp por chave estável (D3). Tracker: IMPROVE-CODE-QUALITY-PLAN.md § Pass 2 · arquitetura: ARCHITECTURE.md.

  • E4R (2026-07-24) — import único da planilha de projeção → municipality.expectedVotes (Bom→otimista, Regular→média, Mínimo→pessimista) + priority (pnpm db:seed:projecao, always-overwrite, dry-run + runbook; Salvador pulado; zero PII). No mesmo dia o grupo duplicado voteGoals ("Meta Bom/Regular/Mínimo") foi removido do app (migration 20260724_133600 com backfill metas→estimativas) — a única série por cenário é expectedVotes. Plano: import-planilha-projecao.md. Seed local verificado (189 estimativas / 50 alta); produção após smoke.

  • E17 (2026-07-24) — tabela comparativa dos 27 Territórios de Identidade no Início staff (/campanha): territoryOverview.ts (rollup puro client-safe: computeTerritoryRollup + sortTerritoryRows) + loadTerritoryOverview.ts (loader server-only, overrideAccess: true) + TerritoryOverviewTable.tsx (tabela densa, ordenação client-side default % da própria votação desc, Metropolitano decomposto em Salvador 19 zonas × Demais RMS, linha→/campanha/municipios?region=<TI>). Somas/razões apenas (salvaguarda MAUP); sem migration/collection. Primeira fatia de E12. Plano: tabela-ti-inicio.md.

  • C12 (2026-07-24) — registro-fundação: versions nativas em votePledge com baseline; sinais tipados em municipalityUpdate; origem do actionPlan; demandas vinculadas com custo derivado e criação múltipla atômica; allocationDecision ex-ante imutável. Migration 20260724_180000_add_campaign_foundation_records; testes int + E2E. Plano: registro-fundacao.md.

  • E8 (2026-07-24) — conta da cadeira: artefato TSE commitado estendido para v2 (campoFederalVotesByYear, federalTallyByYear, majoritarian2022 presidente/governador T1, via pnpm build:election-aggregates); curadoria src/lib/campoParties.ts (ano→siglas do campo, validada contra electionPartySpectrum); global campaignGoals (meta estadual 150 mil + margem, access staff-read/coordinator-write, migration 20260724_180000_add_campaign_goals_global); municipalityPotential.ts (válidos projetados, teto do campo, captura, share intracampo, roll-off 2022-only, meta sugerida + sanity por TI) e goalCoverage.ts (meta = expectedVotes[cenário] ?? suggestedGoal; comprometido = Σ pledges, nunca a expectativa da mesa); encaixado no dashboard, na visão geral e na lista de municípios (nova coluna "Cobertura da meta"), e num novo card "Conta da cadeira" no detalhe. Plano: conta-da-cadeira.md. Desbloqueia E9, E10, E12, E13 (e melhora B13/E16). (A fórmula da meta sugerida foi revista no mesmo dia, durante o E9 — ver abaixo.)

  • B19 (2026-07-24) — gerenciar assessores em /campanha/assessores (lista + novo + detalhe): criar/editar contas advisor, carteira de municípios (auto-save por delta), reenviar link de senha; só CG/candidato (isCampaignUnrestricted); access de update/phone e atribuição municipality.advisors alinhados ao candidate; leitura privilegiada de e-mail no loader (reloadUnrestrictedActor); sem migration. Plano: gerenciar-assessores.md.

  • E9 (2026-07-24) — fila de alocação na própria lista de municípios (sem rota nova, sem coluna nova, sem migration). Corrigiu antes a meta sintética do E8, que repartia os 150 mil proporcionalmente só ao teto do campo e por isso dava meta 2.911 a Vitória da Conquista (5.005 votos em 2022) e 813 a Campo Formoso (47) — ordenar por déficit sobre ela colocaria desertos no topo. deriveSuggestedGoalsByScenario (substitui decomposeStateGoal) ancora a meta na votação própria de 2022 por cenário: pessimista = base×(1−margin), central = base, otimista = base×(stateGoal ÷ Σ base), com clamp; goalCoverage passou a receber SuggestedGoalByScenario. Na fila: lastPledgeAt no agregado de pledges, lastSignalAt = máx(lastUpdateAt, pledge) no view model, sort keys deficit (novo default do staff) e frescor (cenário central fixo), frescor dobrado na coluna "Última atualização" (frio ≥ 21 dias), badge "sem responsável" em prioridade alta sem assessor, "coluna da vergonha" no detalhe da métrica de assessoria (?priority=alta&coverage=sem_assessor) e copy Praças→Municípios. Cortes: votos em jogo → B13, LQ/captura → E10, coluna dedicada de déficit → desnecessária. Plano: fila-de-alocacao.md. Desbloqueia E11.

  • E16 (2026-07-25) — dossiê do município (pré-agenda, pedido O6): aba ?tab=dossie staff-only no detalhe compondo os loaders existentes (municipalityDossierData.ts + MunicipalityDossier.tsx) — capa com data de geração, série TSE/rank (A11), conta da cadeira (E8), rede de lideranças com frescor, conjuntura/dobradinhas/encaminhamentos, agenda (actionPlan), sinais recentes (C12), perfil IBGE (A8 — reentrada do artefato, caveat para zonas); caps 8/5/3+2 com "ver tudo"; campo staff-only municipality.budgetNotes ("Emendas aportadas", G11 manual-first, migration 20260725_022155); visão print via CSS (chrome oculto, shells destravados, tipografia A4 — 1–2 páginas). Plano: dossie-municipio.md.

  • B26 (2026-07-25) — registrar sinal na célula "Último sinal" de /campanha/municipios: Popover (desktop) / Drawer (mobile) cria municipalityUpdate kind: 'sinal' sem sair da fila; submit explícito; revalidateMunicipalityListPaths; sem migration. Plano: registrar-sinal-lista-municipios.md.

  • E10 (2026-07-25) — classificação territorial relativa: municipalityTerritorialClass.ts, função pura sobre o artefato TSE commitado (sem query, sem campaignGoals, sem user, sem migration), classificando os 435 municípios em Reduto · Expansão · Manutenção · Marginal · Sem base — vocabulário do research, que substitui o Defesa/Ataque/Indecisa/Perdida herdado do A5-2. Eixos: LQ contra o padrão estadual do próprio candidato (strongLq 2 / weakLq 0,5), importância na própria votação, teto do campo não capturado contra a mediana do catálogo e pertencimento ao bloco que soma 50% da votação (impede que um município grande com baixa penetração leia como "Marginal"). getStatewideFederalTotals foi extraído/memoizado em bahiaElectionAggregates.ts (municipalityVoteRank passou a reusá-lo). Superfícies: badge + "por quê" no card "Conta da cadeira" (E8), capa do dossiê (E16, com print CSS para o badge não achatar em cinza) e coluna nova "Classe" na lista com sort (?sort=classe) e filtro multi (?class= repetível) — o filtro é derivado, então força o caminho limit: 0 e se aplica também ao escopo da visão geral e às facetas, senão totalDocs mentiria. Dois verbetes novos em /campanha/conceitos (E18). Cortes exatos são ilustrativos (calibração = E15); NEC/competição ficou fora com gatilho. Plano: classificacao-territorial-relativa.md. Desbloqueia B13 e a parte de classes do E11.

  • B16 (2026-07-25) — filtros no header da lista de municípios: MunicipalityHeaderFilter (funil + Popover ao lado do sort do B15) com multi-seleção OR em município, território, assessores e tendência, busca acento-insensível e "Limpar"; mobile mantém os selects empilhados; header sticky com a página como scroller e empty state que troca só as linhas, preservando visão geral e filtros; coluna "Assessoria" removida (ordenação foi para "Assessores", com/sem assessor virou opção do popover e o alerta "sem responsável" do E9 passou a morar na célula de assessores). Plano: filtros-no-header-lista-municipios.md.

  • B20 (2026-07-25) — Municípios prioritários no Início staff: seção deixa de truncar 6 prioritárias sem critério; amostra de 8 ordenada por déficit de cobertura (E9/central) via pickDashboardPriorityMunicipalities; highPriorityCount no Badge; CTA “Ver todas” → /campanha/municipios?priority=alta; markup list-none. Plano: municipios-prioritarios-inicio.md.

  • B38 (2026-07-26) — sidebar recolhido em tablet: CampaignSidebar com collapsible="offcanvas", SidebarTrigger no inset md+, defaultOpen a partir do cookie sidebar_state, colapso padrão 768–1023 via CampaignSidebarViewportDefault quando não há cookie; mobile Sheet inalterado. Plano: sidebar-recolhido-tablet.md.

  • B13 (2026-07-26) — escala relativa no mapa do Início: o coropleto era quase monocromático porque 0–100% dos válidos comprime uma disputa cujo topo estadual é 4,71%. Três escalas relativas em classes discretas — quantis (novo default, degradando para 3 classes com escopo < 10 municípios e declarando isso na legenda), padrão próprio (LQ) nas mesmas âncoras do E10 e posição no município — mais uma camada opcional de bolhas por válidos projetados, coloridas pela classe do E10. "Posição no município" não existia em lugar nenhum: entrou no artefato TSE commitado (v3) como federalRankByIbgeCode (colocação entre todos os candidatos com votos na geografia, 2014/2018/2022; 534 → 623 KB, orçamento 700 KB), não em query de request. computeAggregateTerritorialClass classifica um grupo de slugs como um território só (o mapa é por ibgeCode e Salvador são 19 slugs) somando insumos e rodando o mesmo classificador — o E12 herda o helper. As âncoras do E10 saíram para src/lib/territorialClassAnchors.ts para não arrastar o artefato ao bundle do cliente; a matemática das escalas vive em src/lib/mapScaleClasses.ts. Legenda discreta com faixas reais, readout que nunca mostra a classe sem o porquê, dois conceitos novos em /campanha/conceitos. Rank indisponível em 2026 (dado TSE); sem migration; bundle client de /campanha inalterado (o payload RSC cresceu ~63,6 KB — B13+ F4). Plano: escala-relativa-mapa.md; débitos do /simplify: escala-dry-pos-b13.md.

  • B8 F2 (2026-07-26) — polígonos das 19 ZE de Salvador e re-keying do coroplético. Salvador era um polígono carregando os votos da cidade, então o mapa não respondia "qual pedaço de Salvador está fraco / é meu". Novo artefato commitado bahia-municipality-zones.topo.json (72 KB, ~20 KB gzip, 19 features) gerado por pnpm build:municipality-zone-geometries a partir do BA_bairros_CD2022 do IBGE (shpjs como devDependency — o recorte só existe em SHP), dissolvendo bairro→zona pela RA 02/2017 do TRE-BA: 170/170 polígonos atribuídos, 19/19 zonas, curadoria em 3 tabelas explícitas no script (aliases, bairro sem polígono próprio, bairro que a resolução parte) mais um passe de maior fronteira compartilhada para os 5 que a resolução nunca nomeia — regra validada contra os 4 que ela nomeia sob outro rótulo. A chave do mapa virou map key (slug na zona, ibgeCode no resto): bundle, rollups, navegação e classe territorial re-keyed, e o ramo kind: 'zones' do readout morreu. O polígono municipal de Salvador ficou desenhado por baixo como base sem dado e sem pointer, porque duas malhas simplificadas independentemente não fecham na mesma costa. competitiveRank continua por cidade (o artefato só tem federalRankByIbgeCode, o que a legenda já dizia — int pina que as 19 dividem uma posição). Sem migration, sem collection, sem action. Plano: poligonos-pracas-zona.md; débitos do /simplify (relatórios chegaram depois do merge): escala-dry-pos-b8f2.md.

  • E14 (2026-07-27) — níveis de envolvimento N0–N4 e o primeiro escritor de produção do allocationDecision (C12 ✓). Três campos staff-only em municipality (engagementLevel sem default, levelNote, levelChangedAt derivado por hook) na migration 20260727_161752_add_municipality_engagement_level, que também acrescenta movimento ao enum de outcome — um movimento de nível não é "aceita" nem "descarta". Sem backfill: o campo nasce nulo em 435 municípios, porque carimbar N2 afirmaria uma decisão que ninguém tomou; "sem nível" é o estado honesto e filtrar por ele é a fila de triagem. Mover é decisão de realocação, então só coordinator/candidate (field access própria + reloadUnrestrictedActor); o assessor lê e propõe por sinal. A action grava o município e a decisão na mesma transação, sob o lock municipality-engagement-level:{id} — sem ele o "nível anterior" do snapshot é chute. A histerese do relatório §6.8 (salto de 2 níveis só com choque triangulado, 3 semanas de proteção pós-promoção, um movimento por mês, este derivado só de levelChangedAt) vive num módulo puro e client-safe, então o Popover mostra o motivo antes de submeter e o servidor revalida; a rota responde blocked (409) com as violações tipadas, e o override entra no snapshot com exatamente as regras que contrariou. Submit explícito, não auto-save — a exceção que campanha-edit-where-you-see prevê para flows com nota/confirmação, e que mantém o controle fora do 4º call site do B32+. Na lista: 11ª coluna staff com badge outline numerado, filtro ?level= nativo (o campo é armazenado; "sem nível" vira exists: false num ramo or) e sort nivel em memória — o Postgres põe NULL primeiro no DESC, então o sort nativo entregaria "N4 primeiro" com uma página de "Sem nível"; sortByNullableValue manda o nulo para o fim nas duas direções, como sem_base em classe —, mais o verbete no /campanha/conceitos (E18). Plano: niveis-de-envolvimento.md.

  • B32+ (2026-07-28) — escala e DRY pós-B32, em cinco fases e três premissas corrigidas pela auditoria. Do lado do servidor, campaignJsonMutationRoute(config, handler) passou a exportar o POST das cinco rotas JSON de /campanha já com origem verificada, corpo parseado e catch mapeado — a fase existia porque isSameOriginRequest estava copiada em cinco arquivos e a rota que esquecesse a linha falharia aberta, então o helper proposto pelo plano (mais uma linha que se pode esquecer) foi rejeitado em favor do wrapper, acompanhado de um guard de convenção que impede a 6ª rota de nascer fora. O bodySchema.parse roda dentro do mesmo try de propósito: é o que preserva a mensagem de hoje para corpo inválido. Do lado do cliente, postCampaignJson cobre só o transporte e useCampaignCellAutosave absorve a máquina dos três controles de debounce (B24/B27/B32) — com um valor, porque display e draft eram sempre setados juntos (o par que o plano tratava como a dificuldade central era o mesmo valor com dois nomes), com flush() idempotente como caminho único de save e o hook dono do open, já que fechar É o commit. Duas fases de casca: a região aria-live mudou-se para fora do Popover/Drawer (era anunciada por um nó que o fechamento — o próprio gatilho da gravação — desmontava), e o variant="sheet" ganhou slot footer, o que tirou "Registrar movimento" de baixo da dobra no controle de nível (E14 ✓) e permitiu ao Sinal (B26 ✓) largar o Popover/Drawer que só mantinha por causa do rodapé; formId na casca acabou desnecessário (<button form={id}> é HTML padrão). Sem migration, sem collection, sem action, sem mudança de contrato de rede. Plano: escala-dry-pos-b32.md.

Próximos — Campanha (/campanha)

Programa Inteligência de campanha (E8–E16, B13, C12 · adjacentes A11/E17/E18)

O discovery literatura→persona→entrevista (relatório aprovado; compêndio com ~67 fontes em docs/research/) fixou o kernel: a disputa de DF é conta de quociente fragmentada; o gargalo é converter lealdade de campo em voto nominal município a município via rede; % estadual absoluto é anti-métrica. A sessão real com o Coordenador Geral (2026-07-23 — CUSTOMER.md) confirmou as apostas centrais (fogo amigo intra-PT como ameaça nº 1; canal = ZAP sem registro datado; "coluna da vergonha" validada — Salvador cobrado 10×) e calibrou as âncoras: a leitura relativa da mesa é % da própria votação (concentração da captura própria — critério rígido de prioridade dele), não % do eleitorado local; a restrição dominante é "perna"/estrutura, não dinheiro; piso projetado 150 mil (2022: 129k). O produto deve entregar inteligência, não planilha chique: metas derivadas, leitura relativa, fila de decisão priorizada, sugestões dado→decisão com humano no loop, registro ex-ante auditável. Plano-mestre: inteligencia-campanha.md (incl. gaps de dados G1–G11 e desenho canônico da fila).

ID Fatia Plano Entrega essencial Classe Appetite Janela Depende de
E10 Classificação territorial relativa detalhes ✓ (entregue 2026-07-25) Reduto/Expansão/Manutenção/Marginal por LQ + importância + teto do campo; coluna, sort e filtro na lista B ~1,25d 3 E8 ✓
B13 Escala relativa no mapa detalhes ✓ (entregue 2026-07-26) Quantis (default) + LQ nas âncoras do E10 + posição no município (artefato v3); bolhas por válidos projetados coloridas pela classe B ~3d 3 E8 ✓, E10 ✓
E14 Níveis de envolvimento N0–N4 detalhes ✓ (entregue 2026-07-27) engagementLevel staff-only com histerese e sinais de reversão; movimento grava allocationDecision na mesma transação B ~1,25d 3 C12 ✓
E11 Motor de sugestões v1 detalhes 8 padrões (P1/P2/P3/P5/P6/P7/K-A/K-B), menu com estatuto, aceitar/descartar → allocationDecision, triagem 1–5 C ~3d 3 E8 ✓, E9 ✓, E10, C12
E12 Camada TI detalhes ✓ (entregue 2026-07-26) Rollups MAUP + cobertura/classe/captura em /campanha/territorios; benchmark intra-TI no card Conta da cadeira; gatilhos T = E11 fase 2 B ~1,5d 3 E8 ✓
E13 Planejador de presença/giros detalhes Elegibilidade (5 condições ✓/—), fases construção/consolidação/ativação, compositor de giro, visita pedida×justificada C ~1,5d 3 E8 ✓, C12
E15 Backtest pós-eleição detalhes Pledge (trajetória) vs. resultado por zona; calibração de limiares A ~1d pós-04/10 C12, TSE 2026

Demais itens abertos

  • E4R import único da planilha de projeção — entregue 2026-07-24 (pnpm db:seed:projecao; overwrite-always; plano)
  • A11 posição em votos do município (rank absoluto + % da própria votação) + ordenação da lista por votação — entregue 2026-07-24 (helper municipalityVoteRank; baseline + coluna lista; default ?sort=votos desc; header=2022; consome contrato B15) · plano
  • ~E17 tabela comparativa dos Territórios de Identidade no Início (pedido do candidato) · somas/razões apenas, Metropolitano decomposto (salvaguardas MAUP) · 1d · Janela 1–2 · primeira fatia de E12 · plano — entregue 2026-07-24 (territoryOverview.ts + loadTerritoryOverview.ts + TerritoryOverviewTable.tsx; rollup puro client-safe, loader overrideAccess: true, Metropolitano decomposto, ordenação client-side; sem migration)
  • E12 camada TI (rollups MAUP + benchmark intra-TI) — entregue 2026-07-26 (estende E17/B21: colunas Cobertura/Captura/Classe em /campanha/territorios, loadTerritoryOverview com bundle E8 + computeAggregateTerritorialClass, captura como razão dos agregados com decomposição no tooltip, T4 no MunicipalityGoalAccountCard; territoryIntraCaptureBenchmark.ts; conceitos E18; sem migration) · plano
  • C12 registro-fundação — entregue em código 2026-07-24 (versions, sinais, origem/custo derivado, decisões ex-ante; migration + testes) · plano
  • C13 renomear "Plano de Ação" → "Atividade" (entidade, código, banco e rotas) — entregue 2026-07-25 (collection activity, 4 tabelas + 3 enums + 2 colunas + 39 índices + 14 FKs + 2 sequences renomeados por migration data-preserving escrita à mão 20260725_213000_rename_action_plan_to_activity, que também alinhou os 8 índices fósseis %plaza% da remodelagem; rota /campanha/atividades, sidebar "Atividades", rótulos de campo "Tipo/Origem/Resultado da atividade"; guard de vocabulário table-driven em codebaseConventions.unit.spec.ts varrendo src+tests+scripts) · plano
  • A6 dobradinha 2026 automática quando o TSE publicar candidaturas · gatilho externo: pós-15/08 · camada de insight sobre o registro operacional stateDeputy (M4) · alimenta E13 · plano
  • B5 F2–F3 cache CLI compartilhado + factory mun/TI (scripts continuam) · plano
  • B8 F2 polígonos dos Municípios-zona de Salvador (ZE 1–19) — entregue 2026-07-26 (artefato bahia-municipality-zones.topo.json por dissolve IBGE↔TRE RA 02/2017; coroplético keyed por map key slug|ibgeCode; polígono municipal de Salvador mantido como base sem interação; rank competitivo segue por cidade com int pinando o fato) · débitos com gatilho no plano: ZE exata no card "Onde estou" (B14 ✓) e rank por ZE · plano
  • C5 operação dia D / GOTV (validar com produto) · design Dia-D-GOTV · depende de C2 dados reais
  • D2 push + sino in-app · soft: chave campanha-notificacoes-push (Onda 0) · plano
  • Programa WhatsApp interno (D3–D5) · baixa/média prioridade · 1:1 staff↔lideranças via bridge não oficial · número pessoal de cada assessor/CG (QR no app, parece chat pessoal) · não Business API / não massa / não chip institucional · fecha Little Hire · gatilho honesto (2026-07-24): "ZAP é o campo" é fato confirmado — D3 entra se os deltas do ZAP não estiverem sendo absorvidos pelo fluxo sede-digita (C12) após uso real · plano-mestre whatsapp-interno-campanha.md
    • D3 fundação multi-sessão (QR por campaignUser + log + Consent campanha-whatsapp-canal) · plano
    • D4 envio 1:1 pela sessão do ator · depende de D3 · plano
    • D5 inbox da própria sessão → rascunhos (municipalityUpdate/demanda) com humano no loop · depende de D3 · plano
  • R6 critique/polish visual da vertical remodelada (ciclo /impeccable completo por superfície; smoke visual coordenador feito em 2026-07-21) · absorve os débitos de produto/UX remanescentes de FD2 (field-desk-ux-pos-critique.md): glossário inline (O3 — hipótese ainda sem evidência), triagem em lote, empty states de coordenador · gatilho: antes de 16/08
  • B38 sidebar recolhido em tablet — entregue 2026-07-26 (collapsible="offcanvas" no CampaignSidebar; SidebarTrigger no inset md+; defaultOpen lê cookie sidebar_state; CampaignSidebarViewportDefault colapsa uma vez em 768–1023 sem cookie; mobile Sheet inalterado; sem migration) · plano
  • B14 ✓ município mais próximo (geolocalização → card "Onde estou" no Início staff, entregue 2026-07-26) · pede permissão 1× por sessão se ainda não concedida; matching client-side por point-in-polygon sobre o chunk de geometria do mapa (centroides só ordenam o fallback de 150 km na carteira); Salvador multi-zona → lista filtrada; B8 F2 ✓ (2026-07-26) desenhou os polígonos das ZE, então resolver a zona exata passou a ser possível, mas o card não mudou — segue como débito com gatilho "pedido de campo" no plano do B8 · plano
  • B20 Municípios prioritários no Início — entregue 2026-07-25 (pickDashboardPriorityMunicipalities + amostra 8 por déficit central; Badge com total; “Ver todas” → ?priority=alta; list-none na strip) · plano
  • B21 página própria dos Territórios de Identidade — entregue 2026-07-25 (/campanha/territorios staff-only; 27 TIs + decomposição do Metropolitano; sort/filtros/busca canônicos na URL; drill para municípios; chrome extraído em shared/CampaignSortableHead + CampaignHeaderFilterPopover; painel de TI removido do Início por decisão da sessão) · sem migration/collection/action · plano
    • Downstream atualizado: B25 teve a dependência dura atendida; B29 e B33 não pagam mais a extração do chrome compartilhado e agora implementam apenas política/URL/filtros dos próprios domínios.
  • B15 ordenar lista de municípios pelo header da coluna — entregue 2026-07-24 (?sort=/?dir= na URL + clique no header (desktop) / select compacto (mobile); ordenação global no filtrado; A11 consome o contrato (key votos)) · plano
  • B16 filtros no header das colunas da lista de municípios — entregue 2026-07-24 (funil+Popover no TableHead ao lado do sort B15; barra slim busca+resumo+Limpar; mobile selects empilhados; optimistic + busca no Popover de TI) · plano
  • B22 explicação por coluna no header das listas — entregue 2026-07-26 (description?: ReactNode em CampaignTableHead e MunicipalitySortableHead — rename de tooltip, sublinhado pontilhado no label, mesmo CampaignHoverTooltip compartilhado já promovido pelo E10 ✓; campaignConceptOneLiner em campaignIntelligenceConcepts.ts como fonte única para as colunas cobertas pelo E18 ✓; municipalityColumnDescriptions cobre as 11 colunas de /campanha/municipios, staff e leader; sem migration/action) · plano
  • B23 tooltip de conteúdo nas células das listas — entregue 2026-07-26 (a mecânica genérica — cellTooltip?: (row) => ReactNode em CampaignTableColumn, CampaignCellTooltip, CampaignHoverTooltip promovido para shared/ — já tinha saído de graça no E10 ✓; este item fechou o que faltava: openOnTouch/disabled em CampaignHoverTooltip para conviver com Popover sem roubar o tap nem sobreviver atrás dele (mais um terceiro comportamento do /simplify: content falsy renderiza o trigger sem wrapper), formatAdvisorNamesTooltip, e os dois consumidores de /campanha/municipios — hover na célula "Assessores" mostra os nomes completos (avatares só levavam iniciais, ambíguas a partir do 3º), hover na célula "Tendência" mostra a justificativa (politicalTrend.note, semeada pelo E4R ✓, antes só um <input type="hidden">); em touch a informação passou a aparecer dentro do próprio Popover de edição (a nota da tendência vira leitura acima do select); sem migration) · plano
  • B24 auto-save + justificativa no popover de Tendência da lista de municípios — entregue 2026-07-26 (MunicipalityListTrendControl sem "Salvar": status ~150 ms / justificativa 600 ms + flush blur/fechamento; Textarea no lugar do hidden/<p> read-only; endpoint POST /campanha/municipios/political-trend sem revalidatePath; fecha o adiado "nota no popover" do B9 ✓ e o anti-goal do princípio 4; soft B23 ✓/B27 ✓) · plano
  • B25 link do território na lista de municípios — entregue 2026-07-26 (TerritoryLink na coluna/card de /campanha/municipios → /campanha/territorios#ti-<slug>; src/lib/territoryAnchor.ts; CampaignTable.rowId + âncoras em TerritoryList; escopo A — só lista) · plano
  • B26 registrar sinal a partir da célula "Último sinal" — entregue 2026-07-25 (Popover desktop / Drawer mobile; municipalityUpdate kind: 'sinal'; submit explícito; revalidateMunicipalityListPaths; sem migration) · plano
  • B27 combobox + chips + auto-save no popover de Assessores da lista de municípios — entregue 2026-07-26 (chips removíveis + Command de busca acento-insensível + gravação por delta, sem botão "Salvar"; nextAdvisorIdsAfterMembership subiu para src/lib/municipalityAdvisorMembership.ts; endpoint POST /campanha/municipios/advisors espelha expected-votes/; assignMunicipalityAdvisorsFormAction/replace do array sobrevive só para /editar) · plano
  • B28 e-mail + celular na lista de lideranças — entregue 2026-07-26 (em /campanha/liderancas: colunas E-mail e Celular com o mesmo funcionamento de leitura da tabela de assessores — B19 ✓ — clique copia (toast), "—" se vazio, celular com máscara BR, mais uma coluna de Ações com ícone WhatsApp (wa.me) por linha quando o número normaliza; LeadershipRowViewModel.email populado sem query nova e loadLeadershipDetail deduplicado; primitivo de copy extraído para shared/CampaignCopyableCell.tsx já no 2º call site — AdvisorsTable migrado no mesmo PR — e whatsAppHrefForPhone promovido a lib/phone.ts; CampaignTable permanece (sem clonar AdvisorsTable). Fora: toggle "Editar" / debounce in-list (Contact update + locks = Adiado no plano); sem migration/collection/Consent/action de escrita) · plano
  • B30 convidar para completar cadastro por WhatsApp na lista de lideranças — entregue 2026-07-26 (LeadershipInviteRowAction na coluna actions ao lado do WhatsApp de contato do B28 ✓; UserPlusIcon + Popover two-step com createCampaignInvite/kind: 'autopreenchimento'; disable por normalizeBrazilianPhone; sem migration/action nova) · plano
  • B29 ordenação e filtro no header da lista de lideranças — entregue 2026-07-28 — pedido de produto (2026-07-25): levar para /campanha/liderancas o que a lista de municípios ganhou em B15 ✓ (ordenar clicando no header, ?sort=/?dir= na URL) e B16 ✓ (filtro multi-seleção em Popover no header, com busca, chips e "Limpar"). Hoje o estado da URL é só q+page e a ordem é fixa em -updatedAt — que, com o seed do E4R ✓, é a ordem em que a planilha gravou as fichas. Filtros v1 = as colunas visíveis (Status de apoio, Setor, Município com facet no escopo do ator, Acesso ao app), entrando as colunas "Setor" e "Última atualização" (ambas já no view model, hoje não renderizadas) para não haver ordenação invisível; ordenação no banco, incluindo nome por join contact.name (o adapter resolve caminho pontilhado em buildOrderBy) — sem desnormalizar nome na leadership (segunda fonte de verdade de pessoa é anti-goal do AGENTS; se o join não sair, corta-se o sort por nome, não a normalização). Corrige de passagem o teto mudo de limit: 200 na pré-resolução de contatos da busca. É o 3º call site do header rico (municípios ✓ · B21 · este), o gatilho que o B21 registrou: extrai CampaignSortableHead + CampaignHeaderFilterPopover para shared/ (só chrome — estado/optimistic/hrefs ficam nos wrappers de domínio) e migra municípios no mesmo PR; dá a B22/B23 um único lugar onde pousar. Sem migration/collection/Consent/action · ~1,5–2d · Janela 1–2 · Impeccable B · soft: B21 (quem entrar 2º duplica; B29 migra os três), B28 (mesma tabela — colunas de contato ficam sem head rico), B22/B23 (primitivo), B17/B18 (colunas já são dado; URL nasce salvável), E14 (níveis pousam aqui) · cortável · plano
  • B31 coluna de dobradinhas na lista de lideranças — entregue 2026-07-26 (coluna Dobradinhas entre Organizações e Acesso ao app: chips clicáveis → /campanha/dobradinhas/<slug>, edição por Popover+Command — mesma mecânica do B27 ✓, com escrita por server action chamada direto do cliente via useTransition, padrão de AdvisorMunicipalityCell/B19 ✓, não o endpoint JSON do B27, porque aqui há derivados a revalidar; setLeadershipStateDeputyMembership(Record) em transação + acquireTextAdvisoryLocks + revalidatePath em /campanha/liderancas, /campanha/liderancas/<id> (id concreto — /simplify rodada 2 trocou o template dinâmico), /campanha/dobradinhas e a ficha do deputado tocado; helper puro nextStateDeputyIdsAfterMembership espelha nextAdvisorIdsAfterMembership; catálogo (loadStateDeputyOptions, agora com plainName/party/slug) entregue uma vez por tabela via leadershipColumns(stateDeputyOptions) (columns factory, mesmo precedente de municipalityListColumns — /simplify rodada 2 substituiu o Context inicial); MAX_LEADERSHIP_STATE_DEPUTIES exportado; loadLeadershipDetail deduplicado. É o 2º call site do padrão Popover+Command+chips — extração para shared/ continua esperando o 3º) · plano
  • B32 editar Status de apoio na célula da lista de lideranças — entregue 2026-07-26 (LeadershipListSupportStatusControl: Popover + NativeSelect sempre montado na coluna "Status" de /campanha/liderancas, sem toggle "Editar" de tabela inteira — mesmo anti-goal já travado no B19 ✓/B28/B31 ✓; auto-save por debounce ~150 ms via POST /campanha/liderancas/support-status (2º/3º endpoint JSON da campanha — isSameOriginRequest e o shell campaignJsonMutationRoute.ts já existiam por B24 ✓/B27 ✓, este item só importa), reusando updateLeadershipInternal sem revalidatePath; extraiu leadershipStaffEditSafeMessages (completado nos irmãos liderancas/formActions.ts/nova/formActions.ts pelo /simplify) e leadershipLabels.ts (agora fonte única também para SupportStatusBadge); fecha o "Adiado com gatilho" que o B29 registrou) · plano
  • B33 ordenação e filtro no header da lista de dobradinhas — entregue 2026-07-26 (?sort=/?dir= por Nome/Partido + Popover multi-seleção de Partido com "Sem partido", consumindo o chrome compartilhado do B21 ✓ sem extrair de novo; stateDeputyListUrl.ts/stateDeputyListFilters.ts espelham municipalityListUrl.ts/territoryListUrl.ts; paridade mobile — StateDeputyFilters — entrou por decisão desta sessão; ordem dos nulos de party pinada por int test, não assumida; municipalityCount/leadershipCount seguem sem sort/filtro, como travado) · plano
  • B34 chips editáveis de municípios na lista de lideranças — entregue 2026-07-27 (a coluna "Municípios" de /campanha/liderancas virou chips editáveis: cada município navega para /campanha/municipios/[slug], um chip de território (MapPinIcon + contagem) aparece quando a liderança cobre o TI inteiro, e a célula é sempre editável em ponteiro fino — "×" no canto do chip só no hover/foco, input de busca no fim da linha — enquanto em ponteiro grosso o toque em qualquer ponto da célula abre um Drawer com "×" de 44 px, porque um polegar rolando a tabela não pode ser convidado a distinguir "abrir o município" de "apagar o vínculo". O pedido literal ganhou, com o anti-goal emendado, não reaberto: PRODUCT.md princípio 3 e .cursor/rules/campanha-edit-where-you-see.mdc passaram a admitir input sempre montado para relações por chips em pointer-fine (o input É o readout; um Popover por linha seria um clique a mais para um alvo já na tela) — B28/B31 ✓/B32 ✓ seguem valendo: nada aqui é "modo de tabela", e o toggle de AdvisorsTable foi renomeado para "Editar nome e contato", que é tudo o que ele ainda destrava. Divergências do plano, todas medidas: AdvisorMunicipalityCell foi promovido a shared/MunicipalityPortfolioCell (o plano rejeitava — mas com a célula sempre visível a máquina de ResizeObserver/"Ver mais…" da origem passou de peso morto a requisito, e /campanha/assessores migrou para a peça compartilhada no mesmo PR, então o 2º call site nasceu junto); sem provider de contexto (índice + addableIds viajam por props da factory de colunas, montada uma vez pelo RSC); uma única action de lote (setLeadershipMunicipalitiesMembership, municipalityIds repetidos + assigned) — a gêmea de um id só nunca nasceu e a de assessor (setAdvisorMunicipalityMembershipRecord + schema) foi deletada, com os int tests migrados para a de lote; LeadershipRowViewModel.municipalityNames → municipalityIDs: number[] (nome/slug resolvidos no cliente contra o catálogo); piso de 1 com aria-disabled + razão no toast em vez de disabled, porque disabled esconde do teclado a única cópia da explicação; teto de 30 recusado no cliente antes do apply otimista, para um lote de TI não voltar inteiro com uma mensagem que não diz qual limite estourou. /impeccable fechou dois P0/P1 reais: as sugestões eram só de mouse (agora contrato ARIA 1.2 completo — setas/Home/End/Enter, aria-activedescendant, listbox portalizado com nome próprio, e-mail e "nenhum resultado" fora do listbox) e o auto-save era mudo (aria-busy + dim + live region, rollback só do delta que falhou, e undo em remoção de lote); mais alvo de 24 px no "×" via before:-inset-1 (SC 2.5.8), links dos chips restaurados dentro do Drawer e borda do "×" com contraste real. Bug latente encontrado e corrigido de graça: o seam description de CampaignTableHead (B22 ✓) quebrava qualquer chamador RSC — campaignHoverTooltipAlign/campaignHoverExplanationClassName moravam no módulo 'use client', então o servidor chamava uma referência de cliente; foram para src/lib/campaignHoverTooltip.ts e a lista de lideranças é o primeiro header com explicação em RSC. Sem migration/collection/Consent/mudança de URL) · plano
  • B37 chips editáveis de municípios na lista de dobradinhas — entregue 2026-07-28 (a coluna "Municípios" de /campanha/dobradinhas troca row.municipalityCount pela mesma peça do B34 ✓ — shared/MunicipalityPortfolioCell, sem cópia, sem provider novo — escrevendo do lado invertido municipality.stateDeputies por uma action de lote única (setStateDeputyMunicipalitiesBatch, transacional, lock por município, tudo-ou-nada), sem piso mínimo e sem assertMunicipalitiesWithinScope (canUpdateMunicipality já escopa o assessor); cap de stateDeputiesArraySchema 20 → 435 para caber um lote de território do lado do município. O critique achou dois P1 e a resposta foi generalizar na hora: MunicipalityPortfolioCell e LeadershipStateDeputyRelationCell (B31 ✓/B36 ✓) duplicavam a mesma máquina (delta otimista, undo, combobox ARIA, piso/teto, split pointer-fine/coarse) com contratos incompatíveis — este item era o 3º call site que o B34+ esperava, então shared/RelationChipCell.tsx nasceu como motor único e as duas células viraram wrappers finos; fecha adiantado o useChipRowOverflow do B34+. O segundo P1 — chip de município fora do escopo do assessor, removível na UI mas recusado em silêncio pelo servidor — fechou escondendo o chip (addableIds agora filtra o que a célula renderiza, não só o que oferece adicionar), não só explicando a falha. LeadershipStateDeputyRelationCell ganhou ownerName e sua primeira suíte de testes própria; gate completo (unit 651/651, int 452/452, Aikido 0 findings). Sem migration/collection/Consent/mudança de URL) · plano
  • B34+ escala e DRY pós-B34 — fill-in de engenharia com os débitos que sobreviveram ao /simplify do B34 ✓, cujos relatórios de perf e reuso chegaram depois do fechamento. O achado que manda é medido, não estimado: a entrega custou +58 kB de First Load JS em /campanha/liderancas (246 → 304 kB) e +42 kB em /campanha/assessores (253 → 295 kB), e a decomposição importa — assessores já importava o catálogo estático da Bahia, então os +42 kB de lá são a célula em si (Drawer, links, contrato de combobox, máquina de medição) e só os ~16 kB restantes em lideranças são o catálogo chegando a uma rota que não o tinha; é o mesmo custo que o B14 ✓ já havia nomeado. As duas saídas se opõem (tirar o catálogo do browser engorda o payload de todo toggle; encolher o payload para {id,slug} mantém o bundle), e o plano recomenda a segunda por ser staff que edita muito e navega pouco — com a ressalva registrada de que isso troca o region do banco pelo do catálogo, que é snapshot-testado. Também entram: useChipRowOverflow (o gatilho de extração que o próprio B34 nomeou disparou e já foi fechado — o B37 ✓ não extraiu só a medição de overflow, generalizou a célula inteira em shared/RelationChipCell.tsx, então MunicipalityPortfolioCell e LeadershipStateDeputyRelationCell não têm mais 105 linhas duplicadas, têm zero), um só nextRelationMembershipIds para os três módulos de delta (generalização verificada exata, não aproximada — ainda aberto, o B37 não tocou os módulos de delta/action, só a célula) mais um withLeadershipRelationDelta local para as duas actions gêmeas e adjacentes, caminho só-ids no loader de lista de assessores, e o Combobox do base-ui — que ao contrário do cmdk não está descartado, porque ui/combobox.tsx já aceita anchor e tem hook data-chips. O passe localizado que fechou o B34 já derrubou o custo de CPU medido (chips 4,2 → 0,78 ms frio e 0,043 ms quente; busca 4,59 → 0,077 ms por tecla) e não volta ao plano. A segunda rodada de revisão somou três fases e mudou qual é o maior lever: cada toggle de chip dispara o revalidatePath da própria lista e o Next re-executa a página dentro da resposta da action — 374 statements e 229 ms por clique, mais ~44 kB de payload (F7); o índice de municípios é dado de referência sem cache lido em duas rotas e de novo a cada re-render, e ~10 dos seus 12 ms são o transform por documento do Payload, não a query (F8 — degrau 2 da escada, unstable_cache + tag); e o ramo do assessor entrega uma chave nova ao WeakMap por linha (municipalityIndex.filter), derrotando em 25 linhas a memoização que a própria entrega criou (F9). Duas correções de registro vêm junto: o payload-mínimo recomendado acima poupa ~22 kB crus / 3,75 kB gzip mas nenhum tempo de servidor — a escolha é payload-contra-bundle, não ganho de servidor; e o custo real da página não é nada disto, é o N+1 de access pré-existente que o TECH-DEBT agora carrega em P1 (367 dos 371 statements de /campanha/liderancas são a mesma leitura de campaign_user), então atacar F7/F8 antes dele é esforço mal alocado · ~1,5d · Janela 1–2 · soft: B37 ✓ (pluga a mesma célula numa 3ª relação — entregue 2026-07-28 generalizando para shared/RelationChipCell.tsx em vez de só copiar a máquina; fecha o useChipRowOverflow acima) · cortável (exceto o P1 de N+1 de access, que já não é cortável) · plano
  • B36 coluna de lideranças na lista de dobradinhas — entregue 2026-07-26 (coluna Lideranças em /campanha/dobradinhas: chips clicáveis → /campanha/liderancas/<id>, edição por Popover+Command com escrita por server action + useTransition, reusando sem alteração setLeadershipStateDeputyMembership/leadershipStateDeputyMembershipSchema/lock leadership-state-deputies:{id}/nextStateDeputyIdsAfterMembership do B31 ✓; LeadershipStateDeputiesCell promovido a shared/LeadershipStateDeputyRelationCell bidirecional (direction + measureOverflow — clamp 3 linhas + "Ver mais…" no lado do deputado; lado liderança passa false); columns factory stateDeputyColumns(..., leadershipOptions) — não Context; casca própria dobradinhas/formActions.ts; loadLeadershipOptions; StateDeputyRowViewModel.leadershipCount → leaderships: {id,name}[] na mesma query batch. Sem migration) · plano
  • B17 seletor de colunas (mostrar/ocultar) — entregue 2026-07-28, e nas sete superfícies do sistema de listas, não só em municípios: o picker nasceu como capacidade do CampaignTable (uma prop columnVisibility liga e filtra), então municípios, lideranças, dobradinhas, organizações, demandas, apoiadores e territórios ganharam a mesma barra "Colunas" no mesmo lugar, de graça. Duas premissas do plano morreram na implementação. A primeira é o localStorage: CampaignTableColumn.cell é uma função, o array de colunas não atravessa a fronteira server/client, e esconder no cliente exigiria data-column + <style> por lista — a persistência virou cookie (campaign_columns, path /campanha, um mapa listId → ids ocultos num formato compacto sem JSON/percent-encoding, porque document.cookie e o cookies() do Next discordam sobre decodificação), o servidor filtra e a coluna oculta também some do payload RSC; o custo é um router.refresh() dentro da transição compartilhada, que dima o resultado como todo filtro dessas telas. A segunda é o seam defaultVisible, que morreu: era true em 10 colunas e false em nenhuma, então o armazenamento é só o conjunto de ocultas — coluna nova nasce visível, que é o contrato que E9 ✓/E14 ✓ assumiram. Em troca, label: string virou obrigatório em CampaignTableColumn (o picker precisa de um rótulo serializável), preenchido nas ~55 definições de coluna — e como em 24 delas o head passou a só repetir o label, o /simplify tornou head opcional, com default <CampaignTableHead>{label}</CampaignTableHead>. Cada superfície declara uma coluna mandatory, que aparece no menu desabilitada com o motivo e é refiltrada no servidor, para um cookie velho não deixar ninguém sem a única célula linkada da linha. O bug que a revisão pegou é o de sempre nesta base, em roupa nova: o payload RSC do toggle A chega enquanto B ainda está na janela de debounce, e adotar o servidor ali desfazia B e commitava o conjunto sem ele — a reconciliação agora só roda com nada pendente, pinada num spec verificado vermelho contra o código anterior. Nota de operação, não de código: as duas falhas que pareciam "router.refresh() não funciona" eram a máquina em load 40 (outro worktree) — a suite passa a 1 e a 2 workers · plano
  • B18 filtros salvos na lista de municípios — nomear o estado URL atual (localStorage); Pass 2 W1 deixou trivial: serialização canônica pronta e congelada em municipalityListUrl.ts; acesso rápido de 2º nível sob Municípios no sidebar (hover desktop / expand mobile; sticky só no filtro salvo ativo) · ~1–1,5d · Janela 1–2 · soft: B16/B17 (barra) · cortável · plano
  • B19 gerenciar assessores — /campanha/assessores (lista + novo + detalhe): criar/editar contas advisor, ver/atribuir municípios, reenviar link de senha; só Coordenador Geral e Candidato (isCampaignUnrestricted); alinha access de update/phone do candidate — entregue 2026-07-24 (também alinha atribuição municipality.advisors a unrestricted; e-mail na lista via leitura privilegiada; auto-save da carteira) · plano
  • B39 lembrar de mim / sessão longa em /campanha — entregue 2026-07-27 (checkbox default off; 8 h padrão / 14 d opt-in; CampaignUser configura o teto longo e setCampaignAuthCookie re-assina JWT + cookie para o TTL escolhido; logoutCampaign revoga o sid no servidor antes de limpar cookie/PWA; smoke E2E mede a expiração longa; sem migration/Consent) · plano
  • B40 login com biometria (WebAuthn) em /campanha — após o 1º login com senha, enrollment opt-in no dispositivo; na próxima visita, platform authenticator (digital/Face ID) emite o mesmo campaign-token (TTL longo). Collection/credenciais server-side; senha permanece; migration leve · ~1,5–2d · Janela 1–2 · Impeccable C · soft: B39 (TTL/copy no login) · cortável · plano
  • B41 scroll horizontal + coluna Município fixa na lista de municípios — entregue 2026-07-27 (overflow-x-auto + sticky left-0 na coluna nome, espelho B21 ✓ TerritoryList; revisita o trade-off do B16 ✓) · plano
  • B42 polimento mobile da lista de municípios — entregue 2026-07-27 (Tendência, Assessores e Votos estimados abrem Drawer no mobile e Popover no desktop através da casca compartilhada CampaignCellEditOverlay, extraída já na entrega porque o escopo virou 3 call sites; card mais baixo com Classe e Cobertura em paridade com a tabela — os props layout dos dois viraram dead code e morreram; "Abrir município" saiu e o card inteiro virou alvo de toque por link esticado, sem JS. O critique achou um P0 no gesto central: posicionar as células do <dl> em vez dos controles levantava rótulos e padding acima do overlay e transformava metade do card em toque morto — hoje só o trigger é relative, e o e2e toca justamente num rótulo dentro da grade. Outros três defeitos de comportamento entraram no mesmo passe: o Drawer focava um campo na abertura e subia o teclado virtual sobre a folha (initialFocus no título; o preventContentAutoFocus que devia mitigar isso era inerte no branch sheet — prop renomeado para preventPopoverAutoFocus), o aria-label do trigger anunciava o verbo e engolia o valor que o pill mostra (agora "Editar tendência em X — Favorável", que de passagem tirou municipalityName de payload RSC morto no desktop), e uma gravação que falhava depois do fechamento revertia o badge em silêncio, porque fechar desmonta o Alert que carregaria o erro (toast.error só quando já fechado, nos quatro controles de auto-save). LeadershipListSupportStatusControl (B32 ✓) era o branch Popover da casca redigitado à mão e virou o 4º call site; a classe do trigger, em três arquivos, virou campaignCellEditTriggerClassName. O critique rodou sobre o código: evidência visual em device não foi coletada — alvos de toque são checados por classe, não medidos) · plano
  • E18 documentação de conceitos de inteligência de campanha — entregue 2026-07-24 (/campanha/conceitos staff-only; 7 conceitos de E8 em src/lib/campaignIntelligenceConcepts.ts agrupados em base/diagnóstico/meta; "Saiba mais" por métrica nos tooltips + link de teclado no Popover do card; formatElectionNumber passou a arredondar votos) · cada fatia futura (E9/E10/B13/E11/E12/E13/E14) acrescenta sua seção ao array como parte da própria entrega · plano

Fill-ins abertos

  • Limitar o mapa da Bahia a pan/zoom dentro do estado — ✓ (entregue 2026-07-26) BahiaMap trava pan com maxBounds (BAHIA_BOUNDS.pad(0.1)) + maxBoundsViscosity: 1 e zoom-out com minZoom via getBoundsZoom(BAHIA_BOUNDS, false, L.point(16, 16)) recalculado no ResizeObserver existente; único arquivo tocado; sem migration/props · plano
  • Renomear coluna Cobertura da meta → Cobertura em /campanha/municipios — pedido de produto (2026-07-25): header curto na fila (padrão A11 2022); definição no hover (B22) / glossário (E18 ✓). Só lista (header + card mobile + label de sort deficit); strips/card/conceitos mantêm o nome longo da métrica. Soft: B16 ✓ (saiu "Assessoria", liberou o nome) · ~0,25d · Impeccable B · cortável · plano
  • Remover coluna Tipo em /campanha/municipios — pedido de produto (2026-07-25): a coluna "Tipo" (Município × Zona eleitoral) não informa a coordenação (ambos tratados iguais); filtrar ZE = filtrar Salvador (19 slugs). Remove coluna + filtro/sort UI + param ?kind= do contrato URL da lista; schema/catálogo kind intacto (mapa, B8, dossiê) · ~0,25–0,5d · Impeccable B · soft: B15 ✓/B16 ✓ (header); B17 ganha uma coluna a menos · cortável · plano
  • Remover lista "Municípios por zona eleitoral" do Início — pedido de produto (2026-07-26): com B8 F2 ✓ cada ZE de Salvador tem polígono próprio no mapa; a lista de 19 linhas sob o painel (zoneBreakdown) é densidade morta. Remove o bloco em MunicipalityMapPanel + o campo do bundle; mantém a ressalva de aproximação atrelada à malha — B8+ F3 ✓ já a moveu para fora do gate da lista, condicionada a bundle.hasZoneMunicipalities (campo próprio, que sobrevive ao drop de zoneBreakdown), então resta apagar o <ul> + o campo do bundle · ~0,25d · Impeccable B · dura: B8 F2 ✓ · cortável · plano
  • Dimir o resultado no compositor de giro em /campanha/atividades/giros — trocar de Território de Identidade recompõe a proposta no servidor, mas hoje só o seletor sinaliza (spinner na própria descrição): a proposta do território anterior fica nítida e clicável, e o leitor de tela não recebe evento nenhum, contra o que o engineering-standards.mdc exige ("a região de RESULTADOS dima, não só o controle"). O /simplify do E13 ✓ passou por isto como "too involved — the composer isn't a list"; a auditoria do fechamento mediu o contrário — CampaignListPendingBoundary é um Provider sobre children, CampaignListResults é uma div com data-pending/aria-busy/aria-live e useCampaignListTransition já cai num transition local, então são três fios a ligar, não uma abstração a portar · ~0,25d · Impeccable B · dura: E13 ✓ · cortável · plano
  • Ícone de prioridade na lista em /campanha/municipios — trocar Badge “Prioritária” por ícone Flag + tooltip “Município prioritário” (hover) na coluna do nome · ~0,25d · Impeccable B · plano
  • 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
  • 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
  • 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
  • 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
  • B13+ escala/DRY pós-B13 — quatro fases do /simplify da escala relativa no mapa, as duas primeiras de coerência e não de custo: F1 a razão do LQ é escrita duas vezes (painel cliente :210 × classificador server-only :108, separação deliberada — o classificador arrasta o artefato de 600 KB) sem nenhum teste travando a concordância, que é a rede de segurança do E15; F2 no readout do mapa a posição vale para a cidade inteira enquanto a classe é calculada sobre o escopo do usuário, então dois assessores veem classes diferentes para o mesmo polígono de Salvador — declarar a geografia, sem mudar o cálculo; F3 pnpm build:election-aggregates varre a mesma fatia estadual do TSE 3× por ano (~225k docs extras) porque loadFederalVotesByCityZoneAndCandidate só precisa de party no select para ser superconjunto estrito dos outros dois; F4 competitiveRankByYear soma ~57 dos ~63,6 KB que o B13 pôs no payload RSC, pagos mesmo por quem nunca abre a escala — tupla [rank, candidates] corta ~metade e responde ao "profiling antes de agir" que o ledger pediu em 2026-07-23 · ~0,5–0,75d eng · Impeccable A · sem migration · cortável · plano
  • B8+ escala/DRY pós-B8 F2 — ✓ (entregue 2026-07-27) as quatro fases do /simplify dos polígonos das ZE. F1: a política de escala de chão nasceu em scripts/lib/topology.mjs (primeiro módulo de scripts/lib/ — B5 F2 pousa nele) declarando tolerância de Visvalingam em m² de chão e derivando os dígitos de quantização do bbox de cada topologia; SIMPLIFY_QUANTILE/QUANTIZE_DIGITS morreram nos dois scripts. A auditoria corrigiu a alavanca do plano: quantização sozinha quase não corta (mesmo na grade estadual as zonas ficariam em 203 pontos/feature), o peso absoluto é que manda — as zonas caíram de 9.120 → 574 pontos de arco (480 → 30,2/feature) e 72.310 → 7.687 bytes, bahia-municipalities saiu byte-idêntico e territórios mudou 1 byte (grade 102,730 → 102,822 m). Tolerância própria por malha, com o número medido no comentário: a tolerância municipal chegaria a 17,9/feature nas zonas mas comeria 5% da área de Salvador (306 → 291 km²), que é justamente a grandeza que prova que nenhum bairro sumiu. MAX_TOPO_BYTES ganhou ao lado MAX_POINTS_PER_FEATURE = 60. F2: featureMapKey passou a ser exercido sobre features reais das três malhas, cada uma no laço que já a varre inteira (417 codarea + 27 code + 19 slug) — o /simplify corrigiu a primeira escrita, que asseria a cidade dentro do laço das zonas e portanto cobria uma feature municipal 19 vezes; o teste de precedência sintético caiu e o comentário passou a declarar o invariante real (as três propriedades de chave são mutuamente exclusivas; ibgeCode na zona deliberadamente não é chave). F3: .catch() na metade das zonas — o mapa segue sem elas (com o motivo logado), com Salvador como polígono sem dado nem interação — e a ressalva de aproximação saiu do gate !comparisonActive para um <p> condicionado a bundle.hasZoneMunicipalities, campo novo publicado pelo loader a partir de kind === 'zona': o /simplify rejeitou o predicado isZoneMapKey que lia isso da forma da chave (7 dígitos ⇒ município), porque era uma segunda codificação da regra de mapKeyForMunicipality, e gatear em zoneBreakdown recriaria o acoplamento que o fill-in da lista vai deletar. F4: dois acessores órfãos deletados, a união de navegação de 3 estados colapsada em string | undefined, MunicipalitiesByMapKey virou Record<string, string> (435 strings fora do payload RSC do Início), polygonRingsOf exportado e os helpers de geometria retipados em PolygonalFeature/BahiaFeature<P>/BahiaMeshFeature. Um item do plano foi rejeitado por medição: city/zoneNumber de MunicipalityZoneNeighborhoodEntry ficam — entram nas linhas hasheadas por canonicalEvidenceRows e são colunas do fixture de proveniência da RA 02/2017, então não são código morto (só o zoneNumber do artefato TopoJSON saiu) · sem migration · plano
  • E12+ escala/DRY pós-E12 — duas fases condicionais do /simplify da camada TI: F1 extrair peers/sub-linhas do Metropolitano para lib/ quando surgir 4º call site ou regra mais rica (hoje constantes + benchmark intra-TI compartilham territoryOverview.ts); F2 memo de peer-group no benchmark T4 só com evidência de duplicação no detalhe · ~0,25–0,5d eng · Impeccable A · sem migration · cortável · plano
  • O0+ escala/DRY pós-Onda 0 · plano
  • RS+ auth read leve + shells de senha · plano
  • 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) / volume de planos medido (plano)
  • C8 F1–F2 restantes perf de import/lista (F3 parsers fechou no Pass 2 W1-D2) · gatilho: import em volume real pós-Onda 0 · plano
  • C8 F4b ✓ (entregue 2026-07-27) o guard de formActions só inspecionava arquivos chamados exatamente formActions.ts — ampliado para *FormActions.ts + taskActions.ts; 6 irmãos migrados para runCampaignFormAction/runCampaignRedirectFormAction; taskActions.ts allowlisted (contrato posicional) · achado do /simplify do C13 · plano
  • B33+ ✓ escala/DRY pós-B33 (entregue 2026-07-27) — useCampaignListFilterNavigation em shared/ absorve o scaffold de navegação/debounce dos três filtros (325 linhas removidas contra 228 em src, das quais 138 são o hook novo; JSX só se moveu onde era bug); a auditoria derrubou o racional das "assinaturas diferentes" (os três buildXListHref já canonicalizam, então um só callback toHref basta) e achou dois bugs de comportamento: dobradinhas e territórios descartavam a busca digitada ao tocar um filtro (unificado na semântica de municípios via navigateWithSearch) e o timer do debounce capturava o state do render que o agendou, revertendo um sort clicado dentro da janela de 1s — municípios escapava pelo key= da página, que remonta e mata o timer (corrigido com stateRef, com caso de spec verificado vermelho nas três listas sem o fix). O /simplify somou o fallback de transition copiado em 5 lugares (o hook era o 6º) virando useCampaignListTransition, e um aria-live a menos; a 2ª rodada fechou a ressincronização da caixa de busca com o q commitado (o que permitiu remover o key= de municípios e unificar a política de montagem das três listas — buildMunicipalityFiltersKey orfanou e morreu, junto com shouldUpdateMunicipalitySearchUrl), colapsou setSearch+scheduleSearch num onSearchChange (superfície de 8 → 6 nomes), passou a reusar o buildMunicipalityFilterHref que já existia + criou o irmão buildStateDeputyFilterHref (4 sítios pinavam página 1 à mão), e deu a municípios o resumo de filtros ativos que faltava no bloco mobile; campaignListFilterNavigation.unit.spec.ts (10 casos × 3 listas) é a primeira cobertura desses três componentes. Dívidas deferidas com gatilho (no plano, § "Adiado com gatilho"): shell de <form> compartilhado (gatilho B29), CampaignListSortSelect (bloqueado numa decisão de altura de select, já divergida entre as três), serialize/parseXSortValue triplicado → campaignListUrl, SupporterFilters/ActivityFilters reconstruírem href à mão tendo builders pinados, ~10 kB gzip do catálogo no bundle cliente de /campanha/municipios (pré-existente, achado da review de performance — gatilho já disparado duas vezes: B41 ✓ e B42 ✓ entregaram na rota sem pagar, então deixou de ser condicional e é a dívida de bundle corrente da rota) e o 3º idioma de mock de router nos specs · plano
  • B32+ ✓ escala/DRY pós-B32 (entregue 2026-07-28) — +564 / −802 linhas em 15 arquivos mais quatro novos. A auditoria mudou três premissas antes do código: display vs draft nunca divergiu (os dois eram setados juntos em todos os caminhos — um valor com dois nomes; só o noteDraft do Trend, texto cru vs. nota normalizada, ficou no chamador), reportFailure não nascera nos quatro controles (o de assessores não importava sonner, então falha depois do fechamento era muda — furo do B42, fechado aqui) e o helper do F2 não resolvia o motivo do F2. Por isso F2 virou wrapper de rota: campaignJsonMutationRoute(config, handler) exporta o POST já com origem verificada, corpo parseado e catch mapeado, com o bodySchema.parse dentro do mesmo try para preservar a mensagem de hoje em corpo inválido; parseCampaignJsonRequestBody/campaignJsonMutationErrorResponse deixaram de ser exportados, engagement-level mantém o 409 dentro do handler, e um guard de convenção (molde do de formActions, verificado vermelho com um route.ts probe) impede a 6ª rota de nascer fora — é o que troca a política de origem de disciplina de cópia por estrutura. F3 é postCampaignJson (25 linhas, cinco call sites). F1 é useCampaignCellAutosave: um valor, flush() idempotente como caminho único de save (o inFlightRef do B24 entrou no hook e passou a proteger os três), o hook dono do open porque fechar É o commit, valueRef avançado sincronamente para não reler o render que agendou a mudança (bug que B33+ e B34 já pagaram) e reportFailure embutido; spec novo com fake timers cobre debounce, coalescência, resposta superada, flush no fechamento e revert com toast só depois de fechado — antes essa máquina só tinha rede e2e. F4 moveu a região aria-live para fora do Popover/Drawer (prop statusMessage na casca, que permanece montada). F5 deu footer ao variant="sheet" e o Sinal migrou: o formId que o plano queria na casca é desnecessário — <form id> no chamador + <button form={id}> no footer é HTML padrão, e com isso MunicipalityListSignalControl perdeu o Popover/Drawer próprios e campaignCellEditTriggerClassName virou privada. Gate: 661 unit / 444 int, e2e de tendência e de sinal reexecutados isoladamente (as falhas da suíte cheia foram compilação a frio sob duas outras sessões next dev no mesmo host, o padrão já fichado em B27 ✓/B32 ✓/B42 ✓) · plano
  • B17+ escala/DRY pós-B17 — duas fases independentes do /simplify do seletor de colunas, ambas de estrutura e nenhuma de cálculo (o corte server-side do B17 já economiza as 25 chamadas de cell() e o payload RSC da coluna oculta): F1 esconder a coluna não desliga a query que existe só para ela — loadLeadershipOptions (dobradinhas) e loadStateDeputyOptions (lideranças) são limit: 0, pagination: false dentro do Promise.all, e o cookie é lido depois; como cookies() não faz I/O, subir a leitura e condicionar a promise é ordem de statements, e a rota que mais paga é /campanha/liderancas (~229 ms / 371 statements, P1 no TECH-DEBT); F2 o botão "Colunas" some junto com a tabela no estado vazio em demandas/organizações/apoiadores, que trocam a tabela inteira por um bloco (rows.length ? … : …) em vez de usar a prop empty que a CampaignTable expõe e que municípios/lideranças/territórios já adotam — filtrar até zero linhas tira um controle que não tem relação com o filtro · ~0,5d, F2 cortável · Impeccable A (F1) · plano
  • Higiene PascalCase de componentes legados

A validar (assumptions)

  • C5 GOTV — validar com produto antes de planejar (hoje só design-ref; depende de C2 dados reais).
  • O8 "ilhas isoladas" — fluxo de informação entre política/comunicação/território (pedido de ajuda direto na sessão 2026-07-23; adjacente ao produto) · discovery antes de qualquer build; candidato natural se virar build: D2 sino/resumo semanal.
  • O3 jargão — resgatar o Bloco B (uso observado) das notas dos entrevistadores e/ou 2ª sessão observada antes de investir em glossário além do R6; parcialmente endereçado por evidência datada: E18 ✓ (página de conceitos, pedido de 2026-07-24) e B22 (explicação por coluna no header, pedido de 2026-07-25) — o que continua sem evidência é glossário em badges/estimativas (FD2) (IMPROVE-APP-PLAN.md).

Grafo de dependências (abertos + predecessores mínimos)

flowchart TD
    JUR["Lote jurídico único<br/>(Consent finais + privacidade)"]
    JUR --> C2prod["C2 dados reais<br/>(base nominal)"]
    C2prod --> C5["C5 GOTV (validar)"]
    TSE2026["TSE candidaturas 2026<br/>(após 15/08)"] --> A6["A6 Dobradinha 2026"]
    JUR -.chave push.-> D2["D2 Push + sino"]
    JUR -.chave WA canal.-> D3["D3 WA canal fundação"]
    C2prod -.telefones reais.-> D3
    D3 --> D4["D4 WA envio 1:1"]
    D3 --> D5["D5 WA → rascunhos"]
    D4 -.thread.-> D5
    B8F2["B8 F2 ✓ Polígonos<br/>zonas de Salvador"]
    B14n["B14 ✓ Município mais próximo"]
    B38n["B38 ✓ Sidebar recolhido<br/>em tablet"]
    B39n["B39 ✓ Lembrar de mim<br/>/ sessão longa"]
    B40n["B40 Login biométrico<br/>WebAuthn"]
    B41n["B41 ✓ Scroll horizontal<br/>+ sticky Município"]
    B42n["B42 ✓ Polimento mobile<br/>lista municípios"]
    B15n["B15 ✓ Ordenar lista<br/>por coluna"]
    B16n["B16 ✓ Filtros no header<br/>da lista"]
    B17n["B17 ✓ Seletor de colunas<br/>das 7 listas"]
    B18n["B18 Filtros salvos<br/>+ atalho sidebar"]
    B19n["B19 ✓ Gerenciar assessores"]
    B20n["B20 ✓ Municípios prioritários<br/>no Início"]
    B21n["B21 ✓ Página dos<br/>Territórios de Identidade"]
    B22n["B22 Explicação por coluna<br/>no header das listas"]
    B23n["B23 Tooltip nas células<br/>das listas"]
    B24n["B24 ✓ Auto-save + justificativa<br/>no popover de Tendência"]
    B25n["B25 ✓ Link do território<br/>na lista de municípios"]
    B26n["B26 ✓ Registrar sinal<br/>na célula Último sinal"]
    B27n["B27 ✓ Combobox + chips<br/>no popover de Assessores"]
    B28n["B28 ✓ E-mail + celular<br/>na lista de lideranças"]
    B30n["B30 ✓ Convite WhatsApp<br/>na lista de lideranças"]
    B31n["B31 ✓ Coluna de dobradinhas<br/>na lista de lideranças"]
    B32n["B32 ✓ Editar Status de apoio<br/>na célula da lista"]
    B33n["B33 ✓ Sort + filtro no header<br/>da lista de dobradinhas"]
    B34n["B34 Chips editáveis de município<br/>na lista de lideranças"]
    B37n["B37 Chips editáveis de município<br/>na lista de dobradinhas"]
    B36n["B36 ✓ Chips de lideranças<br/>na lista de dobradinhas"]
    B29n["B29 ✓ Sort + filtro no header<br/>da lista de lideranças"]
    C13n["C13 ✓ Plano de Ação → Atividade<br/>(rename + guard de vocabulário)"]
    E4R["E4R ✓ Import planilha<br/>(seed estratégia)"]
    A11n["A11 ✓ Posição em votos"]
    E17n["E17 ✓ Tabela TI no Início"]
    E18n["E18 ✓ Documentação<br/>de conceitos"]

    subgraph Intel["Inteligência de campanha"]
        E8i["E8 ✓ Conta da cadeira"]
        E9i["E9 ✓ Fila de alocação"]
        C12i["C12 ✓ Registro-fundação"]
        E16i["E16 ✓ Dossiê do município"]
        E10i["E10 ✓ Classificação relativa"]
        B13i["B13 ✓ Escala relativa no mapa"]
        E14i["E14 ✓ Níveis N0–N4"]
        E11i["E11 Motor de sugestões v1"]
        E12i["E12 Camada TI"]
        E13i["E13 Planejador de giros"]
        E15i["E15 Backtest pós-eleição"]
    end

    E4R -.metas seedadas.-> E8i
    E4R -.priority alta.-> B20n
    E8i -.conceitos a documentar.-> E18n
    E8i -.cobertura.-> B20n
    E9i -.critério déficit.-> B20n
    E9i -.célula de frescor.-> B26n
    C12i -.sinal tipado.-> B26n
    B15n -.contrato sort/dir.-> A11n
    B15n -.header sort.-> B16n
    B15n -.ids de coluna.-> B17n
    B16n -.barra slim.-> B17n
    B16n -.barra slim.-> B18n
    B17n -.vizinho barra.-> B18n
    B38n -.rail aberto p/ submenu.-> B18n
    B39n -.TTL / LoginForm.-> B40n
    B16n -.trade-off overflow-x.-> B41n
    B38n -.viewport tablet.-> B41n
    B26n -.Drawer mobile.-> B42n
    B24n -.Trend control.-> B42n
    B27n -.Advisors control.-> B42n
    B16n -.densidade do th.-> B22n
    E18n -.oneLiner dos conceitos.-> B22n
    B22n -.colunas se explicam.-> B17n
    B22n -.header compartilhado.-> B21n
    B22n -.primitivo compartilhado.-> B23n
    B19n -.carteira de assessores.-> B23n
    E4R -.notas de tendência.-> B23n
    E4R -.notas a corrigir.-> B24n
    B23n -.nota no Popover vira campo.-> B24n
    B23n -.células se explicam.-> B21n
    A11n -.coluna/ordenação.-> E9i
    B16n -.pattern header.-> E9i
    B17n -.viewport colunas.-> E9i
    E17n -.rollup + tabela.-> B21n
    B15n -.header sort.-> B21n
    B16n -.header filtro.-> B21n
    B17n -.colunas como dado.-> B21n
    B21n --> B25n
    B17n -.coluna ocultável.-> B25n
    B24n -.contrato do popover na linha.-> B26n
    B23n -.tap na mesma célula.-> B26n
    B19n -.delta + lock + chips.-> B27n
    B24n -.endpoint JSON sem revalidar.-> B27n
    B23n -.tooltip no mesmo gatilho.-> B27n
    B16n -.busca em Popover.-> B27n
    B26n -.contratos vizinhos na mesma linha.-> B27n
    E9i -.badge sem responsável.-> B27n
    E4R -.contas de assessor.-> B27n
    B19n -.copy + WhatsApp na lista.-> B28n
    B28n -.ícone na mesma linha.-> B30n
    B19n -.chips + busca + delta.-> B31n
    B27n -.mesma peça de célula.-> B31n
    B29n -.colunas da mesma tabela.-> B31n
    B28n -.mesma tabela.-> B31n
    B30n -.mesma tabela.-> B31n
    B17n -.coluna ocultável.-> B31n
    E4R -.dobradinha só no município.-> B31n
    B31n -.rede vinculada por dobradinha.-> E13i
    B24n -.mesma família de decisão.-> B32n
    B29n -.gatilho fechado.-> B32n
    B31n -.mesma tabela, célula vizinha.-> B32n
    B29n -.mesma corrida de extração.-> B33n
    B21n -.mesma corrida de extração.-> B33n
    B15n -.contrato a espelhar.-> B33n
    B16n -.contrato a espelhar.-> B33n
    B19n -.lib de origem.-> B34n
    B27n -.mesma peça de célula.-> B34n
    B31n -.mesma tabela + decisão de container.-> B34n
    B28n -.mesma tabela.-> B34n
    B30n -.mesma tabela.-> B34n
    B32n -.mesma tabela.-> B34n
    B17n -.tabela cresce.-> B34n
    E16i -.painel do município a revalidar.-> B34n
    B34n -.mesma promoção de lib + container.-> B37n
    B31n -.mesma família de pedido.-> B37n
    B19n -.lib de origem + cálculo de próximo array.-> B37n
    B33n -.mesma tabela.-> B37n
    E4R -.dobradinha só no município.-> B37n
    B31n -.mutação/schema/lock reusados sem alteração.-> B36n
    B19n -.padrão de origem.-> B36n
    B33n -.mesma tabela.-> B36n
    E4R -.fichas vazias dos dois lados.-> B36n
    B15n -.contrato sort/dir.-> B29n
    B16n -.popover de filtro.-> B29n
    B21n -.3º call site do head.-> B29n
    B28n -.colunas vizinhas na mesma tabela.-> B29n
    B29n -.coluna e filtro de nível.-> E14i
    E4R -.fichas seedadas na lista.-> B29n
    B21n -.superfície regional.-> E12i
    E17n -.primeira fatia.-> E12i
    E8i --> E9i --> E11i
    C12i --> E11i
    C12i --> E14i
    C12i --> E15i
    E8i --> E10i --> B13i
    E10i -.classes.-> E11i
    E8i --> E12i
    E8i -.metas.-> E14i
    E14i -.níveis.-> E11i
    C12i --> E13i
    E8i --> E13i
    E8i -.derivados.-> E16i
    C12i -.sinais.-> E16i
    E16i -.dossiê pré-visita.-> E13i
    A6 -.dobradinha.-> E13i
    C13n -.vocabulário e caminhos da vertical.-> E13i
    C2prod -.dados reais.-> E11i
    E11i -.decisões acumuladas.-> E15i
    B8F2 -.ZE Salvador = débito aberto.-> B14n

Paralelizáveis agora: B13 (E10 ✓ entregou as classes; consome computeMunicipalityTerritorialClass como está), E12 (E8 ✓), E11 (E9 ✓/E10 ✓ entregues — livre), B14 ✓ (atalho geo no Início entregue 2026-07-26; B8 F2 ✓ entregou os polígonos das ZE em 2026-07-26, então desambiguar Salvador virou débito com gatilho "pedido de campo", não dependência), B38 ✓ (sidebar recolhido em tablet entregue 2026-07-26), B39 (lembrar de mim / sessão longa; sem dep dura — cookie/JWT já existem; soft B40 só pela ordem no mesmo LoginForm), B40 (login biométrico; soft B39 pelo TTL/copy — pode começar depois do B39), B41 ✓ (scroll horizontal + sticky — entregue 2026-07-27), B42 ✓ (polimento mobile da mesma lista — entregue 2026-07-27), B21 (página dos TIs; sem dep dura — E17 ✓/B15 ✓/B16 ✓ já entregues; E12 pousa nela depois; B25 é o único item que espera B21 — o link do território na lista só existe depois do destino), B17 (seletor de colunas; soft B15 ✓/B16 ✓ — pousa no header de coluna atual), B18 (filtros salvos + submenu Municípios; soft B16 ✓/B17 — botão na barra), B22 (explicação por coluna no header; sem dep dura — B15 ✓/B16 ✓/E18 ✓), B23 (tooltip nas células; a mecânica genérica e a promoção do primitivo já saíram no E10 ✓ — restam os consumidores Assessores/Tendência e a convivência com o Popover), B24 ✓ (auto-save + justificativa no popover de Tendência; entregue 2026-07-26), B26 (registrar sinal na célula "Último sinal"; sem dep dura — E9 ✓/C12 ✓ já entregues; soft B24/B23 só pela coerência de contrato na mesma linha), B27 (combobox + chips + auto-save no popover de Assessores; sem dep dura — B19 ✓ já deu o delta+lock e o padrão de chips; soft B24 pelo endpoint e B23 pelo gatilho), B28 (e-mail + celular na lista de lideranças; sem dep dura — B19 ✓ é só o padrão soft de copy/WhatsApp; phone já está no view model), B29 (sort + filtro no header da lista de lideranças; sem dep dura — B15 ✓/B16 ✓ deram o contrato e o popover; soft B21 e B28 só pela ordem de chegada: quem entrar depois herda o head compartilhado / encaixa as colunas do outro), fill-ins (renomear coluna Cobertura → quick win de densidade no header; remover coluna Tipo — quick win de densidade; remover lista de zonas do Início — pós-B8 F2 ✓; ícone de prioridade na lista; Cenário junto aos filtros — encaixa na barra slim de B16 ✓ —; selecionados no topo nos filtros do header — helper puro que B21 reusa —; O0+, RS+). B30 (convite WhatsApp na lista de lideranças; sem dep dura — reusa integralmente a action/collection existentes; soft B28 só pelo ícone na mesma linha, qualquer um pode ir primeiro — entregue 2026-07-26), B31 (coluna de dobradinhas na lista de lideranças; sem dep dura — o dado já está no view model e o catálogo já tem loader; soft B27 pela peça de célula, que só vira extração compartilhada no 3º call site, e B28/B29/B30 pela ordem de chegada na mesma tabela), B32 ✓ (editar Status de apoio na célula da lista de lideranças; entregue 2026-07-26), B33 (sort + filtro no header da lista de dobradinhas; sem dep dura — B15 ✓/B16 ✓ deram o contrato; soft B29/B21 só pela ordem de chegada na mesma corrida de extração do chrome compartilhado, quem entrar primeiro paga a peça e o outro só consome), B34 (chips editáveis de municípios na lista de lideranças; sem dep dura — B19 ✓ já deu o delta+lock, o padrão de chips e o loader do catálogo a promover; soft B27/B31 pela mesma peça de célula editável — quem chegar depois herda o combobox/Popover em vez de reinventar —, e B28/B30/B32 só pela mesma tabela; decisão a validar com produto: Popover por célula em vez do toggle "Editar" pedido literalmente, mesmo precedente do B31/B32), B37 (chips editáveis de municípios na lista de dobradinhas; sem dep dura — B19 ✓/B34 já dão a lib a promover e o desenho do Popover+combobox; soft B34 só pela ordem de chegada em quem faz a promoção da lib, B31 pela mesma família de pedido em outra tabela, B33 pela mesma tabela; mesma decisão a validar com produto do B31/B32/B34: Popover por célula, não toggle). B36 (coluna de lideranças na lista de dobradinhas; sem dep dura — reusa integralmente a mutação/schema/lock que o B31 desenha para a mesma relação, do lado oposto; soft B31 pela corrida de extração da célula/action compartilhadas, B19 ✓ pelo padrão de origem, B33 pela mesma tabela), C13 (renomear Plano de Ação → Atividade; sem dep dura — não toca nenhuma das listas em curso (B26–B37 são municípios/lideranças/dobradinhas), então roda em paralelo a elas; a única ordem que importa é antes de E13, que constrói dentro da vertical renomeada, e antes do congelamento ~20/09 por carregar migration; PR mecânico e isolado, não misturar com feature). D3 só após smoke + folga e só se o fluxo sede-digita (C12 ✓) não absorver os deltas do ZAP (não compete com E9).

Sequência por janela (só pendentes)

Janela 1 — agora → 05/08 (convenções): smoke pós-deploy em produção + onboarding do time (Onda 0 §2/§4); E4R ✓ em código (aplicar pnpm db:seed:projecao em produção após smoke); E8 ✓ conta da cadeira entregue; E17 ✓ tabela TI no Início; B15 ✓ (sort por coluna) e A11 ✓ (posição em votos + sort=votos; o default do staff passou a deficit no E9 ✓) entregues 2026-07-24; E18 ✓ documentação de conceitos (/campanha/conceitos, v1 só E8) entregue 2026-07-24; B19 ✓ gerenciar assessores (caminho do onboarding sem /admin — criar/ativar contas e carteiras) entregue 2026-07-24; B16 ✓ filtros no header entregue 2026-07-25; B20 ✓ Municípios prioritários no Início entregue 2026-07-25; B38 ✓ sidebar recolhido em tablet (entregue 2026-07-26 — offcanvas + trigger md+ + cookie/matchMedia); B39 (lembrar de mim / sessão longa — o default de 2 h está derrubando sessão no onboarding; ~0,5 dia, entra cedo) e, logo atrás se folga, B40 (biometria WebAuthn — cortável); B41 ✓ (scroll horizontal + sticky na lista de municípios — entregue 2026-07-27) e B42 ✓ (polimento mobile da mesma lista — Drawer em Tendência/Assessores/Votos estimados + card compacto e clicável; entregue 2026-07-27); B21 (página dos Territórios de Identidade — a leitura regional sai do painel do Início e ganha tela própria com sort/filtro; útil já no onboarding para dividir carteiras) se folga — e, logo atrás dela, B25 (link do território na célula da lista: quarto de dia, é o que faz a página nova ser encontrada de dentro do trabalho); B22 (explicação por coluna no header — a tabela deixa de exigir o autor do E8/E9 ao lado para ser lida; quick win de onboarding, meio dia); B23 (tooltip nas células — nomes dos assessores no hover da coluna, hoje só iniciais, e a justificativa da tendência, hoje invisível na lista; capacidade genérica da célula, meio dia, casa com B22 pelo mesmo primitivo); B24 ✓ (auto-save + justificativa no popover de Tendência — entregue 2026-07-26; a mesa corrige em massa as notas semeadas pelo E4R ✓ sem abrir 435 telas de edição); B26 (registrar sinal na própria célula "Último sinal" — fecha o laço do E9 ✓: hoje a fila diz onde está frio e obriga a sair dela para registrar qualquer coisa; ~0,75 dia, e é o item que mais aumenta a chance do gate de adoção abaixo, porque o registro passa a caber no gesto de varrer a lista, inclusive no celular); B27 (combobox + chips + auto-save nos Assessores — é literalmente a tela em que o CG divide as carteiras no onboarding: hoje procura o nome numa coluna de checkboxes e confirma num botão; ~0,75 dia, casa com B24 pelo mesmo endpoint e com B23 pelo mesmo gatilho); B28 (e-mail + celular na lista de lideranças — copy + WhatsApp como em assessores, sem abrir a ficha; ~0,5–0,75 dia, quick win de campo no onboarding da rede); B30 (convidar para completar cadastro por WhatsApp na mesma lista — reusa o convite que já existe no detalhe, sem abrir a ficha; ~0,25–0,5 dia, o quick win mais barato desta leva porque não toca action/collection nenhuma; casa com B28 pela mesma linha, ícone distinto); B31 (coluna de dobradinhas na mesma lista — é o preenchimento em lote que o onboarding exige: o E4R ✓ deixou dobradinha só no município e as fichas vazias, e completar uma a uma pelo detalhe é o gesto que trava a mesa; ~1 dia, casa com B27 pela mesma peça de célula — quem for primeiro define o padrão do chip+combobox in-list); B32 ✓ (editar Status de apoio na célula da mesma lista — entregue 2026-07-26; fecha o adiado do próprio B29); B34 (chips editáveis de municípios na mesma lista — a coluna "Municípios" ganha o mesmo tratamento da coluna homônima em Assessores (B19 ✓): chips com link + Popover de edição com combobox, em vez do texto morto atual; ~1–1,25 dia, casa com B27/B31 pela mesma peça de célula editável — quem entrar antes define o padrão do combobox+chip in-list que os outros herdam); B37 (o mesmo tratamento de chips editáveis na coluna "Municípios" de /campanha/dobradinhas — hoje um número morto, municipalityCount; ~0,75–1 dia, herda a lib promovida pelo B34 se ele já tiver landado, senão promove ela mesma); B36 (a coluna "Lideranças" da mesma lista ganha o mesmo tratamento — chips nomeados + Popover + busca + auto-save por delta, reusando sem alteração a mutação que o B31 desenha para o lado oposto da mesma relação; ~0,5–1,5 dia dependendo de quem entre B31/B36 chega primeiro na corrida de extração da célula compartilhada); B33 (sort + filtro no header de /campanha/dobradinhas — pedido explícito de reuso: Nome/Partido no header e filtro por Partido, sem duplicar o chrome de /campanha/municipios; ~0,5–1,5 dia dependendo de quem chega primeiro na corrida de extração do chrome compartilhado com B29/B21; entra perto de B29 por reusar a mesma peça); B29 (sort + filtro no header da mesma lista — é a tela onde o assessor recebe a carteira e precisa recortar "quem ainda está a abordar aqui" / "quem ainda não tem acesso ao app" antes de convidar; ~1,5–2 dias porque paga também a extração do head compartilhado, então entra depois dos quick wins — mas antes se B21 for adiado, para o head nascer compartilhado); B17 (seletor de colunas) / B18 (filtros salvos + atalho sidebar) se folga de UX da lista no onboarding; fill-in renomear coluna Cobertura (header curto na fila — quarto de dia; glossário/KPI intactos); fill-in remover coluna Tipo (densidade morta na fila — quarto/meio dia; Salvador via slugs cobre ZE); fill-in remover lista de zonas do Início (densidade morta sob o mapa — quarto/meio dia; polígonos B8 F2 ✓ cobrem a navegação); fill-in selecionados no topo nos filtros do header (desmarcar sem caçar checkbox; helper puro que B21 herda — quarto de dia); C13 ✓ (renomeado "Plano de Ação" → "Atividade" em toda a vertical, incluindo a migration data-preserving — entregue 2026-07-25); gate de adoção: sinal Little Hire — ≥1 update espontâneo até 30/07, cobrança da tabela dispensada (planilhas já em docs/sheets/) — acompanhamento em IMPROVE-APP-PLAN.md; R6 critique/polish; E9 ✓ entregue no mesmo dia; B14 ✓ (atalho geo entregue 2026-07-26); Onda 0 jurídica em paralelo (externa).

Janela 2 — 05/08 → 16/08 (pré-propaganda): C2 dados reais assim que o jurídico liberar; E9 fila de alocação (entregue 2026-07-24, antecipada da janela 2); E16 dossiê do município (entregue 2026-07-25, antecipado da janela 2); D2 se sobrar folga.

Janela 3 — 16/08 → set: A6 dobradinha (pós-TSE 15/08); E10 classificação relativa (entregue 2026-07-25) → B13 escala relativa no mapa; E14 níveis N0–N4 (entregue 2026-07-27); E11 motor de sugestões v1; E12 camada TI; E13 planejador de giros; B8 F2 polígonos das zonas de Salvador (entregue 2026-07-26); débitos sobreviventes. Se houver folga e os deltas do ZAP não estiverem sendo absorvidos pelo fluxo sede-digita (C12): começar D3.

Janela 4 — set → 04/10: C5 GOTV (validar), congelamento ~20/09 (só bugfix/dados). D4 envio 1:1 e D5 inbox→rascunhos só se D3 estiver estável antes do congelamento (senão adiar pós-04/10). E15 backtest pós-eleição (após 04/10).

Cortes seguros / não cortáveis

Não cortáveis: Onda 0 (jurídico/Consent); E4R seed da planilha (entregue 2026-07-24 — ainda aplicar em produção após smoke); E8 conta da cadeira (entregue 2026-07-24); C12 registro-fundação (entregue 2026-07-24); C2 dados reais; assimetria declarado×estimado (relação de campo); E9 fila de alocação (entregue 2026-07-24 — era o mínimo de "inteligência, não planilha"); B19 gerenciar assessores (entregue 2026-07-24 — CG/candidato não entram em /admin); B39 lembrar de mim / sessão longa (default de 2 h derruba sessão no onboarding/campo — atrito diário de adoção).

Cortes seguros (se o prazo apertar, nesta ordem): D5 inbox→rascunhos (manter registro manual + wa.me); D4 envio bridge (manter wa.me); D3 fundação do canal (atalho wa.me continua); E12 camada TI (rollup manual por lista; E17 já dá a leitura regional básica no Início); E13 planejador de giros (rebaixado na fila de corte em 2026-07-24: "perna"/agenda é a restrição dominante nomeada em campo — cortar só depois de E12; agenda segue manual com J-A/J-B como guia); E15 backtest (pós-eleição por definição — cortar = perder o aprendizado 2030); E11 motor v1 (manter a fila E9 ✓ sem sugestões); B13 símbolo proporcional (manter quantis/LQ como escala); E14 níveis (entregue 2026-07-27 — priority continua, intocado); B8 F2 polígonos (entregue 2026-07-26 — o mapa não é mais agregado em Salvador); D2 push (adiar); A6; B14 município mais próximo (entregue 2026-07-26); B38 sidebar recolhido em tablet (entregue 2026-07-26); B40 login biométrico (senha + B39 continuam — cortar custa só o atalho de digital/Face ID; o mais caro e o primeiro a cair desta leva B39–B42); B42 polimento mobile da lista de municípios (Popover e card longos continuam usáveis — cortar custa conforto de teclado e densidade no celular); B41 scroll horizontal + sticky (entregue 2026-07-27) (a tabela continua legível com zoom/pan da página — cortar custa o overflow em tablet; cai depois de B42 porque o bug de layout em mesa é mais caro que o polish mobile); B18 filtros salvos (lista + filtros manuais continuam; Visitados cobrem “voltar”); B21 página dos Territórios de Identidade (o painel do E17 ✓ no Início continua entregando a leitura regional) — B25 cai junto por definição (sem destino, sem link); B17 seletor de colunas (tabela completa continua; scroll horizontal); B22 explicação no header (entregue 2026-07-26); B23 tooltip nas células (entregue 2026-07-26); B24 auto-save + justificativa na Tendência (entregue 2026-07-26); B27 combobox + chips nos Assessores (a lista de checkboxes com "Salvar" continua atribuindo — cortar custa a mesma coerência do B24 mais o tempo do CG procurando nomes numa coluna durante o onboarding; corte junto com B24, já que dividem o endpoint); B28 e-mail + celular na lista de lideranças (a ficha /liderancas/[id] continua mostrando o contato — cortar custa o gesto de copiar/abrir ZAP na varredura; quick win barato de cortar e de fazer); B30 convite de completar cadastro por WhatsApp na lista (entregue 2026-07-26); B31 coluna de dobradinhas nas lideranças (o RelationMultiSelect da ficha interna continua vinculando — cortar custa o preenchimento em lote do onboarding, uma ficha aberta por liderança, e mantém o vínculo invisível na varredura; corte junto com B27, já que dividem a peça de célula, e antes de B29 só se a mesa já tiver preenchido as dobradinhas à mão); B32 editar Status de apoio na célula (entregue 2026-07-26); B34 chips editáveis de municípios na lista de lideranças (o texto simples row.municipalityNames.join(', ') continua legível e a ficha /liderancas/[id] continua vinculando município via formulário — cortar custa o link direto ao município, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma peça de célula editável, e antes de B29 pelo mesmo motivo de coerência); B37 chips editáveis de municípios na lista de dobradinhas (o número municipalityCount continua legível e a ficha de município continua vinculando dobradinha pelo MunicipalityStrategyForm — cortar custa o link direto, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha do município; corte junto com B34, já que dividem a mesma lib e a mesma decisão de container); B36 coluna de lideranças na lista de dobradinhas (entregue 2026-07-26; a contagem leadershipCount era o fallback e a ficha /liderancas/[id] continua vinculando dobradinha via formulário — cortar custa o link direto à liderança, a navegação por chip e o atalho de adicionar/remover sem abrir a ficha; corte junto com B31, já que dividem a mesma mutação/schema/lock); B33 sort + filtro no header das dobradinhas (entregue 2026-07-26); B29 sort + filtro no header das lideranças (a busca por nome continua funcionando e o escopo do assessor já reduz a lista — cortar custa o recorte por status/setor/município no onboarding da rede e mantém o header rico como cópia única de municípios; por ser o item mais caro desta faixa (~1,5–2 dias com a extração do head), cai antes do B26, que é o que sustenta o gate de adoção); B26 registrar sinal na célula (o formulário completo em /[slug]?tab=updates continua sendo o caminho — cortar custa o laço da fila do E9 ✓ e o registro em campo pelo celular, que é justamente onde o gate de adoção se decide; por isso cai depois de B22/B23/B24); C13 renomear Plano de Ação → Atividade (entregue 2026-07-25); B20 prioritários no Início (lista ?priority=alta continua; só perde o atalho — mas a seção atual mente, então cortar = conviver com o bug); fill-in renomear coluna Cobertura (a lista continua legível com o rótulo longo — corte = header mais largo); fill-in remover coluna Tipo (a lista continua completa; só mantém densidade morta — corte = conviver com a coluna); fill-in remover lista de zonas do Início (o mapa B8 F2 ✓ continua abrindo cada ZE — corte = conviver com 19 linhas redundantes sob o painel); B16 filtros no header (entregue 2026-07-25); B15 sort por coluna (entregue 2026-07-24); A11/E17 (entregues 2026-07-24); E18 documentação de conceitos (entregue 2026-07-24); E16 dossiê (entregue 2026-07-25); demais débitos/fill-ins.

Bloqueadores atuais

Item Status Fonte
Lote jurídico final LGPD (Consent + privacidade + RIPD) — PII real em hold caminho crítico para dados reais notebook; onda-0; AGENTS
Smoke pós-deploy + onboarding do time (deploy da remodelagem aplicado em produção 2026-07-23) pendente (operacional) Onda 0 §2/§4; checklist AGENTS
Consent público ainda por ID numérico (submitWhatsapp.ts etc.) pendente AGENTS Known Gap #2
Collection Pages + hero/copy da home editáveis pendente (não bloqueia /campanha) AGENTS Known Gap #3

Site público

Já entregue: post/tag, listagens/artigos, seed, cache posts; /privacidade (texto provisório Onda 0); Pixel do Meta nos abaixo-assinados (plano); export CSV no admin.

Próximos:

  • Textos finais de privacidade + polish O0+ (revalidate globals, DRY Lexical) — mencionar cookies/Meta com Pixel em uso
  • Build resiliente ao global metadata ausente ✓ (entregue 2026-07-27) — resolveSiteMetadata cobre as 4 rotas públicas + layout; cadeia global.URL → NEXT_PUBLIC_SITE_URL → omitir canonical/OG/JSON-LD url; plano · (fonte: simplify B16 2026-07-25)
  • Pages institucionais (bio, mandato, propostas) + hero/copy editáveis
  • Agenda/multimídia via links oficiais; CTA Doar → QueroApoiar
  • Migrar Consent dos fluxos públicos para chave estável

Admin Payload

  • Campo roles em users (migration) antes de ampliar /admin — entregue 2026-07-27 (admin | editor; plano). Access explícito por collection já shipped em 2026-07-23 (hardening Fase 0). Unificação com campaignUser rejeitada (gatilho: Fase 2 white-label).
  • Seed reproduzível de Consent por chave (não por ID)

Débitos de engenharia (hardening 2026-07-23)

  • Ledger vivo em TECH-DEBT.md (tracker: IMPROVE-CODE-QUALITY-PLAN.md); itens maiores: dirigir knip unused-exports de warn→error após os splits (já em ERROR no Pass 2 W4b), redesenho do lease da janela de consent ausente nos testes, e2e fino, CI só no mirror GitHub

Plataforma white-label

  • Fase 2 do README (multi-tenant / marca por mandato) — depois da eleição

Fora de escopo (por enquanto)

  • Serviço Rust separado; self-host/Coolify enquanto Vercel atender; doações in-app
  • Geocodificação de seções eleitorais / unidade = seção (os polígonos aproximados dos 19 Municípios-zona de Salvador saíram no B8 F2 ✓ 2026-07-26, por dissolve de bairros — seções continuam fora)
  • PWA do site//admin; PostGIS sem query espacial real
  • WhatsApp Business API / disparo em massa / blast a apoiadores ou eleitores (Res. TSE 23.610 art. 33; Meta veda WABA político). Exceto o programa D3–D5: bridge não oficial 1:1 interno staff↔lideranças já no CRM (ver whatsapp-interno-campanha.md) — risco ToS/operacional explícito nos planos; não é substituto de WABA
  • Previsão estatística de votos neste ciclo
  • Import automático/recorrente de planilhas de projeção como feature de produto (atualização contínua segue via UI). O seed único E4R foi aprovado em 2026-07-24 (evidência O5 — a planilha é a fonte de verdade da mesa) — plano
  • Reordenar colunas da tabela de /campanha/municipios por drag-and-drop — avaliado 2026-07-24: tecnicamente possível (~1–1,5d), mas não agora. Distinto de B17 (mostrar/ocultar colunas — aberto). B15 já reordena linhas (sort no header); B16 densifica o mesmo header com filtros; PRODUCT anti spreadsheet/data-grid; preferência de ordem visual ≠ decisão de alocação; sem evidência de campo. Gatilho: ≥2 atores pedirem em sessão/R6 ou B16+B17+E9 estáveis com atrito medido de ordem — plano (fonte: avaliação roadmap-item 2026-07-24; rabbit hole B15 / Não escopo B16/B17)

Fontes