<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="pt-BR"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://calixtoneto.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://calixtoneto.com/" rel="alternate" type="text/html" hreflang="pt-BR" /><updated>2026-10-06T23:50:55-03:00</updated><id>https://calixtoneto.com/feed.xml</id><title type="html">João Calixto</title><subtitle>Engenheiro de Software Sênior especializado em Java, microsserviços de alto volume, sistemas distribuídos, Clean Architecture e cloud nos setores financeiro e de seguros.</subtitle><author><name>João Calixto</name></author><entry><title type="html">Clean Architecture &amp;amp; IA: visão geral da série</title><link href="https://calixtoneto.com/blog/2026/10/clean-architecture-e-ia-visao-geral/" rel="alternate" type="text/html" title="Clean Architecture &amp;amp; IA: visão geral da série" /><published>2026-10-06T00:00:00-03:00</published><updated>2026-10-06T00:00:00-03:00</updated><id>https://calixtoneto.com/blog/2026/10/clean-architecture-e-ia-visao-geral</id><content type="html" xml:base="https://calixtoneto.com/blog/2026/10/clean-architecture-e-ia-visao-geral/"><![CDATA[<p>Esta é a abertura da série <strong>Clean Architecture &amp; IA: Engenharia de Restrições na Era dos LLMs</strong>.</p>

<h2 id="a-tese">A tese</h2>

<p>Agentes geram código mais rápido do que qualquer pessoa consegue revisar linha por linha. A saída não é abrir mão da revisão, e sim <strong>automatizar tudo o que é verificável</strong> (estrutura, tipos, comportamento e qualidade dos testes) e reservar o olhar humano para o que a máquina não verifica: intenção, modelagem de domínio e trade-offs.</p>

<h2 id="artigos">Artigos</h2>

<ol>
  <li>O fim da revisão linha por linha</li>
  <li>Arquitetura de restrições no backend Java (Spring Boot e legado Java EE)</li>
  <li>Fronteiras e contratos no frontend Angular</li>
  <li>Testes estritos como quality gates</li>
  <li>O manifesto do engenheiro orquestrador</li>
</ol>

<p>Todos os exemplos evoluem em um mesmo repositório, com código que você pode rodar.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Exemplo de restrição: o domínio não conhece o Spring</span>
<span class="nd">@ArchTest</span>
<span class="kd">static</span> <span class="kd">final</span> <span class="nc">ArchRule</span> <span class="n">dominio_sem_spring</span> <span class="o">=</span>
    <span class="n">noClasses</span><span class="o">().</span><span class="na">that</span><span class="o">().</span><span class="na">resideInAPackage</span><span class="o">(</span><span class="s">"..domain.."</span><span class="o">)</span>
        <span class="o">.</span><span class="na">should</span><span class="o">().</span><span class="na">dependOnClassesThat</span><span class="o">().</span><span class="na">resideInAPackage</span><span class="o">(</span><span class="s">"org.springframework.."</span><span class="o">);</span>
</code></pre></div></div>]]></content><author><name>João Calixto</name></author><summary type="html"><![CDATA[Por que restrições automatizadas valem mais que revisão linha por linha, e o que esta série vai cobrir.]]></summary></entry><entry><title type="html">Dois mapas de votação, zero reais de infraestrutura: dados abertos + LLM + GitHub Pages</title><link href="https://calixtoneto.com/blog/2026/10/mapas-de-voto-com-dados-abertos-e-llm/" rel="alternate" type="text/html" title="Dois mapas de votação, zero reais de infraestrutura: dados abertos + LLM + GitHub Pages" /><published>2026-10-06T00:00:00-03:00</published><updated>2026-10-06T00:00:00-03:00</updated><id>https://calixtoneto.com/blog/2026/10/mapas-de-voto-com-dados-abertos-e-llm</id><content type="html" xml:base="https://calixtoneto.com/blog/2026/10/mapas-de-voto-com-dados-abertos-e-llm/"><![CDATA[<p>Em 5 de outubro de 2026, um dia depois do primeiro turno, criei dois repositórios: o <strong>Mapa do voto · Paraíba</strong> e o <strong>Mapa do voto · Bayeux</strong>. Dois sites públicos, com votos de presidente a vereador (presidente na Paraíba, vereador em Bayeux), histórico de eleições desde 2012, comparação de um candidato entre eleições e votos por escola. Sem servidor, sem banco de dados e sem custo.</p>

<p>Fiz os dois com ajuda de um LLM. Este artigo mostra como o projeto é montado, de onde vêm os dados, como o mapa é desenhado e como tudo é publicado de graça. A ideia é que você consiga repetir para a sua cidade, o seu estado ou qualquer outro assunto com dados abertos.</p>

<ul>
  <li>Paraíba: <a href="https://calixtoneto.github.io/mapa-do-voto-pb/">site</a> · <a href="https://github.com/CalixtoNeto/mapa-do-voto-pb">código</a></li>
  <li>Bayeux: <a href="https://calixtoneto.github.io/mapa-do-voto-bayeux/">site</a> · <a href="https://github.com/CalixtoNeto/mapa-do-voto-bayeux">código</a></li>
