Há aproximadamente um mês, em 1º de abril, publiquei aqui um post anunciando o painel Eleições 2026: uma ferramenta para acompanhar como a imprensa brasileira cobre os pré-candidatos à presidência, com coleta automatizada, classificação de sentimento e tópicos por modelo de linguagem, e um resumo editorial publicado todo sábado. Naquele dia o banco tinha 1.710 artigos, monitorava 9 nomes e o dashboard tinha 5 páginas. Um mês depois, são 14.525 artigos, 12 pré-candidatos ativos e 13 páginas no ar.
Este post não é um changelog estendido — para isso existe a página Changelog do próprio painel. É uma narrativa do que mudou, do que funcionou, do que falhou e foi corrigido. A premissa do projeto desde o primeiro dia foi transparência e precisão; mostrar como o painel evoluiu — incluindo os erros públicos — é parte do contrato.
Os números, primeiro
Em 30 dias o banco saiu de 1.710 para 14.525 artigos — pouco mais de 8 vezes maior. As fontes únicas (veículos com pelo menos um artigo coletado) passaram de 333 para 1.004, o que é um crescimento de cerca de 3 vezes. Os pares de sentimento, que são as classificações artigo × candidato, foram de cerca de 1.400 para 18.900. Há 14.650 artigos com tópicos extraídos e 18.900 vínculos artigo × candidato.
O ponto que esses números evidenciam não é a quantidade absoluta — é a estabilização. Em 1º de abril o pipeline ainda processava em pequena escala; em 1º de maio ele rodou um backfill completo de 11 horas, de madrugada, sem intervenção, e entregou os números acima. O painel deixou de ser um protótipo e virou uma operação contínua.
A coleta: de 3 para 5 caminhos
No anúncio de abril, a coleta vinha por três frentes: Brave Search, Tavily e 32 feeds RSS, totalizando cerca de 90 fontes mapeadas. Hoje são cinco caminhos. Os RSS subiram para 50 feeds diretos. Adicionei Google News RSS, com uma query por pré-candidato e uma query geral sobre eleições, o que ampliou o alcance para veículos regionais que não tinham feed próprio mas estavam indexados. E entrou o coletor por sitemap dos veículos: para os principais portais, em vez de depender de busca, leio diretamente o sitemap diário e processo o que é novo. O coletor também foi reescrito para baixar o corpo completo de cada artigo, em vez de operar só sobre título e descrição.
O resultado é o salto de 90 para 1.004 fontes. Vale a observação: nem todas são veículos jornalísticos no sentido estrito — há blogs, agências de assessoria, sites partidários e canais de campanha no meio. A maioria, porém, é imprensa nacional e regional. A página Metodologia detalha o que entra e como cada fonte é tratada.
O leque de candidatos
Em 1º de abril o painel monitorava 9 nomes, com Eduardo Leite já marcado como dropped depois de Caiado lançar pré-candidatura pelo PSD em 30 de março. Em 1º de maio são 12 ativos e 4 descontinuados. Saíram, além de Leite, Tarcísio (que decidiu concorrer à reeleição e não se desincompatibilizou do governo de São Paulo) e Michelle Bolsonaro (que deve concorrer ao Senado, sem anúncio formal até o fechamento deste post). O histórico de todos eles segue preservado no banco — quem foi monitorado um dia continua consultável.
As adições seguiram o momento político da corrida presidencial: Aldo Rebelo (DC), Augusto Cury (Avante), Cabo Daciolo (Mobiliza Brasil), Hertz Dias (PSTU), Rui Costa Pimenta (PCO) e Samara Martins (UP). O leque agora cobre o espectro à esquerda (Lula, Hertz Dias, Rui Costa Pimenta, Samara Martins), no centro (Ciro, Aldo Rebelo) e à direita (Augusto Cury, Cabo Daciolo, Renan Santos, Caiado, Zema, Flávio Bolsonaro). O painel original era enviesado para os nomes mais cobertos pela imprensa — que é, em si, parte do que o painel deveria estar medindo. Cobrir candidaturas com menor exposição midiática foi uma decisão editorial: se quero analisar a cobertura, preciso incluir candidaturas que recebem pouca cobertura justamente para que esse “pouco” seja visível e mensurável.
O dashboard: 5 páginas, 13 páginas
A versão de 1º de abril tinha Início, Comparativo, Candidatos, Espectro Editorial e Metodologia. O chat com IA estava listado como “em construção”. Hoje as páginas, em ordem, são: Início, Cenário Geral, Cenário Mensal, Cenário Semanal, Pré-candidatos, Comparativo, Radar, Espectro Editorial, Zeitgeist, Análises, Metodologia, Changelog e Privacidade.
O salto editorial mais relevante foi separar três janelas temporais que tentavam conviver na mesma página. Cenário Geral mostra a série completa, desde 25 de março, e serve para olhar tendência. Cenário Mensal compara o mês civil corrente com o anterior, e é onde se enxerga inflexão. Cenário Semanal usa a semana corrida (segunda a domingo) e é a base do resumo de sábado. São três leituras diferentes do mesmo banco, e tentar empilhá-las numa só visualização gerava ruído sem ganho analítico.
A página Zeitgeist, lançada em 27 de abril, foi a sexta dimensão analítica adicionada ao pipeline — depois de sentimento, tópicos, gênero jornalístico, espectro editorial e enquadramento. Ela mede em que grau a cobertura de cada candidato dialoga com seis agendas globais estruturais (clima, IA, geopolítica, crise da democracia, entre outras). Já são 2.616 artigos com classificação de zeitgeist no banco, ainda em fase de calibragem.
A jornada dos modelos: três trocas em 30 dias
Esta é a parte mais instrutiva do mês. Comecei com Claude Haiku 4.5 via CLI Claude Code para classificar sentimento e extrair tópicos. Funcionou bem para validar conceito, mas o CLI tinha um problema: expirava silenciosamente em runs longos. Em 14 de abril migrei para o SDK direto da Anthropic, o que deu controle de timeout e logging muito melhor.
Em 18 de abril, pela manhã, tentei migrar a classificação para Sonnet 4.6 via SDK. A qualidade era visivelmente superior em casos ambíguos. Calculei a projeção mensal de custo para o volume que estávamos coletando — chegou a R$ 470 por mês. No mesmo dia, à tarde, reverti a decisão (commit 8266410). A hipótese foi testada, o número foi calculado, a decisão foi documentada.
Ainda em 18 de abril fiz a migração para Gemma 4 self-hosted via Ollama, em infraestrutura local, custo marginal zero por tokens. O coletor passou a rodar com modelo open-weight. Em 27 de abril, com a chegada do Gemini 2.5 Flash Lite Preview e logo do 3.1 Flash Lite Preview, voltei para uma API gerenciada — latência boa, qualidade próxima dos modelos de fronteira, custo viável.
O episódio que fechou o mês começou na madrugada de 30 de abril. Olhando o card “Candidato divisivo” do dashboard, identifiquei casos invertidos do Gemini 2.5: matérias em que o candidato era alvo de uma acusação apareciam classificadas como positivas. O modelo confundia “destaque no título” com “tom positivo” e não distinguia o autor da fala do alvo dela. Um smoke test mostrou que o 3.1 corrigia todos os casos do conjunto-teste. Decidi reclassificar o banco inteiro.
O backfill rodou das 22h36 do dia 30 até as 9h43 do dia 1º — cerca de 11 horas. Foram reclassificados 17.630 pares de sentimento, 13.088 artigos com tópicos, 13.086 com gênero e 11.265 com zeitgeist. O verificador, num passe paralelo, removeu 4.319 pares falsos-positivos (vínculos artigo × candidato em que a menção era tangencial demais para sustentar análise) e 918 artigos completamente falsos-positivos. Um snapshot pré-backfill ficou preservado, caso fosse preciso comparar.
A lição embutida nessas trocas é simples: nenhum modelo é a escolha definitiva. A decisão certa em 25 de março era Haiku via CLI; a decisão certa em 1º de maio é Gemini 3.1 Flash Lite com verificador. Trocar três vezes em 30 dias não é instabilidade — é a operação se calibrando contra o custo, a latência e os erros que só aparecem em escala.
Os erros que viraram correções públicas
O Changelog acumula bugs que merecem ser citados, porque cada um indica algo sobre como o painel é construído.
Recorte editorial vs. recorte de coleta (v3.8.2, 29/abr). A página filtrava artigos por fetched_at — a data em que o coletor pegou o artigo — e não por published_at, a data editorial da matéria. Como o coletor entrou em produção em 25 de março capturando publicações retroativas, 871 artigos publicados entre 25 e 30 de março estavam sumindo da Timeline numa janela rolling de 30 dias, e o problema crescia 1 dia por dia. Correção: recorte oficial fixo em 25/03/2026, filtrando por data de publicação.
Há um aprendizado lateral nesse mesmo bug: nem todos os veículos formatam datas de modo consistente, e alguns simplesmente não informam published_at. Aparecem RSS sem o campo published, HTML sem og:published_time nem article:published_time, e — pior — páginas que entregam a data do servidor em vez da data editorial, ou que reescrevem a data quando a matéria é republicada. Quando isso acontece, o pipeline cai em fallbacks (data do feed, data do scraping) e segue. Uma fração dos artigos termina com data deslocada em horas ou dias. Convivo com esse ruído porque o custo de descartar tudo que vem com data suja seria maior do que o ganho de precisão — mas é um lembrete de que dado público raramente é dado limpo.
KPI inflado (v3.9.1, 29/abr). O indicador “Candidatos cobertos” da Cenário Semanal contava o número de IDs únicos na tabela de vínculos da semana sem filtrar candidatos descontinuados. Resultado: o card mostrava 14, o grid abaixo mostrava 12. Os quatro dropped tinham vínculos históricos que ainda contavam. Pequeno, mas exatamente o tipo de inconsistência que corrói credibilidade.
Bug detectado por Playwright, não por monitoramento (v3.9.1, 29/abr). Depois de renumerar arquivos de página, o roteador continuava registrando os caminhos antigos. O Streamlit subia em HTTP 200, o monitoramento dizia “ok”, e o erro só aparecia ao tentar trocar de página. Lição: status code não é saúde funcional. Hoje há um teste de navegação que clica em cada item do menu.
Falso positivo removido à mão (v3.9.2, 29/abr). Uma matéria da Agência Brasil sobre uma pesquisa Datafolha de eleição passada apareceu vinculada a Cabo Daciolo. A republicação tinha atualizado o published_at do artigo, e o classificador, sem contexto, fez o vínculo. Os critérios automáticos do verificador não pegaram. Removi à mão e abri ticket para refinar o filtro de republicações.
Vale generalizar a partir desse caso: a remoção de falsos-positivos ainda depende de filtros manuais sobre o que os modelos deixam passar. Os classificadores acertam a maioria absoluta dos vínculos artigo × candidato, mas erram em situações ambíguas — republicações com data trocada, menções tangenciais, citações sem âncora clara, contexto irônico. Não existe pipeline puramente automatizado que dê conta dessa cauda; o olhar humano sobre os outliers continua sendo etapa indispensável, não vestígio de imaturidade do projeto. Modelos de linguagem são bons o suficiente para a primeira camada de classificação em escala — mas a confiança no número final exige que alguém olhe os casos estranhos com calma.
Incidente em produção, 30/abr, 21h00 BRT (v3.11.0). A primeira coleta após o deploy do snapshot atômico do banco rodou com bug. O script run_coleta.sh fazia source .env, que setava DB_PATH apontando para o arquivo público; o pipeline inteiro (coleta mais cinco produtores de classificação) leu esse env e escreveu direto no banco público em vez do privado. O bloco final do script faria a cópia privado→público, o que sobrescreveria os dados novos com a versão de 14 horas antes. Detectei a inconsistência no dashboard, parei o serviço, fiz dois backups defensivos do estado parcial, troquei os arquivos para preservar os 367 artigos novos, corrigi o override do DB_PATH no script (export imediatamente depois do source), subi a correção e disparei nova coleta às 21h18. Concluiu 22h19, com snapshot funcionando. Dezoito minutos do início ao fim do recovery, sem perda de dados. A lição: pipeline e leitor compartilhando o mesmo arquivo de ambiente precisa de override defensivo, ou — melhor — de arquivos de ambiente separados.
O snapshot atômico em si é a outra metade dessa entrega. O pipeline agora escreve em eleicoes2026.db (privado, de trabalho) e, ao final do passe de zeitgeist, faz uma cópia mais movimentação atômica para eleicoes2026-public.db. O dashboard e os scripts de resumo leem do snapshot público. Eliminei a janela de 30 a 85 minutos em que o dashboard mostrava dados parciais durante a coleta. O tradeoff é que o público pode ficar até cerca de 85 minutos defasado entre o início da coleta e o snapshot final — mas sempre coerente consigo mesmo.
O produto editorial paralelo
Em 28 dias publiquei, na categoria Eleições 2026, o anúncio inicial, quatro resumos semanais e o primeiro resumo mensal. Os semanais foram em 4, 11, 18 e 25 de abril; o mensal saiu em 28 de abril, cobrindo de 26 de março a 25 de abril, com 17.609 artigos no período analisado. Cada texto leu o banco que estava ali naquele momento — um banco que dobrava de tamanho a cada dez dias, mais ou menos.
O modelo editorial também subiu junto com o analítico. Comecei com Haiku para o resumo semanal, o que era suficiente para listar dados mas insuficiente para narrativa. Em 15 de abril troquei por Opus, que tem mais fôlego analítico. Em 28 de abril, para o resumo mensal, fui para Opus 4.7 com extended thinking (12 mil tokens de orçamento de pensamento) e ordem de seções dinâmica em função do volume de menções da semana. Há uma cadeia de fallback (Opus 4.7 → Opus 4.6 → Sonnet 4.6 → Haiku 4.5) para cobrir indisponibilidade pontual. O max_tokens da chamada subiu de 4.000 para 8.000 e depois para 28.000 (16K de saída visível mais 12K de thinking) ao longo do mês, depois de truncamentos visíveis em drafts anteriores. Como uma chamada com thinking, prompt longo e max_tokens alto passa de 10 minutos, o SDK precisou ser usado em modo stream(), sob pena de timeout.
O que não aconteceu
O post de 1º de abril listava um chat com IA “em construção, ainda não disponível publicamente”. Em 18 de abril descontinuei essa funcionalidade depois de fazer a conta de quanto custaria mantê-la com qualidade aceitável. Não havia volume de uso projetado que justificasse o gasto, e havia outras frentes (Cenário Mensal, Zeitgeist, snapshot atômico) com retorno editorial mais claro. Registro a decisão aqui pelo mesmo motivo que registro as correções de bug: cortar uma funcionalidade anunciada também é parte da história.
Coda: a virada de stack, três dias depois do aniversário
Este texto foi rascunhado em 1º de maio, mas finalizado três dias depois — e o que aconteceu nesses três dias merece registro. Rascunhar uma retrospectiva força a pergunta de qual fricção sobrou. A resposta foi: o Streamlit. Ele entregou rápido o protótipo de março e segurou a operação por 30 dias, mas a cada nova iteração visual exigia mais override paliativo contra um markup proprietário cheio de classes geradas (st-emotion-cache-*). Continuar nele significava brigar com o framework para impor o design system; sair dele significava reescrever o dashboard.
A decisão saiu na noite de 1º de maio. O cutover foi feito na manhã de 2 de maio. O dashboard hoje roda em FastAPI com Jinja2 e Plotly, em quatorze rotas servidas como HTML real, com URLs limpas (/cenario-geral em vez de /Cenario_Geral), sidebar custom com numerais nos itens, e cache em memória que invalida automaticamente quando o snapshot da coleta é trocado. A página mais pesada — Comparativo, com co-citação e bipartite de tópicos — caiu de 7 segundos para 700 milissegundos no cache hit. SEO real, primeira renderização rápida, e zero CSS-fight contra o framework.
Junto com o cutover saiu a página Privacidade, com 13 seções aderentes à LGPD em português — responsável pelo tratamento, dados coletados, finalidades, base legal, compartilhamento com Cloudflare e Google, transferência internacional, direitos do titular pelo Art. 18, retenção, segurança pelo Art. 48, proteção de crianças e adolescentes pelo Art. 14, contato com a ANPD. O painel passou a usar Google Analytics 4, com IP anonimizado e respeito ao cabeçalho Do Not Track do navegador, em paralelo ao Cloudflare Web Analytics que já roda no edge sem cookies. Os dois convivem.
No dia 3 de maio veio a última iteração visual: a marca SapiensLabs, que ocupava o topo da sidebar em wordmark vertical, virou assinatura discreta no rodapé, com link clicável para o site principal. E entrou cache-busting nas folhas de estilo — sem ele, qualquer alteração de CSS ficava invisível por até quatro horas no Cloudflare edge, mesmo após reload no navegador. Detalhe pequeno; impacto grande na velocidade de iteração.
O Streamlit segue ligado em standby na infraestrutura, como rede de segurança caso algo apareça nos primeiros dias do FastAPI. O arquivamento definitivo do código antigo está agendado para 16 de maio. A partir daí, o painel passa a ter uma única implementação. Fim da migração — e também o ponto em que considero o painel um produto estável, em vez de um protótipo em iteração contínua.
Para os próximos meses
A corrida presidencial vai até outubro de 2026. Faltam, em números redondos, cinco meses até o primeiro turno em outubro — mais um, em caso de segundo turno. O painel continua coletando todos os dias, o resumo continua saindo aos sábados, e o banco vai continuar crescendo no mesmo ritmo, ou mais rápido, à medida que as candidaturas se formalizarem e a cobertura se intensificar.
Quem quiser explorar diretamente, o painel está em eleicoes2026.sapienslabs.com.br. A página Metodologia documenta o pipeline atual e o Changelog registra cada alteração relevante, com data e versão. O próximo resumo semanal sai sábado.

Como esse post foi feito
Os números deste texto vêm do banco de dados do Eleições 2026 Dashboard — consultado em 1º de maio e atualizado em 3 de maio de 2026 — e do histórico do projeto registrado em commits, no Changelog público do dashboard e na página de Metodologia. Os resumos semanais e o resumo mensal citados estão publicados na categoria Eleições 2026 do SapiensLabs.
O texto editorial foi rascunhado por Claude Opus 4.7 com extended thinking e revisado por mim antes da publicação. As classificações de sentimento, tópicos, gênero e zeitgeist usadas nas análises do dashboard são feitas por Gemini 3.1 Flash Lite Preview (Google), com verificador anti-falsos-positivos. As imagens são geradas por Imagen 4.0 (Google). A análise editorial dos resumos semanais e mensais usa Opus 4.7 com extended thinking; este post sobre o aniversário do dashboard usa o mesmo modelo.