The /simplify triage produced two live debts, and both are about the same pair of functions the B34+ plan already names in F3, so they landed as F10 of that plan rather than as an escala-dry-pos-b37: the município-side batch actions write one município at a time (findByID + update each — ~870 round trips at the 435 ceiling B37 itself raised, but ~52 at the largest real TI, and no test above a batch of 2), and the two are twins at 2 call sites, one short of the bar. The common trigger is a "select all"/multi-TI affordance or a third batch on that side, and F10 records why the bulk-SQL rewrite is not obviously the answer: it would hand back the scope check that overrideAccess: false gives for free. This delivery also closed B34+ phases outright, which is worth reading in the plan rather than inferring from a diff. F2 is done and its prediction was wrong in a way worth keeping: it stated "o B37 não retira este débito", and B37 read the same evidence the other way — the ~165 measured lines were not deduplicated into useChipRowOverflow, they were deleted when both cells became wrappers of one engine. F9 is done via scopedPortfolioIndex. Half of F3 is done (the three single-id deltas), and the half that stayed out — the set-with-floor form — stayed out because seeing the collapsed code confirmed the plan's own inverted recommendation. F5 and two F6 bullets shrank for the same reason: "written twice across the two surfaces" stopped having two surfaces. Also corrects a comment I got wrong in the previous commit: the batch's sort does not carry the deadlock-avoidance guarantee, since acquireTextAdvisoryLocks sorts its own keys (C8 batched them into one round trip); it only makes the apply order stable.
144 KiB
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, int 452/452, Aikido 0 findings). O /simplify do fechamento achou um bug latente, não uma dedução: o helper de delta que este item criou era a terceira cópia byte-a-byte do mesmo cálculo de um id, e o pareamento importa porque duas das mensagens de teto eram literais crus repetidos em três lugares de src cada — mapCampaignFormActionError casa a mensagem lançada contra safeMessages por string exata, então reescrever uma delas colapsaria silenciosamente uma recusa real em "tente novamente"; o algoritmo virou nextIdsAfterMembership (os três nomes de domínio são wrappers de uma linha), as mensagens viraram constantes ao lado dos seus caps, e os três specs clonados viraram um describe.each com mais cobertura que antes (unit 651 → 715). Também: scopedPortfolioIndex fecha o F9 do B34+ (o .filter() do escopo do assessor devolvia um array novo por linha, derrotando o WeakMap que existe para compartilhar derivações entre linhas) e a busca da célula de dobradinhas parou de renormalizar o catálogo a cada tecla; maxItems "por paridade" foi rejeitado (dobradinha não tem teto de produto, e um "30 generoso" recusaria uma dobradinha estadual legítima); os dois débitos que sobraram — o laço município-a-município das duas actions de lote e a gemelaridade delas (2 call sites) — foram absorvidos no B34+ como F10, sem abrir ID novo · 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") ecandidate("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 registradeclaredVotese 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).
- 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. - Smoke pós-deploy (desbloqueado — deploy da remodelagem aplicado em produção em 2026-07-23) —
NEXT_PUBLIC_SITE_URLHTTPS, login/campanha, município de teste; checklist no AGENTS.md. - Ativação com dados reais assim que (1) liberar — lideranças/apoiadores reais e import em massa.
- Onboarding do time — usuários
coordinator/advisor, primeiros municípios assumidos, treino de campo;B19(entregue 2026-07-24 — CG/candidato sem/campanha/assessores/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), dobradinhasstateDeputy+/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.estimatedVotesemunicipality.expectedVotes; agregação por cenário (default média); seletor no mapa/overview; liderança segue com umdeclaredVotes(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, banas 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 domunicipalityUi, fronteiras lib/utilities zeradas +server-onlyem 21 loaders +components/campaignem 15 domínios (decisão: semsrc/domains/),electionInsightsdeletado (E10 nasce limpo), knipexportsem ERROR no CI (0 mortos), tabs de detalhe extraídas,runCampaignFormActionfecha 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 duplicadovoteGoals("Meta Bom/Regular/Mínimo") foi removido do app (migration20260724_133600com 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
votePledgecom baseline; sinais tipados emmunicipalityUpdate; origem doactionPlan; demandas vinculadas com custo derivado e criação múltipla atômica;allocationDecisionex-ante imutável. Migration20260724_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,majoritarian2022presidente/governador T1, viapnpm build:election-aggregates); curadoriasrc/lib/campoParties.ts(ano→siglas do campo, validada contraelectionPartySpectrum); globalcampaignGoals(meta estadual 150 mil + margem, access staff-read/coordinator-write, migration20260724_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) egoalCoverage.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 contasadvisor, carteira de municípios (auto-save por delta), reenviar link de senha; só CG/candidato (isCampaignUnrestricted); access de update/phone e atribuiçãomunicipality.advisorsalinhados aocandidate; 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(substituidecomposeStateGoal) 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;goalCoveragepassou a receberSuggestedGoalByScenario. Na fila:lastPledgeAtno agregado de pledges,lastSignalAt= máx(lastUpdateAt, pledge) no view model, sort keysdeficit(novo default do staff) efrescor(cenáriocentralfixo), 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=dossiestaff-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-onlymunicipality.budgetNotes("Emendas aportadas", G11 manual-first, migration20260725_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) criamunicipalityUpdatekind: '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, semcampaignGoals, semuser, sem migration), classificando os 435 municípios em Reduto · Expansão · Manutenção · Marginal · Sem base — vocabulário do research, que substitui oDefesa/Ataque/Indecisa/Perdidaherdado do A5-2. Eixos: LQ contra o padrão estadual do próprio candidato (strongLq2 /weakLq0,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").getStatewideFederalTotalsfoi extraído/memoizado embahiaElectionAggregates.ts(municipalityVoteRankpassou 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 caminholimit: 0e se aplica também ao escopo da visão geral e às facetas, senãototalDocsmentiria. 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) viapickDashboardPriorityMunicipalities;highPriorityCountno Badge; CTA “Ver todas” →/campanha/municipios?priority=alta; markuplist-none. Plano: municipios-prioritarios-inicio.md. -
B38 (2026-07-26) — sidebar recolhido em tablet:
CampaignSidebarcomcollapsible="offcanvas",SidebarTriggerno insetmd+,defaultOpena partir do cookiesidebar_state, colapso padrão 768–1023 viaCampaignSidebarViewportDefaultquando 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.computeAggregateTerritorialClassclassifica um grupo de slugs como um território só (o mapa é poribgeCodee Salvador são 19 slugs) somando insumos e rodando o mesmo classificador — o E12 herda o helper. As âncoras do E10 saíram parasrc/lib/territorialClassAnchors.tspara não arrastar o artefato ao bundle do cliente; a matemática das escalas vive emsrc/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/campanhainalterado (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 porpnpm build:municipality-zone-geometriesa partir doBA_bairros_CD2022do IBGE (shpjscomo 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 (slugna zona,ibgeCodeno resto): bundle, rollups, navegação e classe territorial re-keyed, e o ramokind: '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.competitiveRankcontinua por cidade (o artefato só temfederalRankByIbgeCode, 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 emmunicipality(engagementLevelsem default,levelNote,levelChangedAtderivado por hook) na migration20260727_161752_add_municipality_engagement_level, que também acrescentamovimentoao enum deoutcome— 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 lockmunicipality-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ó delevelChangedAt) vive num módulo puro e client-safe, então o Popover mostra o motivo antes de submeter e o servidor revalida; a rota respondeblocked(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 quecampanha-edit-where-you-seeprevê 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" viraexists: falsenum ramoor) e sortnivelem 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";sortByNullableValuemanda o nulo para o fim nas duas direções, comosem_baseemclasse—, 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 oPOSTdas cinco rotas JSON de/campanhajá com origem verificada, corpo parseado ecatchmapeado — a fase existia porqueisSameOriginRequestestava 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. ObodySchema.parseroda dentro do mesmotryde propósito: é o que preserva a mensagem de hoje para corpo inválido. Do lado do cliente,postCampaignJsoncobre só o transporte euseCampaignCellAutosaveabsorve a máquina dos três controles de debounce (B24/B27/B32) — com um valor, porquedisplayedrafteram sempre setados juntos (o par que o plano tratava como a dificuldade central era o mesmo valor com dois nomes), comflush()idempotente como caminho único de save e o hook dono doopen, já que fechar É o commit. Duas fases de casca: a regiãoaria-livemudou-se para fora do Popover/Drawer (era anunciada por um nó que o fechamento — o próprio gatilho da gravação — desmontava), e ovariant="sheet"ganhou slotfooter, 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é;formIdna 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 |
|---|---|---|---|---|---|---|---|
| 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 ✓ | ||
| 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 ✓ | ||
| 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 |
| 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 (helpermunicipalityVoteRank; baseline + coluna lista; default?sort=votosdesc; 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) ·— entregue 2026-07-24 (1d · Janela 1–2 · primeira fatia de E12 · planoterritoryOverview.ts+loadTerritoryOverview.ts+TerritoryOverviewTable.tsx; rollup puro client-safe, loaderoverrideAccess: 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,loadTerritoryOverviewcom bundle E8 +computeAggregateTerritorialClass, captura como razão dos agregados com decomposição no tooltip, T4 noMunicipalityGoalAccountCard;territoryIntraCaptureBenchmark.ts; conceitos E18; sem migration) · planoC12 registro-fundação— entregue em código 2026-07-24 (versions, sinais, origem/custo derivado, decisões ex-ante; migration + testes) · planoC13 renomear "Plano de Ação" → "Atividade" (entidade, código, banco e rotas)— entregue 2026-07-25 (collectionactivity, 4 tabelas + 3 enums + 2 colunas + 39 índices + 14 FKs + 2 sequences renomeados por migration data-preserving escrita à mão20260725_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 emcodebaseConventions.unit.spec.tsvarrendosrc+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 (artefatobahia-municipality-zones.topo.jsonpor dissolve IBGE↔TRE RA 02/2017; coroplético keyed por map keyslug|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
- 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"noCampaignSidebar;SidebarTriggerno insetmd+;defaultOpenlê cookiesidebar_state;CampaignSidebarViewportDefaultcolapsa uma vez em 768–1023 sem cookie; mobile Sheet inalterado; sem migration) · planoB14✓ 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 · planoB20 Municípios prioritários no Início— entregue 2026-07-25 (pickDashboardPriorityMunicipalities+ amostra 8 por déficitcentral; Badge com total; “Ver todas” →?priority=alta;list-nonena strip) · planoB21 página própria dos Territórios de Identidade— entregue 2026-07-25 (/campanha/territoriosstaff-only; 27 TIs + decomposição do Metropolitano; sort/filtros/busca canônicos na URL; drill para municípios; chrome extraído emshared/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 (keyvotos)) · planoB16 filtros no header das colunas da lista de municípios— entregue 2026-07-24 (funil+Popover noTableHeadao lado do sort B15; barra slim busca+resumo+Limpar; mobile selects empilhados; optimistic + busca no Popover de TI) · planoB22 explicação por coluna no header das listas— entregue 2026-07-26 (description?: ReactNodeemCampaignTableHeadeMunicipalitySortableHead— rename detooltip, sublinhado pontilhado no label, mesmoCampaignHoverTooltipcompartilhado já promovido pelo E10 ✓;campaignConceptOneLineremcampaignIntelligenceConcepts.tscomo fonte única para as colunas cobertas pelo E18 ✓;municipalityColumnDescriptionscobre as 11 colunas de/campanha/municipios, staff e leader; sem migration/action) · planoB23 tooltip de conteúdo nas células das listas— entregue 2026-07-26 (a mecânica genérica —cellTooltip?: (row) => ReactNodeemCampaignTableColumn,CampaignCellTooltip,CampaignHoverTooltippromovido parashared/— já tinha saído de graça no E10 ✓; este item fechou o que faltava:openOnTouch/disabledemCampaignHoverTooltippara conviver com Popover sem roubar o tap nem sobreviver atrás dele (mais um terceiro comportamento do/simplify:contentfalsy 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) · planoB24 auto-save + justificativa no popover de Tendência da lista de municípios— entregue 2026-07-26 (MunicipalityListTrendControlsem "Salvar": status ~150 ms / justificativa 600 ms + flush blur/fechamento;Textareano lugar do hidden/<p>read-only; endpointPOST /campanha/municipios/political-trendsemrevalidatePath; fecha o adiado "nota no popover" do B9 ✓ e o anti-goal do princípio 4; soft B23 ✓/B27 ✓) · planoB25 link do território na lista de municípios— entregue 2026-07-26 (TerritoryLinkna coluna/card de/campanha/municipios→/campanha/territorios#ti-<slug>;src/lib/territoryAnchor.ts;CampaignTable.rowId+ âncoras emTerritoryList; escopo A — só lista) · planoB26 registrar sinal a partir da célula "Último sinal"— entregue 2026-07-25 (Popover desktop / Drawer mobile;municipalityUpdatekind: 'sinal'; submit explícito;revalidateMunicipalityListPaths; sem migration) · planoB27 combobox + chips + auto-save no popover de Assessores da lista de municípios— entregue 2026-07-26 (chips removíveis +Commandde busca acento-insensível + gravação por delta, sem botão "Salvar";nextAdvisorIdsAfterMembershipsubiu parasrc/lib/municipalityAdvisorMembership.ts; endpointPOST /campanha/municipios/advisorsespelhaexpected-votes/;assignMunicipalityAdvisorsFormAction/replace do array sobrevive só para/editar) · planoB28 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.emailpopulado sem query nova eloadLeadershipDetaildeduplicado; primitivo de copy extraído parashared/CampaignCopyableCell.tsxjá no 2º call site —AdvisorsTablemigrado no mesmo PR — ewhatsAppHrefForPhonepromovido alib/phone.ts;CampaignTablepermanece (sem clonarAdvisorsTable). Fora: toggle "Editar" / debounce in-list (Contact update + locks = Adiado no plano); sem migration/collection/Consent/action de escrita) · planoB30 convidar para completar cadastro por WhatsApp na lista de lideranças— entregue 2026-07-26 (LeadershipInviteRowActionna colunaactionsao lado do WhatsApp de contato do B28 ✓;UserPlusIcon+ Popover two-step comcreateCampaignInvite/kind: 'autopreenchimento'; disable pornormalizeBrazilianPhone; sem migration/action nova) · planoB29 ordenação e filtro no header da lista de lideranças— entregue 2026-07-28 — pedido de produto (2026-07-25): levar para/campanha/liderancaso 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+pagee 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 joincontact.name(o adapter resolve caminho pontilhado embuildOrderBy) — sem desnormalizar nome naleadership(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 delimit: 200na pré-resolução de contatos da busca. É o 3º call site do header rico (municípios ✓ · B21 · este), o gatilho que o B21 registrou: extraiCampaignSortableHead+CampaignHeaderFilterPopoverparashared/(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 · planoB31 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 viauseTransition, padrão deAdvisorMunicipalityCell/B19 ✓, não o endpoint JSON do B27, porque aqui há derivados a revalidar;setLeadershipStateDeputyMembership(Record)em transação +acquireTextAdvisoryLocks+revalidatePathem/campanha/liderancas,/campanha/liderancas/<id>(id concreto —/simplifyrodada 2 trocou o template dinâmico),/campanha/dobradinhase a ficha do deputado tocado; helper puronextStateDeputyIdsAfterMembershipespelhanextAdvisorIdsAfterMembership; catálogo (loadStateDeputyOptions, agora complainName/party/slug) entregue uma vez por tabela vialeadershipColumns(stateDeputyOptions)(columns factory, mesmo precedente demunicipalityListColumns—/simplifyrodada 2 substituiu o Context inicial);MAX_LEADERSHIP_STATE_DEPUTIESexportado;loadLeadershipDetaildeduplicado. É o 2º call site do padrão Popover+Command+chips — extração parashared/continua esperando o 3º) · planoB32 editar Status de apoio na célula da lista de lideranças— entregue 2026-07-26 (LeadershipListSupportStatusControl: Popover +NativeSelectsempre 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 viaPOST /campanha/liderancas/support-status(2º/3º endpoint JSON da campanha —isSameOriginRequeste o shellcampaignJsonMutationRoute.tsjá existiam por B24 ✓/B27 ✓, este item só importa), reusandoupdateLeadershipInternalsemrevalidatePath; extraiuleadershipStaffEditSafeMessages(completado nos irmãosliderancas/formActions.ts/nova/formActions.tspelo/simplify) eleadershipLabels.ts(agora fonte única também paraSupportStatusBadge); fecha o "Adiado com gatilho" que o B29 registrou) · planoB33 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.tsespelhammunicipalityListUrl.ts/territoryListUrl.ts; paridade mobile —StateDeputyFilters— entrou por decisão desta sessão; ordem dos nulos departypinada por int test, não assumida;municipalityCount/leadershipCountseguem sem sort/filtro, como travado) · planoB34 chips editáveis de municípios na lista de lideranças— entregue 2026-07-27 (a coluna "Municípios" de/campanha/liderancasvirou 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.mdprincípio 3 e.cursor/rules/campanha-edit-where-you-see.mdcpassaram a admitir input sempre montado para relações por chips empointer-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 deAdvisorsTablefoi renomeado para "Editar nome e contato", que é tudo o que ele ainda destrava. Divergências do plano, todas medidas:AdvisorMunicipalityCellfoi promovido ashared/MunicipalityPortfolioCell(o plano rejeitava — mas com a célula sempre visível a máquina deResizeObserver/"Ver mais…" da origem passou de peso morto a requisito, e/campanha/assessoresmigrou para a peça compartilhada no mesmo PR, então o 2º call site nasceu junto); sem provider de contexto (índice +addableIdsviajam por props da factory de colunas, montada uma vez pelo RSC); uma única action de lote (setLeadershipMunicipalitiesMembership,municipalityIdsrepetidos +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 comaria-disabled+ razão no toast em vez dedisabled, porquedisabledesconde 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./impeccablefechou 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 "×" viabefore:-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 seamdescriptiondeCampaignTableHead(B22 ✓) quebrava qualquer chamador RSC —campaignHoverTooltipAlign/campaignHoverExplanationClassNamemoravam no módulo'use client', então o servidor chamava uma referência de cliente; foram parasrc/lib/campaignHoverTooltip.tse a lista de lideranças é o primeiro header com explicação em RSC. Sem migration/collection/Consent/mudança de URL) · planoB37 chips editáveis de municípios na lista de dobradinhas— entregue 2026-07-28 (a coluna "Municípios" de/campanha/dobradinhastrocarow.municipalityCountpela mesma peça do B34 ✓ —shared/MunicipalityPortfolioCell, sem cópia, sem provider novo — escrevendo do lado invertidomunicipality.stateDeputiespor uma action de lote única (setStateDeputyMunicipalitiesBatch, transacional, lock por município, tudo-ou-nada), sem piso mínimo e semassertMunicipalitiesWithinScope(canUpdateMunicipalityjá escopa o assessor); cap destateDeputiesArraySchema20 → 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:MunicipalityPortfolioCelleLeadershipStateDeputyRelationCell(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ãoshared/RelationChipCell.tsxnasceu como motor único e as duas células viraram wrappers finos; fecha adiantado ouseChipRowOverflowdo 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 (addableIdsagora filtra o que a célula renderiza, não só o que oferece adicionar), não só explicando a falha.LeadershipStateDeputyRelationCellganhouownerNamee 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
/simplifydo 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 oregiondo banco pelo do catálogo, que é snapshot-testado. Também entram:(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 emuseChipRowOverflowshared/RelationChipCell.tsx, entãoMunicipalityPortfolioCelleLeadershipStateDeputyRelationCellnão têm mais 105 linhas duplicadas, têm zero),um só delta de membership para os três módulos de um id(fechado no/simplifydo B37 ✓:nextIdsAfterMembershipcom o cap como dado, três wrappers de uma linha, três specs clonados → umdescribe.each; a forma "conjunto com piso e teto" ficou de fora de propósito — a recomendação invertida da segunda leitura se confirmou depois de ver o código colapsado, porque um genérico cobrindo as duas formas pediria quatro callbacks para poupar ~20 linhas, e o helper novo tem 12 sem parâmetro opcional) — resta owithLeadershipRelationDeltalocal para as duas actions gêmeas e adjacentes, caminho só-ids no loader de lista de assessores, e oComboboxdo base-ui — que ao contrário do cmdk não está descartado, porqueui/combobox.tsxjá aceitaanchore tem hookdata-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 orevalidatePathda 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); eo ramo do assessor entrega uma chave nova ao WeakMap por linha✓ (municipalityIndex.filterderrotava em 25 linhas a memoização que a própria entrega criou — fechado no/simplifydo B37 ✓ porscopedPortfolioIndex, memoizado por(index, addableIds)na lib e não numa segunda camada de cache dentro da célula; F9). A triage desse fechamento absorveu uma fase nova em vez de abrir ID (F10): as duas actions de lote do lado do município escrevem município a município (findByID+updatecada, ~870 round trips no teto de 435 que o próprio B37 elevou, mas ~52 no maior TI real — Litoral Sul, 26 — e nenhum teste acima de lote 2), e são gêmeas em 2 call sites; gatilho comum: uma afordância de "selecionar tudo"/multi-TI, ou um terceiro lote desse lado — quem chegar mede antes de escolher entre SQL bulk (que devolveria à mão a checagem de escopo que ooverrideAccess: falsedá de graça) e um teto honesto abaixo do universo. 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 oTECH-DEBTagora carrega em P1 (367 dos 371 statements de/campanha/liderancassão a mesma leitura decampaign_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 parashared/RelationChipCell.tsxem vez de só copiar a máquina; fecha ouseChipRowOverflowacima) · 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+Commandcom escrita por server action +useTransition, reusando sem alteraçãosetLeadershipStateDeputyMembership/leadershipStateDeputyMembershipSchema/lockleadership-state-deputies:{id}/nextStateDeputyIdsAfterMembershipdo B31 ✓;LeadershipStateDeputiesCellpromovido ashared/LeadershipStateDeputyRelationCellbidirecional (direction+measureOverflow— clamp 3 linhas + "Ver mais…" no lado do deputado; lado liderança passafalse); columns factorystateDeputyColumns(..., leadershipOptions)— não Context; casca própriadobradinhas/formActions.ts;loadLeadershipOptions;StateDeputyRowViewModel.leadershipCount→leaderships: {id,name}[]na mesma query batch. Sem migration) · planoB17 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 doCampaignTable(uma propcolumnVisibilityliga 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 é olocalStorage:CampaignTableColumn.cellé uma função, o array de colunas não atravessa a fronteira server/client, e esconder no cliente exigiriadata-column+<style>por lista — a persistência virou cookie (campaign_columns, path/campanha, um mapalistId → ids ocultosnum formato compacto semJSON/percent-encoding, porquedocument.cookiee ocookies()do Next discordam sobre decodificação), o servidor filtra e a coluna oculta também some do payload RSC; o custo é umrouter.refresh()dentro da transição compartilhada, que dima o resultado como todo filtro dessas telas. A segunda é o seamdefaultVisible, que morreu: eratrueem 10 colunas efalseem 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: stringvirou obrigatório emCampaignTableColumn(o picker precisa de um rótulo serializável), preenchido nas ~55 definições de coluna — e como em 24 delas oheadpassou a só repetir olabel, o/simplifytornouheadopcional, com default<CampaignTableHead>{label}</CampaignTableHead>. Cada superfície declara uma colunamandatory, 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 emmunicipalityListUrl.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 —— entregue 2026-07-24 (também alinha atribuição/campanha/assessores(lista + novo + detalhe): criar/editar contasadvisor, ver/atribuir municípios, reenviar link de senha; só Coordenador Geral e Candidato (isCampaignUnrestricted); alinha access de update/phone docandidatemunicipality.advisorsa unrestricted; e-mail na lista via leitura privilegiada; auto-save da carteira) · planoB39 lembrar de mim / sessão longa em— entregue 2026-07-27 (checkbox default off; 8 h padrão / 14 d opt-in;/campanhaCampaignUserconfigura o teto longo esetCampaignAuthCookiere-assina JWT + cookie para o TTL escolhido;logoutCampaignrevoga osidno 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 mesmocampaign-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-0na coluna nome, espelho B21 ✓TerritoryList; revisita o trade-off do B16 ✓) · planoB42 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 compartilhadaCampaignCellEditOverlay, 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 propslayoutdos 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 (initialFocusno título; opreventContentAutoFocusque devia mitigar isso era inerte no branchsheet— prop renomeado parapreventPopoverAutoFocus), oaria-labeldo trigger anunciava o verbo e engolia o valor que o pill mostra (agora "Editar tendência em X — Favorável", que de passagem tiroumunicipalityNamede payload RSC morto no desktop), e uma gravação que falhava depois do fechamento revertia o badge em silêncio, porque fechar desmonta oAlertque carregaria o erro (toast.errorsó 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, viroucampaignCellEditTriggerClassName. 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) · planoE18 documentação de conceitos de inteligência de campanha— entregue 2026-07-24 (/campanha/conceitosstaff-only; 7 conceitos de E8 emsrc/lib/campaignIntelligenceConcepts.tsagrupados em base/diagnóstico/meta; "Saiba mais" por métrica nos tooltips + link de teclado no Popover do card;formatElectionNumberpassou 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)BahiaMaptrava pan commaxBounds(BAHIA_BOUNDS.pad(0.1)) +maxBoundsViscosity: 1e zoom-out comminZoomviagetBoundsZoom(BAHIA_BOUNDS, false, L.point(16, 16))recalculado noResizeObserverexistente; ú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 A112022); definição no hover (B22) / glossário (E18 ✓). Só lista (header + card mobile + label de sortdeficit); 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álogokindintacto (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 emMunicipalityMapPanel+ 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 abundle.hasZoneMunicipalities(campo próprio, que sobrevive ao drop dezoneBreakdown), 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 oengineering-standards.mdcexige ("a região de RESULTADOS dima, não só o controle"). O/simplifydo E13 ✓ passou por isto como "too involved — the composer isn't a list"; a auditoria do fechamento mediu o contrário —CampaignListPendingBoundaryé um Provider sobrechildren,CampaignListResultsé umadivcomdata-pending/aria-busy/aria-liveeuseCampaignListTransitionjá 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 emsrc/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— moverVoteEstimateScenarioFieldpara a fileira/barra slim deMunicipalityFilters(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 mesmoPromise.all· plano- E10+ escala/DRY pós-E10 — quatro fases do
/simplifyda classificação territorial: F1 a lista lêmunicipality2× por request (listQuery+loadMunicipalityScope, mesmowhere, select superset) — fecha o "Adiado com gatilho" do A11, cujo gatilho ("3º path derivado no loader") disparou comclassesendo o 4º (votos,deficit,frescor); F2computeAllMunicipalityPotentials()recomputa 435 linhas puras do artefato por request (memo de processo, comobahiaElectionAggregates/municipalityVoteRank); F3 umTooltipProviderpor 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 mesmamunicipalityFilterDefinitionsque 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
/simplifyda 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× classificadorserver-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; F3pnpm build:election-aggregatesvarre a mesma fatia estadual do TSE 3× por ano (~225k docs extras) porqueloadFederalVotesByCityZoneAndCandidatesó precisa departynoselectpara ser superconjunto estrito dos outros dois; F4competitiveRankByYearsoma ~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/simplifydos polígonos das ZE. F1: a política de escala de chão nasceu emscripts/lib/topology.mjs(primeiro módulo descripts/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_DIGITSmorreram 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-municipalitiessaiu 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_BYTESganhou ao ladoMAX_POINTS_PER_FEATURE = 60. F2:featureMapKeypassou a ser exercido sobre features reais das três malhas, cada uma no laço que já a varre inteira (417codarea+ 27code+ 19slug) — o/simplifycorrigiu 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;ibgeCodena 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!comparisonActivepara um<p>condicionado abundle.hasZoneMunicipalities, campo novo publicado pelo loader a partir dekind === 'zona': o/simplifyrejeitou o predicadoisZoneMapKeyque lia isso da forma da chave (7 dígitos ⇒ município), porque era uma segunda codificação da regra demapKeyForMunicipality, e gatear emzoneBreakdownrecriaria 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 emstring | undefined,MunicipalitiesByMapKeyvirouRecord<string, string>(435 strings fora do payload RSC do Início),polygonRingsOfexportado e os helpers de geometria retipados emPolygonalFeature/BahiaFeature<P>/BahiaMeshFeature. Um item do plano foi rejeitado por medição:city/zoneNumberdeMunicipalityZoneNeighborhoodEntryficam — entram nas linhas hasheadas porcanonicalEvidenceRowse são colunas do fixture de proveniência da RA 02/2017, então não são código morto (só ozoneNumberdo artefato TopoJSON saiu) · sem migration · plano- E12+ escala/DRY pós-E12 — duas fases condicionais do
/simplifyda camada TI: F1 extrair peers/sub-linhas do Metropolitano paralib/quando surgir 4º call site ou regra mais rica (hoje constantes + benchmark intra-TI compartilhamterritoryOverview.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 deformActionssó inspecionava arquivos chamados exatamenteformActions.ts— ampliado para*FormActions.ts+taskActions.ts; 6 irmãos migrados pararunCampaignFormAction/runCampaignRedirectFormAction;taskActions.tsallowlisted (contrato posicional) · achado do/simplifydo C13 · planoB33+✓ escala/DRY pós-B33 (entregue 2026-07-27) —useCampaignListFilterNavigationemshared/absorve o scaffold de navegação/debounce dos três filtros (325 linhas removidas contra 228 emsrc, 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êsbuildXListHrefjá canonicalizam, então um só callbacktoHrefbasta) 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 vianavigateWithSearch) e o timer do debounce capturava ostatedo render que o agendou, revertendo um sort clicado dentro da janela de 1s — municípios escapava pelokey=da página, que remonta e mata o timer (corrigido comstateRef, com caso de spec verificado vermelho nas três listas sem o fix). O/simplifysomou o fallback de transition copiado em 5 lugares (o hook era o 6º) virandouseCampaignListTransition, e umaria-livea menos; a 2ª rodada fechou a ressincronização da caixa de busca com oqcommitado (o que permitiu remover okey=de municípios e unificar a política de montagem das três listas —buildMunicipalityFiltersKeyorfanou e morreu, junto comshouldUpdateMunicipalitySearchUrl), colapsousetSearch+scheduleSearchnumonSearchChange(superfície de 8 → 6 nomes), passou a reusar obuildMunicipalityFilterHrefque já existia + criou o irmãobuildStateDeputyFilterHref(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/parseXSortValuetriplicado →campaignListUrl,SupporterFilters/ActivityFiltersreconstruí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 · planoB32+✓ 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:displayvsdraftnunca divergiu (os dois eram setados juntos em todos os caminhos — um valor com dois nomes; só onoteDraftdo Trend, texto cru vs. nota normalizada, ficou no chamador),reportFailurenão nascera nos quatro controles (o de assessores não importavasonner, 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 oPOSTjá com origem verificada, corpo parseado ecatchmapeado, com obodySchema.parsedentro do mesmotrypara preservar a mensagem de hoje em corpo inválido;parseCampaignJsonRequestBody/campaignJsonMutationErrorResponsedeixaram de ser exportados,engagement-levelmantém o 409 dentro do handler, e um guard de convenção (molde do deformActions, verificado vermelho com umroute.tsprobe) 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 (oinFlightRefdo B24 entrou no hook e passou a proteger os três), o hook dono doopenporque fechar É o commit,valueRefavançado sincronamente para não reler o render que agendou a mudança (bug que B33+ e B34 já pagaram) ereportFailureembutido; 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ãoaria-livepara fora do Popover/Drawer (propstatusMessagena casca, que permanece montada). F5 deufooteraovariant="sheet"e o Sinal migrou: oformIdque o plano queria na casca é desnecessário —<form id>no chamador +<button form={id}>no footer é HTML padrão, e com issoMunicipalityListSignalControlperdeu o Popover/Drawer próprios ecampaignCellEditTriggerClassNamevirou 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õesnext devno mesmo host, o padrão já fichado em B27 ✓/B32 ✓/B42 ✓) · plano- B17+ escala/DRY pós-B17 — duas fases independentes do
/simplifydo seletor de colunas, ambas de estrutura e nenhuma de cálculo (o corte server-side do B17 já economiza as 25 chamadas decell()e o payload RSC da coluna oculta): F1 esconder a coluna não desliga a query que existe só para ela —loadLeadershipOptions(dobradinhas) eloadStateDeputyOptions(lideranças) sãolimit: 0, pagination: falsedentro doPromise.all, e o cookie é lido depois; comocookies()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 propemptyque aCampaignTableexpõ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✓ (entregue 2026-07-27) —metadataausenteresolveSiteMetadatacobre as 4 rotas públicas + layout; cadeiaglobal.URL→NEXT_PUBLIC_SITE_URL→ omitir canonical/OG/JSON-LD url; plano · (fonte: simplify B16 2026-07-25)Pagesinstitucionais (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— entregue 2026-07-27 (rolesemusers(migration) antes de ampliar/adminadmin|editor; plano). Access explícito por collection já shipped em 2026-07-23 (hardening Fase 0). Unificação comcampaignUserrejeitada (gatilho: Fase 2 white-label).- Seed reproduzível de
Consentpor 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: dirigirknipunused-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/municipiospor 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
AGENTS.md— decisões travadas, Known Gaps, checklist.cursor/rules/projects/nucleos-eleitorais.mdc— status operacional (era Núcleos → Praças → Municípios)README.md— missãodocs/plans/*.md— planos por item (remodelagem-municipios.md e inteligencia-campanha.md são os planos-mestre vigentes)docs/research/— embasamento de produto/domínio (compêndio de literatura + relatório de discovery aprovado 2026-07-21)docs/CUSTOMER.md— job, OST e evidência de campo (sessão real do CG em 2026-07-23; O1–O8) ·docs/IMPROVE-APP-PLAN.md— journey de discovery/UX (fases, próximas ações)- Res. TSE 23.760/2026 · Res. TSE 23.610/2019 art. 33 · Politipédia AVM