</ul>

<p><img src="/assets/img/blog/pb-uso.gif" alt="Mapa da Paraíba: escolhendo eleição, cargo e candidato, e vendo os votos por escola" /></p>

<h2 id="o-que-os-sites-fazem">O que os sites fazem</h2>

<ul>
  <li><strong>Paraíba:</strong> votos de presidente, governador, senador, deputado federal e deputado estadual nos 223 municípios. Ao clicar num município, aparecem os votos por escola (local de votação).</li>
  <li><strong>Bayeux:</strong> votos de vereador, deputados, senador e governador <strong>por bairro</strong>, de 2012 a 2026.</li>
  <li><strong>Evolução do candidato:</strong> quando a mesma pessoa disputou mais de uma eleição, o site mostra os votos em cada uma e, no mapa, onde ela ganhou e perdeu votos.</li>
</ul>

<p><img src="/assets/img/blog/bayeux-comparar.gif" alt="Comparando a votação de um candidato entre eleições em Bayeux" /></p>

<h2 id="a-regra-do-jogo-nada-que-custe-dinheiro">A regra do jogo: nada que custe dinheiro</h2>

<p>Antes de escrever código, defini restrições. Elas decidem toda a arquitetura:</p>

<table>
  <thead>
    <tr>
      <th>Peça</th>
      <th>Solução</th>
      <th>Custo</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Fonte de dados</td>
      <td>Portal de Dados Abertos do TSE, API de resultados do TSE, IBGE</td>
      <td>R$ 0</td>
    </tr>
    <tr>
      <td>Banco de dados</td>
      <td>Nenhum. Arquivos JSON commitados no repositório</td>
      <td>R$ 0</td>
    </tr>
    <tr>
      <td>Backend</td>
      <td>Nenhum. O site é 100% estático</td>
      <td>R$ 0</td>
    </tr>
    <tr>
      <td>Atualização dos dados</td>
      <td>GitHub Actions (disparo manual)</td>
      <td>R$ 0 em repositório público</td>
    </tr>
    <tr>
      <td>Hospedagem</td>
      <td>GitHub Pages</td>
      <td>R$ 0</td>
    </tr>
    <tr>
      <td>Bibliotecas</td>
      <td>Preact + htm e TopoJSON via CDN, sem etapa de build</td>
      <td>R$ 0</td>
    </tr>
  </tbody>
</table>

<p>O ponto central é este: <strong>o navegador de quem visita nunca chama o TSE</strong>. Os dados são gerados uma vez, viram arquivos, e o site só lê arquivos. Isso elimina servidor, limite de requisições e dependência de uma API que pode cair no dia da eleição.</p>

<h2 id="passo-1-de-onde-vêm-os-dados">Passo 1: de onde vêm os dados</h2>

<p>Os resultados eleitorais são públicos, mas espalhados. O gerador (<code class="language-plaintext highlighter-rouge">scripts/gerar-dados.mjs</code>) combina três fontes, nesta ordem de preferência:</p>

<ol>
  <li><strong>CSV dos Dados Abertos do TSE</strong>, com a votação nominal por município e zona. O presidente vem do arquivo nacional do mesmo .zip.</li>
  <li><strong>API de resultados do TSE</strong>, só para o que o CSV ainda não tem, como um segundo turno recém-apurado. A API guarda apenas o ciclo atual e o anterior, então ela não serve como fonte do histórico.</li>
  <li><strong>CSV por seção eleitoral</strong>, para o detalhe por local de votação.</li>
</ol>

<p>Cada eleição vira um arquivo <code class="language-plaintext highlighter-rouge">ANO-tTURNO.json</code> em <code class="language-plaintext highlighter-rouge">public/data/eleicoes/</code>. Um <code class="language-plaintext highlighter-rouge">index.json</code> lista o que existe, e o site monta o menu de eleições a partir dele. Para ligar o mesmo candidato entre eleições, o gerador cria um <code class="language-plaintext highlighter-rouge">pessoas.json</code> usando o nome completo registrado no TSE.</p>

<p>Os arquivos por eleição ficam entre 1 e 2 MB. O detalhe por escola fica em arquivos separados (<code class="language-plaintext highlighter-rouge">ANO-tTURNO-locais.json</code>) que só são baixados quando o visitante clica num município. Assim a página inicial carrega rápido.</p>

<h2 id="passo-2-coletar-sem-derrubar-o-servidor-dos-outros">Passo 2: coletar sem derrubar o servidor dos outros</h2>

<p>Baixar dados públicos de um órgão do governo pede educação. O módulo <code class="language-plaintext highlighter-rouge">scripts/lib/tse.mjs</code> tem três cuidados simples:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// poucas requisições em paralelo</span>
<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">paralelo</span><span class="p">(</span><span class="nx">itens</span><span class="p">,</span> <span class="nx">limite</span><span class="p">,</span> <span class="nx">fn</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>

