Preserve pledge history, type field signals, link plan demands, and record immutable ex-ante allocation decisions.
10 KiB
C12 — Registro-fundação (trajetória, sinais tipados, origem de visita, decisões ex-ante)
Status: entregue em código
Atualizado em: 2026-07-24 (implementação, migration, testes e simplify concluídos)
Item do roadmap: docs/roadmap.md (seção "Inteligência de campanha", C12; plano-mestre inteligencia-campanha.md)
Impeccable: B — sem rota nova; campos novos em forms existentes (municipalityUpdate, actionPlan) + collection admin-hidden
Appetite: ~2 dias eng; 1 migration consolidada
Responsável: —
Design (Impeccable)
Âncoras: PRODUCT.md (princípio 1 — registros próprios como fonte de verdade; princípio 7 — feel the action) / DESIGN.md (register product).
Na implementação: craft compacto → critique → polish (as superfícies são forms existentes; a collection allocationDecision não tem UI própria neste item — é escrita por actions e lida pelo E11/E15).
- Persona / contexto: assessor registrando "vereador esfriou" em 15 segundos no celular; coordenador registrando por que descartou uma sugestão.
- Job principal: transformar sinal oral em série datada sem virar formulário de 10 campos (anti-goal E1 do relatório: extração sem devolução).
- Estratégia de cor: Restrained.
- Edit where you see: sim — o log de 1 linha nasce do mesmo form de atualização já usado no detalhe do município.
- Anti-goals: formulário longo obrigatório; sobrescrita silenciosa de valores; segundo feed paralelo ao
municipalityUpdate.
Contexto
FU4 do relatório: backtest compara o que se sabia antes com o que aconteceu depois — "se o valor novo apaga o velho, o 'antes' deixa de existir". Hoje: votePledge guarda só o valor corrente (declarado/estimado sobrescrevem); municipalityUpdate é imutável mas sem semântica de sinal (kinds semanal|urgente|nota); actionPlan não registra origem da visita (pedida × justificada — J-B) nem custo; e não existe registro de decisão (sugestão dada, aceita/descartada, leitura alternativa). Este item fecha os gaps G1–G4 do plano-mestre e é pré-requisito do motor (E11) e do backtest (E15). "O TSE estará lá em novembro; o lado da campanha não é reconstruível retroativamente."
Objetivos
- G1 — trajetória de pledge:
versions: true(sem drafts) emvotePledge; leitura da série por município/liderança para o delta semanal (E8) e o backtest (E15). - G2 — sinais tipados:
municipalityUpdate.kindganhasinal; campos condicionaissignalType(invasao | esfriamento | visita_adversario | proposta_broker | outro),signalSource(texto curto),triangulated(checkbox staff-only). Continua imutável e mantendomunicipality.lastUpdateAt. - G3 — origem/custo de plano:
actionPlan.origin(dado | pedido_broker | obrigacao_politica, defaultdado, staff-only) + custo derivado decampaignDemand.costnas demandas vinculadas ao plano. A relação opcionalcampaignDemand.actionPlanjá existe e só aceita o mesmo município. - G4 — decisões ex-ante: collection
allocationDecision(admin groupCampanha,admin.hiddencomosupporterImportBatch):municipality,patternId(texto — P1…K-C oumanual),suggestion(texto),decision(aceita | descartada | adiada),alternativeReading(obrigatório ao descartar),snapshot(json — classificação/números no momento),decidedBy(derivado),note. Imutável após criação. - Access: tudo staff;
leadernão lê sinais staff-only (triangulated), não lêallocationDecision, não lêorigin/resourceCost— mesmo padrãocanReadCampaignStaffField. - Garantia transversal: nenhuma action nova sobrescreve histórico; escritas multi-collection com
withPayloadTransaction+req. - Aceite de campo (O7/O4 — CUSTOMER.md): registrar o delta do dia (ex.: "Cairu: ex-prefeito ligou, 1.000 → 300–500") em ≤30 segundos no celular, a partir do detalhe/lista — o pledge atualizado vira versão (G1) e/ou sinal de 1 linha (G2). É o momento Orolândia da sessão de 2026-07-23: ele edita o mapa em viagem; se o registro custar mais que a edição da planilha, o dado continua morrendo no ZAP.
Decisões travadas
- Histórico via Payload
versions, não collection de snapshots. Menos código, admin de graça, migration gerada. Rejeitado:pledgeSnapshotprópria (duplica access/hooks); campohistoryjson (não consultável, cresce sem índice). - Decisão registrada é collection própria e imutável — é o dado que o backtest paga caro para ter; mudanças de nível N0–N4 (E14) também gravam aqui, um mecanismo só. Rejeitado: log em
municipalityUpdate(semântica de feed de campo, autoria de liderança possível — decisão é de coordenação); campo nomunicipality(sobrescreve). - Sinal nasce no form existente de atualização (custo de digitação mínimo; "o campo dita, a sede registra" continua possível por quem tiver acesso). Rejeitado: collection nova de sinais (segundo feed; FU1 pede agregação, não outra caixa).
- i18n e naming:
signalType,signalSource,triangulated,origin,allocationDecision,patternId,alternativeReading; enums como valores de dado em pt (invasao,pedido_broker,aceita) por paridade com enums existentes (semanal,alta); labels pt-BR.
Questões em aberto
versionstambém emmunicipality(para foto da classificação)? Opções: sim | não — snapshot vai noallocationDecision.snapshot. Recomendação: não; o snapshot por decisão cobre o "ex-ante" com custo menor que versionar 435 municípios a cada edição.- Retenção de versions de
votePledge? Recomendação:maxPerDoc: 0(ilimitado) até a eleição; avaliar poda pós-E15.
Abordagem proposta
flowchart LR
Pledge["votePledge + versions"]
Update["municipalityUpdate kind=sinal<br/>(signalType/source/triangulated)"]
Plan["actionPlan origin + demandas/custo"]
Dec["allocationDecision<br/>(imutável, staff)"]
Series["leitores de série<br/>(E8 delta · E11 gatilhos · E15 backtest)"]
Pledge --> Series
Update --> Series
Plan --> Series
Dec --> Series
Componentes:
src/collections/VotePledge.ts:versions: true; sem mudança de campos.src/collections/MunicipalityUpdate.ts: opçãosinalno kind +bodycurto,signalType,signalSourceetriangulatedcondicional; access staff notriangulated.src/collections/ActionPlan.ts:origincomcanReadCampaignStaffField; custo é lido dascampaignDemandvinculadas, sem duplicar valor no plano.src/collections/AllocationDecision.ts(nova): imutável (update/delete admin-only),createdByderivado no hook (padrãoSupporter.createdBy).- Leituras e actions downstream: E8/E15 definem o leitor de versions conforme seu contrato; E11 define a action de criar
allocationDecisionjunto ao fluxo de sugestão. C12 entrega as estruturas e seus invariantes, sem abstrações sem consumidor. - Migration:
pnpm migrate:create add_intel_foundation— tabelas de versions dovotePledge, campos novos, collectionallocationDecision.
Dependências
- Dura: nenhuma pendente — deploy da remodelagem aplicado em produção (2026-07-23). Nenhuma de outro plano do programa (é a fundação; E8 usa se existir).
- Reusa:
withPayloadTransaction(payloadTransaction.ts),campaignAccess.ts, padrãocreatedBydeSupporter.ts,municipalityUpdatePageData.ts(feed).
Dados → decisão → apresentação
- Dados: sinais de rede, causalidade da visita e custo de demandas ligadas ao plano.
- Decisão: registrar um fato de campo datado, distinguir visita justificada de pedido político e julgar o recurso já comprometido com cada plano.
- Forma: formulário curto e feed cronológico com badges de tipo/fonte/triangulação; lista de demandas e soma em R$ no detalhe do plano. Sem KPI, gráfico ou mapa nesta fatia.
- Anti-goals: segundo feed, formulário de inteligência longo, dashboard de custos ou métrica estadual absoluta.
Não escopo
- Computar gatilhos sobre os sinais (E11); UI de leitura da trajetória (E8 delta/E15); nível N0–N4 em si (E14 — só o registro da mudança mora aqui); notificações de sinal (D2).
Rabbit holes
- "Já que estamos", versionar tudo. Versions em
municipality/leadershipmultiplica tabelas e ruído de admin. Mitigação: sóvotePledgeneste item; demais têm gatilho abaixo. - Sinal virar formulário de inteligência. 3 campos, um deles opcional; qualquer campo além disso é feature do E11, não do registro.
snapshotvirar dump gigante. Limitar ao objeto pequeno (classe, cobertura, nível, números-chave) — não serializar o bundle do município.
Adiado com gatilho
- Versions em
municipality(histórico de estratégia). Gatilho: E15 precisar de foto além doallocationDecision.snapshotem caso real. - Import de sinais via WhatsApp/copy-paste estruturado. Gatilho: volume real de sinais >20/semana com reclamação de digitação.
Revisões
- 2026-07-24: G3 passa a reutilizar
campaignDemand.actionPlan+campaignDemand.cost, já existentes; removidoresourceCostduplicado no plano. Leitores de versions e action de decisão seguem para E8/E15 e E11, respectivamente. - 2026-07-24: implementação concluída com migration consolidada
20260724_180000_add_campaign_foundation_records, testes de integração e jornada E2E; débitos de escala plano↔demandas absorvidos em C11.
Referências
docs/roadmap.md(Inteligência de campanha, C12) · plano-mestre (gaps G1–G4)docs/research/relatorio-entrevista-persona-campanha.mdFU4 (5 registros; nunca sobrescrever), FU1 (anedota estrutural), §6.4 (falso positivo de registro), J-B (origem da visita)src/collections/VotePledge.ts,src/collections/MunicipalityUpdate.ts,src/collections/ActionPlan.ts,src/collections/Supporter.ts(padrão createdBy/access),src/collections/SupporterImportBatch.ts(padrão admin-hidden)src/utilities/payloadTransaction.ts,src/utilities/campaignAccess.ts- AGENTS.md — migrations, transações com
req, access fail-closed