The one finding the /simplify skipped for the wrong reason becomes a fill-in: the tour composer signals a território change only on the control, and the pending pieces it dismissed as list-coupled are a Provider, a div and a hook with a local fallback. The batched goal-account loader stays a trigger on the E13 plan — the planner and the list diverge on missing data on purpose.
19 KiB
E13 — Planejador de presença e giros (agenda do candidato)
Status: entregue 2026-07-27 (v1 = núcleo decisório; os cortes seguem em "Cortes da v1", cada um com gatilho)
Atualizado em: 2026-07-27 (as-built da implementação acrescentado; no mesmo dia, auditoria contra o repositório antes de implementar — duas premissas falsas derrubadas, refs do C13 corrigidas, questões em aberto fechadas, seção Dados acrescentada; revisão anterior 2026-07-24 sincronizou refs pós-remodelagem Municípios + hardening)
Item do roadmap: docs/roadmap.md (seção "Inteligência de campanha", E13; plano-mestre inteligencia-campanha.md)
Impeccable: C — superfície nova de planejamento em /campanha/atividades/giros (sub-rota), sem design-ref
Appetite: ~2 dias eng; sem migration (ver "Revisão 2026-07-27", achado 1)
Responsável: —
Revisão 2026-07-27 — o que a auditoria mudou
O plano foi escrito em 2026-07-24 e o repositório andou. Cinco achados materiais, registrados aqui para que nenhuma fase seja executada contra o estado antigo:
kind: 'visita'NÃO entrou na migration do C12 — e não deve entrar. O C12 entregouorigin(dado | pedido_broker | obrigacao_politica), masactivityKinds(src/lib/schemas/activity.ts) segue semvisita; adicioná-lo éALTER TYPE ... ADD VALUE, ou seja, migration. Decisão: não criar o kind.activity.deputyPresent(checkbox indexado, existe desde o C3) já é o marcador de presença do candidato — é literalmente o que um "planejador de presença" filtra. O giro é derivável pordeputyPresent: true+ TI + janela de datas, e o kind de cada parada continua sendo o real (reuniao_apoiono satélite,comicio/atona âncora). Rejeitado: novo valor de enum, que custaria migration e criaria um kind sobreposto areuniao_apoio/atosem dizer nada quedeputyPresentjá não diga.allocationDecisionnão tem write path. A collection e o access existem (canCreateAllocationDecisione irmãos), mas nada emsrc/cria um registro — sótests/int/campaignAllocationDecision.int.spec.ts. O "aviso com override registrado" exigiria construir a primeira action + zod + testes, trabalho que o appetite de ~1,5d não cobria. Fora da v1; o aviso "não vá" aparece como texto, e a gravação vira gatilho do E11, que precisa desse write path de qualquer forma.- Rota e collection renomeadas pelo C13 (2026-07-25).
actionPlan→activity,/campanha/planos→/campanha/atividades, componentes emsrc/components/campaign/activity/. Não é cosmético:tests/unit/codebaseConventions.unit.spec.ts§ "banned campaign terminology" falha o build seaction[\s_-]?plan,plano de a[çc][aã]oou/campanha/planosaparecer emsrc,testsouscripts. O caminhosrc/components/campaign/TourComposer*.tsxda versão anterior também morreu — o Pass 2 W2 organizoucomponents/campaignem subpastas de domínio, então o compositor nasce emsrc/components/campaign/tour/. - Não existe grafo de contiguidade em runtime. src/lib/bahiaTerritories.ts tem só pertencimento município→TI (
territoryForCity,citiesForTerritory); adjacência de polígono existe apenas dentro dos scripts de build das malhas. Em v1, "encaixe em giro" significa mesmo TI, dito com essas palavras na UI — construir adjacência real é exatamente o rabbit hole "otimizador de rota" que este plano já proibia. - E14 (níveis N0–N4) não foi entregue; E12 e E16 foram. O gatilho do "não vá" no texto original lê
N0/N1: em v1 ele lê a classe do E10 ✓ (sem_base/marginal) somada a rede vazia. Em compensação,loadTerritoryOverview(E12 ✓) e o dossiê (E16 ✓) já existem e são reusados — o compositor linka o dossiê de cada parada, como previsto.
As-built 2026-07-27 — o que a implementação descobriu
Entregue como planejado (sem migration, sem collection, sem Consent, sem entidade tour), com oito desvios que valem registro:
- O número de headroom passou a ter uma dona só. O classificador do E10 calculava teto-do-campo-não-capturado e a mediana do catálogo por conta própria, e a checklist do E13 precisa exatamente dos mesmos dois números. Em vez de um segundo cálculo,
uncapturedFieldVotese as medianas (catalogMedianUncapturedFieldVotes/catalogMedianProjectedValidVotes, memoizadas) foram extraídas para municipalityPotential.ts e o classificador passou a consumi-las. Sem isso, o card "Elegibilidade" e a coluna "Classe" poderiam discordar na mesma tela — e ninguém teria como saber qual das duas contas estava certa. - A escrita precisou de uma checagem de escopo própria, fail-closed. O
createdeactivityé um booleano de staff — Payload não expressa restrição por município no create —, entãocreateTourDraftActivitiesRecordrelê os ids das paradas comoverrideAccess: falsedentro da transação e aborta se algum não voltar. Foi um teste de integração que expôs o buraco: sem essa releitura, um assessor podia semear rascunhos no estado inteiro postando ids que o compositor nunca lhe ofereceu. - "Encaixe em giro" não é avaliável para um município sozinho. A condição é uma afirmação sobre os vizinhos, então
loadMunicipalityVisitEligibilityroda o mesmo loader recortado no TI do município em vez de um segundo avaliador — é assim que o card do detalhe e a proposta do compositor não começam a discordar. - A ordem da composição é âncora → semente → satélites. A primeira versão preenchia satélites antes e a semente de expansão nunca aparecia, porque os satélites consumiam todas as vagas. Caps:
TOUR_MAX_SATELLITES3,MAX_TOUR_STOPS8 (um giro é um dia ou dois), verificado no cliente e revalidado na action. - O seletor de TI recebe cada href já serializado pelo RSC, não importa o serializador — a lição do B14: importar
buildTourComposerHrefnum client component arrastabahiaTerritories+municipalityCatalogpara o bundle do navegador. O href é dado da opção; o cliente só navega./campanha/atividades/girosfechou em 8,61 kB / 226 kB de First Load JS, na faixa das rotas irmãs; o detalhe do município ficou em 7,3 kB / 232 kB com o card novo. - Duas suítes foram fundidas, não acrescentadas. Arquivos separados para as views do planejador (unit) e para o compositor (int) empurraram as suítes para timeout; o conteúdo vive em
tests/unit/visitEligibility.unit.spec.tsetests/int/campaignActivity.int.spec.ts. As listas de ids (CALENDAR_PHASES,VISIT_CONDITIONS,VISIT_CONTRAINDICATIONS) são exportadas e varridas lá para provar que todo rótulo existe — umconstusado só como tipo é warning de ESLint, e a varredura é o motivo honesto de ele ser público. - O título do rascunho passou a sair do banco, não do formulário (
/simplify). A primeira versão compunha"<giro> — <município>"na borda doFormData, com o nome que o cliente havia enviado, e validava esse nome à mão. A action já relê os municípios das paradas para checar escopo — então ela passou a pedirnamenessa mesma leitura e a compor o título ali: o cliente só manda o id, que é a única coisa que autoriza algo. Com isso a validação artesanal das paradas virou um schema zod (tourStopDraftsSchema, colhido dos próprios campos deactivity) e a checagem de comprimento do título morreu —MAX_TOUR_NAME_LENGTH(90) mais o maior nome de município não alcança o limite de 160 do título. campaignMunicipalities.e2e.spec.tscontinua instável, e não é desta entrega. Com a árvore emgit stasho mesmo arquivo falha, em teste diferente a cada corrida (o flake já registrado no AGENTS.md). Gate verde no resto:tsc,lint,format:check,knip,check:cycles, 429 testes unit+int,build, e scan Aikido sem findings.
Design (Impeccable)
Âncoras: PRODUCT.md (princípios 2 e 5) / DESIGN.md (register product) · /campanha/atividades existente (C3: tabs, cards, filtros).
Na implementação: shape → craft → critique → harden (Fase 4: form/action nova + empty state novo) → polish (classe C). optimize fica fora — sem sinal de perf.
Brief compacto:
- Persona / contexto: coordenador montando a semana do candidato sob pressão de pedidos ("quem grita mais leva" — T5); precisa dizer não com critério. A restrição que ele nomeou na sessão de campo é "perna" — "não posso marcar um compromisso que eu não possa cumprir".
- Job principal: compor giros de 2–3 dias por território com municípios elegíveis — e tornar visível o que NÃO justifica visita.
- Estratégia de cor: Restrained; elegibilidade como checklist de 5 condições (✓/—), nunca score numérico com falsa precisão.
- Edit where you see: sim — criar
activity(comdeputyPresent) direto do município candidato. - Anti-goals: otimizador de rota (TSP/mapas de estrada); score 0–100 de município; agenda auto-aprovada; expor "não vá" com esse rótulo fora do staff (é despriorização — vocabulário duplo).
Dados → decisão → apresentação
- Decisão que a tela autoriza: "incluir este município no próximo giro, ou oferecer contra-oferta" — e, no detalhe do município, "vale a pena levar o candidato aqui?".
- Forma: checklist de 5 condições ✓/— por município + lista agrupada por TI. Nunca score numérico: falsa precisão é o erro documentado (docs/research §6.4) e a disciplina de exigir as cinco é o valor. Nenhum chart novo — a escada de pobreza para esta pergunta termina em texto e lista.
- Anti-goals (
PRODUCT.md§6): score 0–100, gauge de SaaS, quilometragem como métrica, % estadual absoluto, contagem bruta, sugestão automática sem humano no loop.
Contexto
Relatório §6.7: visita tem efeito modesto e o canal é mobilização do núcleo — "a visita vale o que a rede local converte dela; agenda é multiplicador de estrutura". Elegibilidade = 5 condições (volume, headroom, rede de recepção, janela política, encaixe em giro); o calendário muda o produto (construção jul–ago / consolidação set / ativação última semana); há "não vá" explícitos e intermediários ("mande o coordenador/vídeo/dobradinha"); padrões J-A (município elegível maduro sem visita), J-B (pedida vs. justificada — activity.origin de C12), J-C (composição do giro: âncora+satélites+semente, por agrupamento de TI). O /campanha/atividades (C3) já tem eventos com município/advisors/status; falta a camada de decisão de agenda. A sessão de campo de 2026-07-23 reforçou o item por dois lados: a restrição dominante nomeada é "perna"/agenda, e o pedido O6 de dossiê pré-agenda virou E16 ✓ (dossie-municipio.md) — o compositor de giro linka o dossiê de cada município do giro como preparação da visita.
Objetivos
- Elegibilidade por município: checklist das 5 condições derivadas — volume (E8), headroom (E8), rede (lideranças + assessor responsável), janela (dobradinha vinculada ou tendência não-desfavorável, mais nota livre), encaixe (mesmo TI de outra candidata) — exposta no município e numa lista "candidatas a visita" ordenada.
- Fase do calendário: rótulo automático (construção/consolidação/ativação por data) mudando o texto do "produto da visita" sugerido.
- Visão "Giros" em
/campanha/atividades/giros: compositor simples — escolher TI, ver âncora sugerida (maior estoque comprometido), satélites do mesmo TI e 1 semente de expansão (P12), gerar asactivityem rascunho comdeputyPresent. - J-B na prática: criar atividade a partir de pedido registra
origin: 'pedido_broker'+ contra-oferta sugerida em texto (coordenador/vídeo/parada em giro). - "Não vá" visível ao staff: município de classe
sem_base/marginalou sem rede aparece com a contraindicação citada (aviso, nunca bloqueio).
Decisões travadas
- Planejador compõe
activitys existentes; não cria entidade "giro" persistida na v1 — giro = agrupamento por TI+datas das atividades geradas. Rejeitado: collectiontournova (migration + access + UI por um agrupamento derivável; revisitar com gatilho). - Checklist ✓/— em vez de score numérico. Falsa precisão é o erro documentado (Hersh — §6.4); a disciplina das 5 condições é o valor. Rejeitado: score composto 0–100.
- Presença marcada por
deputyPresent, não por kind novo (ver Revisão 2026-07-27, achado 1). - Aviso sem gravação na v1. "A geografia serve à política" (T5-contraindicação), mas gravar o override exige o primeiro write path de
allocationDecision— adiado para o E11 com gatilho. Rejeitado: hard-block de municípios de classe baixa; e construir o write path aqui, fora do appetite. - Sub-rota, não tab nova. As tabs de
/campanha/atividadessão filtros de status traduzidos embuildActivityListWhere; um planejador não é um where-clause./campanha/atividades/girosalcançada por botão "Planejar giro", sem entrada nova no sidebar. Rejeitado: 5ª tab "Giros" (abriria exceção no contrato tab→where); rota de 1º nível/campanha/giros(mais uma entrada num sidebar com 9). - Cenário fixo
centralna conta de headroom, como o card do E8 já faz. Rejeitado: seletor de cenário no planejador. - i18n e naming:
visitEligibility,tourComposer,calendarPhase(construcao|consolidacao|ativacao),origin(C12); labels pt-BR.
Abordagem proposta
flowchart LR
Elig["visitEligibility<br/>(5 condições sobre E8/rede)"]
Phase["calendarPhase (data)"]
Cands["lista candidatas a visita<br/>(J-A ordenadas)"]
Comp["tourComposer<br/>(âncora + satélites do TI + semente)"]
Plans["activity em rascunho<br/>(origin, deputyPresent)"]
Elig --> Cands --> Comp --> Plans
Phase --> Cands
Componentes:
src/lib/visitPlannerAnchors.ts:CALENDAR_PHASE_ANCHORSnum único objeto versionado, no precedente deterritorialClassAnchors.ts— cortes ilustrativos, recalibração é diff de uma linha.src/utilities/visitEligibility.ts: as 5 condições + fase, puro sobre um input explícito (espelhaTerritorialClassInput), então detalhe e lista alimentam o mesmo avaliador de fontes diferentes.src/utilities/visitPlannerData.ts(server-only):loadVisitCandidatescom uma leitura própria demunicipality(overrideAccess: false) — nãoloadMunicipalityScope, cujo select é superset compartilhado do dashboard/mapa/lista e não carregapoliticalTrend/stateDeputies; alargá-lo cobraria de todos por dois campos que só o planejador usa. Contagem de lideranças em uma query com Map, no precedente deleadershipCountemorganizationData.ts.src/components/campaign/tour/*: compositor na sub-rota; gera rascunhos por uma action em transação única.- Detalhe do município: card compacto "Elegibilidade para visita" (checklist) com CTA "Planejar giro".
- Sem migration — nenhuma mudança de schema.
Cortes da v1
Decididos nesta sessão, com o gatilho de cada um:
- Override "não vá" gravando
allocationDecision→ E11 (que precisa do write path de qualquer forma). - Níveis N0–N4 no gatilho do "não vá" → E14; até lá, classe E10 + rede vazia.
- Padrões J como sugestão automática → E11 fase 2.
- Entidade
tourpersistida (com resultado do giro) → 3º giro real composto e time pedindo visão consolidada pós-giro.
Um débito de engenharia ficou registrado aqui em vez de virar ID, na triage do fechamento (2026-07-27):
- Um loader batelado da conta da cadeira. O planejador compõe
loadStatewideSuggestedGoals+computeGoalCoveragediretamente, como fazloadMunicipalityGoalCoverageBundle(E8) — são as mesmas primitivas, não um segundo orquestrador, e não há laço deawaitpor município. Mas os dois divergem de propósito no dado ausente: a lista preenche meta zero para um slug fora do catálogo (a linha existe e tem de aparecer) e o planejador descarta o município (não se propõe visita sem base para raciocinar). Um revisor do/simplifyleu isso como definição conflitante; é intencional, e por isso está dito aqui. Revisitar quando: um 3º consumidor compor essas primitivas fora demunicipalityGoalAccount.ts— aí a política de dado ausente passa a ser argumento do loader, em vez de convenção repetida em três lugares.
Dependências
- Duras: E8 ✓ (volume/headroom), C12 ✓ (
origin). Suaves: E16 ✓ dossiê (dossie-municipio.md — preparação da visita, link por município do giro), E12 ✓ (rollup de TI), E4R ✓ (semeoustateDeputiesepoliticalTrend, que sustentam a condição de janela), A6 dobradinha (janela política ganha dado real pós-15/08), E14 (níveis alimentam o "não vá" quando existirem). - Reusa:
/campanha/atividadesinteiro (C3), actions deactivity,bahiaTerritories.ts,municipalityCatalog.ts.
Não escopo
Roteirização fina/otimização de deslocamento (humano decide a ordem); sincronização com calendário externo (Google/ics); agenda do majoritário; padrões J como sugestões automáticas (E11 fase 2); registro de override em allocationDecision (E11); base de eventos municipais para janela política.
Rabbit holes
- Virar otimizador de rota. Agrupamento por TI + ordenação é o teto; qualquer "distância por estrada" explode o item — e o dado de adjacência não existe em runtime (achado 4).
- Compositor virar wizard de 6 passos. Escolher TI → revisar 4–6 municípios sugeridos → gerar rascunhos. Três interações.
- Janela política estruturada (datas de festas/feiras por município). Catálogo inexistente; proxy derivado + campo livre + conhecimento do assessor. Não construir base de eventos municipais.
Referências
docs/roadmap.md(Inteligência de campanha, E13) · plano-mestredocs/research/relatorio-entrevista-persona-campanha.md§6.7 (elegibilidade, fases, não-vá, J-A/J-B/J-C), Rodada 6 J1–J4 · CUSTOMER.md (restrição "perna", pedido O6)src/collections/Activity.ts(kinds/status/access,deputyPresent),src/app/(campaign)/campanha/(app)/atividades/(superfície C3)src/utilities/municipalityGoalAccount.ts,src/utilities/municipalityPotential.ts,src/utilities/municipalityTerritorialClass.ts,src/lib/bahiaTerritories.tsPRODUCT.md/DESIGN.md— âncoras da superfície nova- AGENTS.md — transações, access, naming