<span class="c1">// retentativa com espera crescente em erro de rede, 429 e 5xx</span>
<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">getJson</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="p">{</span> <span class="nx">tentativas</span> <span class="o">=</span> <span class="mi">4</span><span class="p">,</span> <span class="nx">esperaMs</span> <span class="o">=</span> <span class="mi">1500</span> <span class="p">}</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>

<span class="c1">// arquivo grande vai para o disco e é reaproveitado nas próximas execuções</span>
<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nx">baixar</span><span class="p">(</span><span class="nx">url</span><span class="p">,</span> <span class="nx">destino</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
</code></pre></div></div>

<p>O gerador também se identifica com um <code class="language-plaintext highlighter-rouge">User-Agent</code> próprio. Os .zip do TSE são lidos em streaming, linha a linha, com a biblioteca <code class="language-plaintext highlighter-rouge">fflate</code>, sem carregar tudo na memória. Os CSVs vêm em Windows-1252 com campos entre aspas e separados por ponto e vírgula, então o parser é feito à mão e sabe lidar com as aspas.</p>

<h2 id="passo-3-a-geometria-do-mapa">Passo 3: a geometria do mapa</h2>

<p><strong>Paraíba.</strong> O contorno dos 223 municípios vem de um GeoJSON do IBGE (repositório <code class="language-plaintext highlighter-rouge">geodata-br</code>). Um script (<code class="language-plaintext highlighter-rouge">gerar-malha.mjs</code>) renomeia municípios cuja denominação mudou desde a malha de 2010 e passa tudo pelo <code class="language-plaintext highlighter-rouge">mapshaper</code>, que simplifica a geometria em 12% e exporta TopoJSON. O resultado é um arquivo pequeno, suficiente para desenhar o estado inteiro no navegador.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">execFileSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">npx</span><span class="dl">'</span><span class="p">,</span> <span class="p">[</span><span class="dl">'</span><span class="s1">mapshaper</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">tmp/pb.geojson</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">-simplify</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">12%</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">keep-shapes</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">-rename-layers</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">municipios</span><span class="dl">'</span><span class="p">,</span>
  <span class="dl">'</span><span class="s1">-o</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">format=topojson</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">quantization=1e5</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">public/data/pb.topo.json</span><span class="dl">'</span><span class="p">]);</span>
</code></pre></div></div>

<p><strong>Bayeux e a pergunta dos bairros.</strong> Aqui apareceu o problema mais interessante: o TSE <strong>não publica votos por bairro</strong>. O que existe é:</p>

<ul>
  <li>o voto por <strong>seção eleitoral</strong>;</li>
  <li>uma tabela “Eleitorado por local de votação”, que diz, para cada seção, em qual escola ela funciona, em que bairro (<code class="language-plaintext highlighter-rouge">NM_BAIRRO</code>) e com quais coordenadas.</li>
</ul>

<p>Juntando as duas, cada seção soma seus votos no bairro do local onde funciona. O site desenha um círculo por bairro, no centro dos locais de votação dele, com o tamanho proporcional aos votos do candidato. Por cima vai o contorno do município, vindo da API de malhas do IBGE.</p>

<p>Isso tem uma consequência que o site avisa e que vale repetir aqui: o mapa mostra <strong>onde o voto foi depositado, não onde o eleitor mora</strong>. É uma boa aproximação para comparar regiões da cidade, mas não é o endereço do eleitor.</p>

<h2 id="passo-4-o-front-end-sem-build">Passo 4: o front-end sem build</h2>

<p>A pasta <code class="language-plaintext highlighter-rouge">public/</code> é o site. Não há Node em produção, nem webpack, nem etapa de build. O <code class="language-plaintext highlighter-rouge">index.html</code> carrega duas bibliotecas por CDN:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/htm@3.1.1/preact/standalone.umd.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://cdn.jsdelivr.net/npm/topojson-client@3.1.0/dist/topojson-client.min.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"js/app.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>O mapa é desenhado num <code class="language-plaintext highlighter-rouge">&lt;canvas&gt;</code> 2D. Para saber em qual município o visitante clicou, o código guarda o <code class="language-plaintext highlighter-rouge">Path2D</code> de cada um e testa o ponto com <code class="language-plaintext highlighter-rouge">isPointInPath</code>. Isso funciona bem para 223 polígonos e permite zoom e arrasto sem biblioteca de mapas.</p>

<p>Preact com <code class="language-plaintext highlighter-rouge">htm</code> dá a ergonomia de componentes sem JSX e sem compilar nada. Para um projeto desse tamanho (cerca de 600 linhas de JavaScript no app inteiro), é suficiente.</p>

<h2 id="passo-5-publicar-de-graça">Passo 5: publicar de graça</h2>

<p>A publicação é um workflow de poucas linhas que envia a pasta <code class="language-plaintext highlighter-rouge">public/</code> para o GitHub Pages a cada <code class="language-plaintext highlighter-rouge">push</code> na <code class="language-plaintext highlighter-rouge">main</code>:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/upload-pages-artifact@v3</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">public</span>
<span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/deploy-pages@v4</span>
</code></pre></div></div>

<p>Para as próximas eleições há um segundo workflow, de disparo manual: em <strong>Actions → Atualizar dados de uma eleição → Run workflow</strong>, informa-se o ano. Ele roda o gerador, commita os JSON novos e dispara a publicação. Em repositório público, o GitHub Actions e o Pages não cobram nada. O Pages tem limites (1 GB por site e uso moderado de banda), que um projeto como este, com cerca de 13 MB de dados, não chega perto de tocar.</p>

<h2 id="como-a-ia-entrou-nisso">Como a IA entrou nisso</h2>

<p>A ideia veio da minha época de estágio no Tribunal de Contas da União. Lá eu trabalhei com R, Python e banco de dados, tratando bases de dados abertos e transformando em gráficos e mapas para análises, inclusive com dados abertos da Paraíba. Fiz isso em R Markdown: um arquivo para tratar os dados, outro para os gráficos e outro para um mapa por município.</p>

<p>Anos depois, a pergunta foi outra: <strong>quão prático e rápido seria repetir esse tipo de trabalho hoje, com um LLM ao lado?</strong> O exercício que escolhi foi o das eleições. Eu queria ler os dados do TSE e montar mapas para ver como cada município do meu estado votou, analisar a votação de cada candidato e acompanhar a evolução dele entre uma eleição e outra: onde ganhou votos, onde perdeu. É a mesma lógica das análises que eu fazia no TCU, só que agora publicada num site que qualquer pessoa abre.</p>

<p>O que mudou foi a forma de trabalhar. Em vez de escrever tudo do zero, fui em passos pequenos e verificáveis, e o histórico de commits mostra a ordem:</p>

<ol>
  <li>um gerador que baixa e agrega os dados de <strong>um</strong> ano;</li>
  <li>o mesmo gerador para vários anos;</li>
  <li>o site lendo os arquivos gerados;</li>
  <li>a lista de eleições montada sozinha, sem enviar arquivo;</li>
  <li>votos por escola;</li>
  <li>comparação entre eleições.</li>
</ol>

<p>A IA escreve rápido a parte mecânica: parser de CSV, leitura de .zip, componentes, workflows. O desenho e a análise continuam sendo do analista: o que perguntar aos dados, como juntar as fontes, o que o número significa. A sensação de quem já passou por R Markdown e consultas SQL é de que a parte trabalhosa deixou de ser o código e passou a ser entender os dados.</p>

<h2 id="o-que-a-ia-não-resolve-sozinha">O que a IA não resolve sozinha</h2>

<p>Quase todos os problemas reais deste projeto estão nos dados, não no código. Os que apareceram:</p>

<ul>
  <li><strong>Nomes de municípios mudaram</strong> depois da malha do IBGE de 2010, então foi preciso um mapa de correção (<code class="language-plaintext highlighter-rouge">NOMES_ATUAIS</code>).</li>
  <li><strong>O CSV por seção vem com <code class="language-plaintext highlighter-rouge">#NULO#</code></strong> no nome de alguns locais em 2026, e o nome tem de ser buscado na tabela de locais.</li>
  <li><strong>Candidaturas anuladas</strong> deixam votos que não devem entrar na soma por local.</li>
  <li><strong>O presidente não está no arquivo estadual</strong>, só no nacional.</li>
  <li><strong>A API do TSE só guarda dois ciclos</strong>, então o histórico precisa ser gerado e guardado por você.</li>
  <li><strong>Comparar cargos diferentes engana.</strong> Em ano de dois senadores, cada eleitor vota duas vezes, então a variação de votos não mede crescimento de apoio. O site avisa quando você compara cargos ou turnos diferentes.</li>
</ul>

<p>Nenhum desses pontos aparece se você só aceita o primeiro código gerado. Todos aparecem quando você confere o resultado contra os números oficiais. É por isso que a regra continua sendo a mesma de sempre: <strong>LLM acelera, mas o resultado precisa ser verificável</strong>. A verificação possível aqui é comparar totais por município e por bairro com o que o próprio TSE publica.</p>

<h2 id="como-repetir-para-a-sua-cidade">Como repetir para a sua cidade</h2>

<ol>
  <li>Escolha o recorte (município ou estado) e ache o código do TSE e o do IBGE dele.</li>
  <li>Baixe um ano de votação por município e zona nos Dados Abertos do TSE e veja as colunas.</li>
  <li>Escreva um gerador que transforme o CSV em JSON por eleição.</li>
  <li>Baixe o contorno no IBGE e simplifique com <code class="language-plaintext highlighter-rouge">mapshaper</code>.</li>
  <li>Faça uma página estática que leia os JSON e desenhe o mapa.</li>
  <li>Publique a pasta no GitHub Pages.</li>
</ol>

<p>Os dois repositórios estão abertos. Para uma nova cidade, o ponto de partida é o de Bayeux: troque o código do município e rode <code class="language-plaintext highlighter-rouge">npm run dados</code> e <code class="language-plaintext highlighter-rouge">npm run eleicoes</code>.</p>

<h2 id="o-que-fica">O que fica</h2>

<p>Há alguns anos, um projeto assim exigiria servidor, banco, deploy e uma conta mensal. Hoje cabe em uma pasta de arquivos estáticos, em um repositório público e em pouco mais de um dia de trabalho com um assistente de código. Dados abertos existem, hospedagem estática é gratuita, e um LLM encurta a distância entre ter a ideia e ter o site no ar. O trabalho que sobra, e que importa, é entender os dados e conferir o resultado.</p>

<h2 id="extra-refatorando-com-uncle-bob-pensando-no-agente">Extra: refatorando com Uncle Bob, pensando no agente</h2>

<p>O site ficou no ar, mas o código foi escrito para ficar pronto rápido, não para durar. O gerador da Paraíba (<code class="language-plaintext highlighter-rouge">scripts/gerar-dados.mjs</code>) tinha 251 linhas num arquivo só, funções de 33 a 39 linhas, 22 linhas com mais de 120 caracteres e <strong>nenhum teste</strong>. Funciona, mas cada mudança dependia de rodar tudo contra o TSE e conferir na mão.</p>

<p>Bayeux tinha o mesmo problema em dois scripts: <code class="language-plaintext highlighter-rouge">gerar-dados.mjs</code> (163 linhas) e <code class="language-plaintext highlighter-rouge">gerar-secoes.mjs</code> (117 linhas), que ainda carregava a própria cópia do parser de CSV e do leitor de .zip.</p>

<p>Os princípios de <em>Clean Code</em> do Uncle Bob ganharam uma leitura nova com agentes de IA escrevendo boa parte do código. Usei três deles como roteiro para refatorar os dois geradores, com o próprio agente fazendo o trabalho. Cada repositório recebeu um pull request (<a href="https://github.com/CalixtoNeto/mapa-do-voto-pb/pull/1/commits">Paraíba</a>, <a href="https://github.com/CalixtoNeto/mapa-do-voto-bayeux/pull/1/commits">Bayeux</a>) com três commits que dá para ler na ordem: a trava, a refatoração e o CI.</p>

<h3 id="1-primeiro-a-trava-depois-a-refatoração">1. Primeiro a trava, depois a refatoração</h3>

<p>O ciclo de refatoração do Uncle Bob tem uma condição: só se mexe em código coberto por teste. Para código gerado por máquina isso vale ainda mais. O agente precisa conseguir rodar os testes <strong>sozinho</strong>, sem pedir nada a ninguém, para saber se a mudança dele quebrou algo.</p>

<p>Aqui apareceu a primeira dificuldade: a rede desta sessão não chegava ao TSE. Isso acabou sendo bom, porque obrigou o teste a ser offline desde o início. A solução foi um teste de caracterização (<em>golden master</em>), escrito <strong>contra o código antigo, antes de qualquer mudança</strong>:</p>

<ol>
  <li>Um cenário pequeno de 2022, com os mesmos formatos do TSE: CSV com aspas, ponto e vírgula e Windows-1252, dentro de .zip.</li>
  <li>Os .zip vão para <code class="language-plaintext highlighter-rouge">tmp/</code>, onde o gerador já procura antes de baixar. Nenhum download acontece.</li>
  <li>Um <code class="language-plaintext highlighter-rouge">fetch</code> falso, carregado com <code class="language-plaintext highlighter-rouge">node --import</code>, responde como a API de resultados para o 2º turno.</li>
  <li>O script roda inteiro e a saída é gravada em <code class="language-plaintext highlighter-rouge">test/fixtures/esperado/</code>.</li>
</ol>

<p>O cenário passa de propósito pelos casos difíceis da seção anterior: presidente só no arquivo nacional, nome de local <code class="language-plaintext highlighter-rouge">#NULO#</code>, candidatura anulada, voto de legenda, branco, outra UF, município sem código IBGE e 2º turno vindo da API. Conferi a saída gravada à mão antes de confiar nela.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// test/fixtures/fetch-falso.mjs: URL fora do arquivo responde 404,</span>
<span class="c1">// que é como o TSE responde a um arquivo que ainda não existe.</span>
<span class="nx">globalThis</span><span class="p">.</span><span class="nx">fetch</span> <span class="o">=</span> <span class="k">async</span> <span class="nx">url</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">corpo</span> <span class="o">=</span> <span class="nx">respostas</span><span class="p">[</span><span class="nb">String</span><span class="p">(</span><span class="nx">url</span><span class="p">)];</span>
  <span class="k">return</span> <span class="nx">corpo</span> <span class="p">?</span> <span class="nx">Response</span><span class="p">.</span><span class="nx">json</span><span class="p">(</span><span class="nx">corpo</span><span class="p">)</span> <span class="p">:</span> <span class="k">new</span> <span class="nx">Response</span><span class="p">(</span><span class="dl">'</span><span class="s1">não encontrado</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">status</span><span class="p">:</span> <span class="mi">404</span> <span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>

<p>Em Bayeux, o cenário tem duas eleições: 2024 pelo CSV por seção e 2026 só pela API, que é o caminho de quando o TSE ainda não publicou o arquivo por seção. Ele também cobre o que é próprio dos bairros: “CENTRO” e “Centro” no mesmo bairro, coordenada com vírgula decimal, coordenada fora do município e seção que não está na tabela de locais.</p>

<p>Esse teste foi o primeiro commit nos dois repositórios. Só depois dele o código começou a mudar.</p>

<h3 id="2-funções-e-arquivos-curtos-como-contrato-de-contexto">2. Funções e arquivos curtos como contrato de contexto</h3>

<p>A leitura atual do “funções pequenas” é que um bloco de 10 a 20 linhas cabe inteiro na atenção do modelo. Vale ser preciso aqui: um LLM lê 251 linhas sem esforço. O ganho real está em outro lugar. Com unidades pequenas, o agente consegue <strong>ler só o que vai mudar, mudar só isso e testar só isso</strong>. A edição fica cirúrgica, o diff fica pequeno e a revisão humana fica possível.</p>

<p>Antes, uma função lia o .zip, filtrava a linha, convertia o código do município, somava o voto e corrigia a situação do candidato, tudo junto:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">ibge</span> <span class="o">=</span> <span class="nx">ibgeDe</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="nx">ix</span><span class="p">.</span><span class="nx">CD_MUNICIPIO</span><span class="p">]);</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ibge</span><span class="p">)</span> <span class="p">{</span> <span class="nx">semMun</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">c</span><span class="p">[</span><span class="nx">ix</span><span class="p">.</span><span class="nx">CD_MUNICIPIO</span><span class="p">]);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span>
<span class="kd">const</span> <span class="nx">turno</span> <span class="o">=</span> <span class="nx">c</span><span class="p">[</span><span class="nx">ix</span><span class="p">.</span><span class="nx">NR_TURNO</span><span class="p">],</span> <span class="nx">tk</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">ano</span><span class="p">}</span><span class="s2">|</span><span class="p">${</span><span class="nx">turno</span><span class="p">}</span><span class="s2">|</span><span class="p">${</span><span class="nx">cargo</span><span class="p">}</span><span class="s2">`</span><span class="p">,</span> <span class="nx">key</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">tk</span><span class="p">}</span><span class="s2">|</span><span class="p">${</span><span class="nx">c</span><span class="p">[</span><span class="nx">ix</span><span class="p">.</span><span class="nx">SQ_CANDIDATO</span><span class="p">]}</span><span class="s2">`</span><span class="p">;</span>
<span class="kd">const</span> <span class="nx">ds</span> <span class="o">=</span> <span class="nx">porTurno</span><span class="p">[</span><span class="nx">turno</span><span class="p">]</span> <span class="o">||=</span> <span class="nx">novoDs</span><span class="p">(</span><span class="dl">'</span><span class="s1">csv</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<p>Depois, cada fonte de dados separa duas coisas: o <strong>tratamento de uma linha</strong>, que é uma função pura, e a <strong>leitura do arquivo</strong>, que é I/O. A primeira é testada sem .zip nenhum:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">aoRegistro</span><span class="p">:</span> <span class="p">(</span><span class="nx">campos</span><span class="p">,</span> <span class="nx">colunas</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ehDaEleicao</span><span class="p">(</span><span class="nx">campos</span><span class="p">,</span> <span class="nx">colunas</span><span class="p">,</span> <span class="nx">ano</span><span class="p">,</span> <span class="nx">cargoAceito</span><span class="p">))</span> <span class="k">return</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">ibge</span> <span class="o">=</span> <span class="nx">ibgeDe</span><span class="p">(</span><span class="nx">campos</span><span class="p">[</span><span class="nx">colunas</span><span class="p">.</span><span class="nx">CD_MUNICIPIO</span><span class="p">]);</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">ibge</span><span class="p">)</span> <span class="p">{</span> <span class="nx">semIbge</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="nx">campos</span><span class="p">[</span><span class="nx">colunas</span><span class="p">.</span><span class="nx">CD_MUNICIPIO</span><span class="p">]);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span>
  <span class="kd">const</span> <span class="nx">candidato</span> <span class="o">=</span> <span class="nx">candidatoDoRegistro</span><span class="p">(</span><span class="nx">campos</span><span class="p">,</span> <span class="nx">colunas</span><span class="p">,</span> <span class="nx">ano</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">apuracao</span> <span class="o">=</span> <span class="nx">apuracaoDoTurno</span><span class="p">(</span><span class="nx">porTurno</span><span class="p">,</span> <span class="nx">candidato</span><span class="p">.</span><span class="nx">turno</span><span class="p">,</span> <span class="dl">'</span><span class="s1">csv</span><span class="dl">'</span><span class="p">);</span>
  <span class="nx">somarVoto</span><span class="p">(</span><span class="nx">apuracao</span><span class="p">,</span> <span class="nx">candidato</span><span class="p">,</span> <span class="nx">ibge</span><span class="p">,</span> <span class="nx">inteiro</span><span class="p">(</span><span class="nx">campos</span><span class="p">[</span><span class="nx">colunas</span><span class="p">[</span><span class="nx">colunaDeVotos</span><span class="p">]]));</span>
  <span class="c1">// ...</span>
<span class="p">},</span>
</code></pre></div></div>

<p>O gerador da Paraíba virou 14 arquivos, cada um com uma responsabilidade:</p>

<table>
  <thead>
    <tr>
      <th>Pasta</th>
      <th>O que tem</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">gerar-dados.mjs</code></td>
      <td>Só a linha de comando: escolhe os anos e encadeia as etapas</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">eleicao/</code></td>
      <td>Configuração (UF, cargos), conversão TSE→IBGE e a soma de votos</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">fontes/</code></td>
      <td>Uma fonte por arquivo: CSV por município, API, CSV por seção, tabela de locais</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">saida/</code></td>
      <td>Poda dos locais, <code class="language-plaintext highlighter-rouge">pessoas.json</code>, <code class="language-plaintext highlighter-rouge">index.json</code> e escrita dos arquivos</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">lib/</code></td>
      <td>CSV do TSE e acesso à rede (retentativa, paralelismo, cache, .zip)</td>
    </tr>
  </tbody>
</table>

<p>O que toca a rede entra por parâmetro (<code class="language-plaintext highlighter-rouge">buscarJson</code>, <code class="language-plaintext highlighter-rouge">ibgeDe</code>), então o teste troca por uma versão falsa sem gambiarra.</p>

<p>Bayeux seguiu a mesma divisão e ganhou uma pasta <code class="language-plaintext highlighter-rouge">secoes/</code> para a tabela de locais: a grafia dos bairros, as coordenadas e a montagem da tabela viraram funções puras. A cópia do parser de CSV sumiu, e a pasta <code class="language-plaintext highlighter-rouge">lib/</code> passou a ser igual nos dois repositórios. Duplicação também é contexto: se o agente corrige um bug numa cópia, nada garante que ele vai lembrar da outra.</p>

<h3 id="3-nomes-que-explicam-comentários-que-justificam">3. Nomes que explicam, comentários que justificam</h3>

<p>Modelos de linguagem processam nomes como texto com significado. <code class="language-plaintext highlighter-rouge">ds</code>, <code class="language-plaintext highlighter-rouge">tk</code>, <code class="language-plaintext highlighter-rouge">ix</code>, <code class="language-plaintext highlighter-rouge">c</code> e <code class="language-plaintext highlighter-rouge">MIN_DIG</code> obrigam quem lê, pessoa ou modelo, a reconstruir a intenção a partir do uso. Viraram <code class="language-plaintext highlighter-rouge">apuracao</code>, <code class="language-plaintext highlighter-rouge">chaveDoCargo</code>, <code class="language-plaintext highlighter-rouge">colunas</code>, <code class="language-plaintext highlighter-rouge">campos</code> e <code class="language-plaintext highlighter-rouge">DIGITOS_DO_CANDIDATO</code>. Os códigos de cargo (<code class="language-plaintext highlighter-rouge">'1'</code>, <code class="language-plaintext highlighter-rouge">'3'</code>) ganharam nome (<code class="language-plaintext highlighter-rouge">CARGOS.PRESIDENTE</code>, <code class="language-plaintext highlighter-rouge">CARGOS.GOVERNADOR</code>), e regras soltas no meio de um <code class="language-plaintext highlighter-rouge">if</code> viraram funções com nome:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="kd">function</span> <span class="nx">ehVotoNominal</span><span class="p">(</span><span class="nx">cargo</span><span class="p">,</span> <span class="nx">numero</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">digitos</span> <span class="o">=</span> <span class="nx">DIGITOS_DO_CANDIDATO</span><span class="p">[</span><span class="nx">cargo</span><span class="p">];</span>
  <span class="k">return</span> <span class="nb">Boolean</span><span class="p">(</span><span class="nx">digitos</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="nx">numero</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;=</span> <span class="nx">digitos</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="nx">NUMEROS_BRANCO_E_NULO</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">numero</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Os comentários que sobraram explicam o <strong>porquê</strong>, que é o que o código não consegue dizer: por que o presidente vem de outro arquivo, por que existe <code class="language-plaintext highlighter-rouge">#NULO#</code>, por que a poda de locais existe. Comentários que repetiam o código saíram.</p>

<p>Uma exceção consciente: os nomes curtos dentro dos JSON (<code class="language-plaintext highlighter-rouge">cands</code>, <code class="language-plaintext highlighter-rouge">tot</code>, <code class="language-plaintext highlighter-rouge">mun</code>) ficaram como estavam. Eles são o contrato com o front-end, e renomear quebraria o site sem ganho nenhum.</p>

<h3 id="o-que-a-sabotagem-mostrou">O que a sabotagem mostrou</h3>

<p>Um teste que nunca falha não prova nada. Por isso, depois da refatoração, quebrei o código de propósito para ver se a trava pegava.</p>

<p>Na Paraíba, tirei o “95” (voto branco) da lista de números ignorados. <strong>O golden master continuou passando.</strong> O motivo: mais adiante, a poda de locais remove qualquer número que não seja de um candidato, e o erro some antes de chegar à saída. O golden master garante que o resultado final não mudou, mas não garante que cada regra funciona sozinha. Se a poda mudar um dia, esse erro aparece.</p>

<p>Por isso vieram os testes unitários, um por regra: divisão do CSV, voto de legenda, branco e nulo, <code class="language-plaintext highlighter-rouge">#NULO#</code>, poda de candidaturas anuladas, ligação de pessoas entre eleições e o que é pedido à API. Com eles, a mesma sabotagem falha na hora, com uma mensagem que diz exatamente o que quebrou:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>not ok 15 - branco (95) e nulo (96) não são votos nominais
</code></pre></div></div>

<p>A lição vale para qualquer código gerado por IA: <strong>o golden master protege a refatoração, e o teste unitário protege a regra</strong>. Um não substitui o outro.</p>

<h3 id="o-agente-precisa-saber-como-se-verificar">O agente precisa saber como se verificar</h3>

<p>A última peça é dizer ao agente, por escrito, como ele confere o próprio trabalho. O repositório ganhou um <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> curto:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">-</span> Rode <span class="sb">`npm test`</span>. Roda offline e em menos de um segundo; não há outro passo de verificação.
<span class="p">-</span> Se o golden master falhar, a saída mudou: corrija o código. Só regrave quando
  a mudança na saída for o objetivo da tarefa, e diga no resumo o que mudou.
<span class="p">-</span> Regra nova ou caso novo dos dados do TSE: escreva primeiro o teste, veja falhar, depois o código.
</code></pre></div></div>

<p>E o CI passou a rodar <code class="language-plaintext highlighter-rouge">npm test</code> em todo push e pull request, e também antes do workflow que gera os dados de uma eleição nova. Se o gerador estiver quebrado, os dados errados não chegam a ser publicados.</p>

<h3 id="antes-e-depois">Antes e depois</h3>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>Paraíba antes</th>
      <th>Paraíba depois</th>
      <th>Bayeux antes</th>
      <th>Bayeux depois</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Arquivos dos geradores</td>
      <td>2</td>
      <td>14</td>
      <td>3</td>
      <td>17</td>
    </tr>
    <tr>
      <td>Maior arquivo</td>
      <td>251 linhas</td>
      <td>96 linhas</td>
      <td>163 linhas</td>
      <td>93 linhas</td>
    </tr>
    <tr>
      <td>Maior função</td>
      <td>39 linhas</td>
      <td>16 linhas</td>
      <td>38 linhas</td>
      <td>20 linhas</td>
    </tr>
    <tr>
      <td>Funções com mais de 20 linhas</td>
      <td>4</td>
      <td>0</td>
      <td>4</td>
      <td>0</td>
    </tr>
    <tr>
      <td>Linhas com mais de 120 caracteres</td>
      <td>22</td>
      <td>0</td>
      <td>27</td>
      <td>0</td>
    </tr>
    <tr>
      <td>Testes</td>
      <td>0</td>
      <td>31</td>
      <td>0</td>
      <td>31</td>
    </tr>
    <tr>
      <td>Saída gerada</td>
      <td> </td>
      <td>idêntica</td>
      <td> </td>
      <td>idêntica</td>
    </tr>
  </tbody>
</table>

<p>Os testes rodam offline e levam menos de meio segundo em cada repositório. O código ficou maior: na Paraíba, de 357 para 625 linhas; em Bayeux, de 386 para 602. É o preço de nomes mais longos, funções separadas e regras explícitas, e é um preço que vale. A conferência final foi com os dados reais: rodando o gerador de índice sobre os dados já publicados, <code class="language-plaintext highlighter-rouge">index.json</code> e <code class="language-plaintext highlighter-rouge">pessoas.json</code> saíram idênticos aos que estão no ar nos dois sites.</p>

<p>O resumo é o mesmo do resto do artigo, agora aplicado ao código: <strong>o LLM acelera, mas o resultado precisa ser verificável</strong>. Na primeira versão, quem verificava era eu, comparando números com o TSE. Agora o próprio agente verifica, em meio segundo, toda vez que mexe em algo.</p>]]></content><author><name>João Calixto</name></author><summary type="html"><![CDATA[Passo a passo de como montei o Mapa do voto da Paraíba e de Bayeux com ajuda de IA, coletando dados do TSE e publicando tudo de graça.]]></summary></entry></feed>