OlhoNaLei · Projeto nº 571/2026 Em tramitação olhonalei.com
A tramitação do OlhoNaLei
Tecnologias, tropeços e otimizações de um projeto que lê o Congresso Nacional todos os dias.
Vai ler sozinho? Ative as explicações
Plano de tempo (núcleo de ~54 min): Abertura 3 · Dados 5 · Busca 10 · IA 6 · Relevância 3 · Latência 5 · Crons 5 · Alertas 4 · Custos 5 · Alcance 5 · Lições 3. Os 12 slides marcados como opcional (aparecem assim na visão geral, tecla O, e no topo destas notas) somam mais uns 11 minutos: use-os se sobrar tempo. Para fechar em 45 minutos, pule os opcionais e resuma em uma frase a ingestão de embeddings (cap. 2) e a rede de alertas (cap. 7).
Teclas: → e ← avançam e voltam (inclusive as etapas dentro do slide), N abre estas notas com o relógio, O abre a visão geral, F põe em tela cheia, T troca o tema claro/escuro, E liga a versão explicada. O relógio das notas zera com um clique.
Duas versões: “Palestra” (esta) e “Explicado”, que abre ao lado de cada slide um painel com a explicação para quem lê sozinho e os conceitos do glossário. A chave fica no rodapé; o botão “Entenda” no canto de cada slide também liga. Para mandar a versão explicada a alguém, compartilhe o link terminado em #explicado .
Para apresentar com projetor: abra o mesmo link em duas janelas. Ponha uma no projetor, em tela cheia (F). Na outra, no seu notebook, ligue as notas (N). As duas andam juntas, inclusive nas etapas. Cliques dentro dos interativos só valem na janela em que você clica: faça as demonstrações na janela do projetor.
Abertura: o projeto tem o número 571 porque são 571 commits. A brincadeira do formato é proposital: a apresentação é organizada como um processo legislativo. Cada capítulo é um despacho.
Esta é a parte técnica de uma palestra sobre o OlhoNaLei , um site que acompanha o Congresso Nacional: projetos de lei, votações, resumos feitos por inteligência artificial e alertas.
A capa imita a folha de rosto de um projeto de lei porque a apresentação inteira foi organizada como uma tramitação: cada capítulo é um “despacho”. O número do projeto, 571, é a quantidade de commits (alterações registradas no código) entre 30/04 e 06/10/2026.
Como navegar
Use as setas do teclado ou os botões do rodapé. A linha do rodapé mostra os capítulos como etapas de uma tramitação; clique num ponto para pular. A tecla O abre a visão geral com todos os slides e o glossário.
Este painel explica cada slide e define os termos técnicos. Para voltar à versão de palestra, use a chave “Palestra” no rodapé ou a tecla E.
Abertura Leitura
Cada despacho segue o mesmo rito
Problema O que precisávamos resolver.
Tecnologia O que escolhemos, e por quê.
Deu errado O que quebrou em produção.
Solução Como medimos e corrigimos.
Lição O que levamos para o próximo despacho.
Os números
São reais: tirados do banco de produção, do git, das faturas e dos logs. Inclusive custos, erros e robôs bloqueados.
Os erros
Os nossos e os do assistente de IA que ajudou a escrever o código. Transparência é premissa do projeto, então vale para nós também.
Explique o rito em 30 segundos: problema, tecnologia escolhida, o que deu errado, a solução medida e a lição. É o mesmo ciclo de qualquer projeto real: a parte bonita da tecnologia e a parte em que ela quebra.
Ponto importante para a plateia de estudantes: quase tudo aqui foi escrito com ajuda de um assistente de IA. Ele acelerou muito, e também errou. Os erros dele aparecem nos slides com o mesmo destaque que os nossos.
Todos os capítulos seguem o mesmo roteiro, e um carimbo no topo de cada slide diz em que ponto da história ele está:
Problema : o que precisávamos resolver. Tecnologia : o que escolhemos e por quê. Deu errado : o que quebrou em produção. Solução : como medimos e corrigimos. Lição : o que vale levar para outros projetos.
Sobre os números
Todos são reais: saíram do banco de dados de produção, do histórico do código, das faturas e dos registros de execução, medidos até 06/10/2026. Os erros também aparecem, inclusive os do assistente de inteligência artificial que ajudou a escrever boa parte do código. Transparência é uma premissa do projeto.
Abertura Tecnologia clique numa caixa para ver as conexões
Três nuvens e um banco no meio de tudo
Comece só com as fontes públicas: APIs de dados abertos da Câmara e do Senado e as notícias por RSS. Avance uma camada por vez.
(1) Railway, nos EUA: a API em FastAPI recebe os disparos e o worker faz o trabalho pesado de texto. (2) Supabase, em São Paulo: Postgres com pgvector, o agendador pg_cron e a Edge Function da busca. Os provedores de IA aparecem nessa camada: Claude para ler, Voyage para os vetores, Gemini para imagens. (3) Vercel, em São Paulo: o site em Next.js e as rotas de automação. (4) Onde chega: pessoas, newsletter e redes.
Ideia central: o banco é o centro do sistema. Ele guarda os dados, agenda o trabalho e ainda roda a busca. Clique no Postgres para ver tudo o que depende dele. Detalhe para guardar: a API fica nos EUA e o banco em São Paulo. Isso vai custar caro no capítulo 5.
O mapa mostra as peças do sistema e quem conversa com quem.
À esquerda, as fontes públicas : as APIs de dados abertos da Câmara e do Senado e notícias por RSS. No meio, três nuvens. O Railway , nos EUA, roda o código Python que busca e processa os dados. O Supabase , em São Paulo, guarda tudo num banco Postgres, agenda as tarefas e roda a busca. A Vercel , em São Paulo, serve o site.
Embaixo ficam os provedores de IA: o Claude lê e resume os textos, a Voyage transforma texto em vetores e o Gemini gera imagens. À direita, onde a informação chega: pessoas no site, newsletter e redes sociais.
Como ler
Avance (→) para revelar uma camada por vez. Clique em qualquer caixa para destacar as conexões dela e ler o que faz.
Por que importa
O banco de dados está no centro: guarda os dados, agenda o trabalho e ainda roda a busca. E a distância física entre as peças (EUA e São Paulo) vai custar caro no capítulo 5.
Abertura Escala banco de produção · 06/10/2026
O tamanho do que roda toda madrugada
172.307
proposições no banco, das duas casas. 46% são substantivas (projetos, PECs, MPs); o resto é requerimento, emenda e afins.
367.356
trechos de texto com vetor, prontos para a busca por sentido
13.611
proposições lidas por IA: resumo, temas, jabutis e impacto fiscal
33
tarefas agendadas no próprio banco; cerca de 54 mil disparos por mês
82.400
PDFs baixados das duas casas: 21,3 GB de inteiro teor
2.924
notícias dos últimos 30 dias, todas com vetor para achar a proposição de que falam
Só para dar escala. Os números sobem na tela; não precisa ler todos.
O que importa: isso roda com um time pequeno, sem servidor próprio, e quase tudo acontece de madrugada, sem ninguém apertando botão. O resto da palestra é como chegamos aqui, e o que quebrou no caminho.
A escala do sistema em 06/10/2026:
172.307 proposições das duas casas, das quais 46% são substantivas, isto é, do tipo que pode criar ou mudar normas. 367.356 trechos de texto transformados em vetores para a busca por sentido. 13.611 proposições lidas por IA. 33 tarefas agendadas, que disparam cerca de 54 mil vezes por mês. 82.400 PDFs baixados (21,3 GB). 2.924 notícias dos últimos 30 dias.
Quase tudo isso roda de madrugada, sem ninguém apertar botão, com um time pequeno e sem servidor próprio.
Despacho 01 · abr → jul 2026
O Congresso publica tudo. Só não publica pronto.
Duas APIs públicas, cada uma com seu formato, limites que ninguém documenta e PDFs que não são texto.
Primeiro capítulo: antes de qualquer IA, é preciso ter os dados. E dado público não é dado limpo.
Spoiler do capítulo: a frase que mais se repete é “carregou sem erro”. Ela quase nunca quer dizer “carregou tudo”.
Abre o capítulo sobre a coleta de dados.
A Câmara e o Senado publicam seus dados em APIs públicas, mas cada uma tem um formato próprio, limites de uso que ninguém documenta e textos em PDF que nem sempre podem ser lidos como texto. Antes de qualquer inteligência artificial, é preciso ter dado completo e confiável.
Despacho 01 · Dados Tecnologia ingestion/ · supabase/migrations
Da API do Congresso até a página, em nove paradas
Avance parada por parada (são 9). A ideia geral: separar quem busca o dado de quem mostra o dado. Ninguém que abre o site espera a API do Congresso.
Destaques para comentar: (2) o dado bruto fica guardado antes de ser transformado, então dá para reprocessar sem chamar a API de novo; (4) o filtro é uma coluna gerada no próprio banco; (5) PDF vira texto, e quando o texto sai ilegível, vira imagem e passa por OCR; (7) a IA roda em lote, à noite, com desconto; (9) o site usa ISR: a página é gerada uma vez e fica em cache.
Os horários são reais. A maior parte roda entre meia-noite e 7h da manhã.
A viagem de uma proposição, da API do Congresso até a página do site, em nove paradas:
(1) as fontes públicas; (2) o dado bruto é guardado antes de ser transformado; (3) os metadados vão para a tabela principal; (4) um filtro separa o que é substantivo; (5) o PDF vira texto, com OCR quando é imagem; (6) o texto é cortado em trechos e cada trecho vira um vetor; (7) a IA lê e resume, em lote, à noite; (8) uma nota de relevância ordena o que importa; (9) o site mostra a página, guardada em cache.
Como ler
Avance (→) para percorrer as paradas, ou clique em qualquer uma. Cada parada mostra onde roda, quando roda e um detalhe real.
Por que importa
Ninguém que abre o site espera pela API do Congresso: todo o trabalho pesado já aconteceu antes, de madrugada.
Despacho 01 · Dados Tecnologia commits 8d439c7 · 6b1964e
Nem tudo que tramita muda alguma coisa
-- coluna calculada pelo próprio Postgres
is_substantive boolean GENERATED ALWAYS AS (
bill_type = ANY (ARRAY ['PL' ,'PLP' ,'PEC' ,'MPV' ,
'PLV' ,'PLN' ,'PDL' ,'PDC' ,'PRC' ,'PLC' ,
'PLS' ,'PRS' ,'RCP' ])
) STORED
O site lê só a view bill_substantive . Requerimento não ocupa a tela, não entra na fila de texto e não gasta IA.
Tropeço · 01/07
O id 163889 era o PL 914/2024 no Senado e a MSC 365/1989 na Câmara. Cada casa numera do seu jeito: a chave virou o par (casa, id) .
Câmara, os quatro tipos mais comuns. Verde: substantivo. Cinza: fica fora do site. No total, 46% das 172.307 proposições passam no filtro.
Primeira decisão de produto que é decisão de engenharia: o que conta como proposição “de verdade”. Projetos, PECs, medidas provisórias e decretos legislativos entram. Requerimentos, emendas e pedidos de informação ficam de fora.
O filtro é uma coluna gerada: o banco calcula sozinho a cada inserção, e não existe chance de o código esquecer de preencher.
O tropeço da chave: no começo, a chave era só o id externo. Mas Câmara e Senado têm numerações independentes, e o mesmo número apontava para duas coisas diferentes. Moral: identidade de um registro vem da fonte inteira, não só do número.
A maior parte do que tramita no Congresso são requerimentos, emendas e pedidos de informação, que não criam nem mudam leis. Uma coluna calculada pelo próprio banco marca como substantivas 13 tipos de proposição: projetos de lei, PECs, medidas provisórias, decretos legislativos e afins.
O site lê apenas essas, e só elas entram na fila de texto e de IA. Isso economiza espaço na tela, tempo de processamento e dinheiro.
Como ler
O gráfico mostra os quatro tipos mais comuns na Câmara: só o PL (verde) passa no filtro. No total, 46% das 172.307 proposições passam.
O tropeço
Câmara e Senado numeram seus documentos de forma independente: o mesmo número, 163889, apontava para coisas diferentes nas duas casas. A identidade de cada registro passou a ser o par (casa, número).
Despacho 01 · Dados Tecnologia medição de 24/07 · commits 80d02b2 · 443cde5
Ninguém documenta o limite. Medimos.
API da Câmara: respostas “429, pedidos demais” conforme a taxa oferecida.
40/s
sem nenhum erro no Senado. Usamos 15/s.
O Senado devolve 400 a qualquer pedido sem User-Agent.
Os dois hosts são independentes: as casas rodam em paralelo .
O host de documentos aguenta 2/s.
Opcional. Lição curta para quem vai consumir API pública: o limite existe mesmo quando não está escrito. Mandar devagar demais desperdiça horas; rápido demais toma 429 e perde dado.
Medimos oferecendo taxas crescentes e contando as respostas 429. Na Câmara o erro começa perto de 15 por segundo; usamos 10. No Senado, 40 por segundo passou sem erro; usamos 15. E o Senado recusa quem não se identifica com User-Agent, com um 400 que não explica nada.
Como os hosts são independentes, o sistema passou a rodar uma tarefa pesada por casa, em paralelo. Isso volta no capítulo dos crons.
As APIs do Congresso não dizem quantos pedidos por segundo aceitam. O teste foi medir: mandar pedidos cada vez mais rápidos à API da Câmara e contar as respostas de erro 429 (“pedidos demais”).
Até 10 pedidos por segundo, nenhum erro; a 30, um quarto dos pedidos falha. O Senado aceitou 40 por segundo sem erro, mas recusa com erro 400 quem não se identifica com um User-Agent.
Como ler
Cada barra é uma taxa testada; a altura é a porcentagem de respostas 429. A linha marca o padrão escolhido para a Câmara: 10 por segundo.
Por que importa
Devagar demais desperdiça horas; rápido demais perde dados. E como as duas casas têm servidores independentes, o sistema passou a processá-las em paralelo.
Despacho 01 · Dados Deu errado commits 5aca534 · 6cd99b0 · e7d9da2
Carregou sem erro. Não carregou tudo.
1º PL de 2026 2.599.887
último PL de 2025 2.622.750
PL 1396/2026 · id 2.611.850 · nunca carregado
cursor = maior id já carregado 2.641.462 só anda para cá
trecho que o cursor considera “já carregado”
A causa
Ler “só o que é novo” assumia que id maior é proposição mais nova. Não é. E o filtro de data da API é por tramitação , não por apresentação.
A prova
Contar banco contra API, mês a mês: março de 2024 tinha 264 no banco e 571 na API. Junho de 2025, 200 contra 468.
A correção
Fatias semanais por data de apresentação, num step-runner que avança um passo por vez e sobrevive a quedas.
25.810 → 62.614
substantivas da Câmara
Esse é o bug mais instrutivo do capítulo. O carregamento usava um cursor: “já carreguei até o id X, agora só leio o que vier depois”. É o jeito barato de ler só o novo.
Mostre a reta: os ids da Câmara não seguem a ordem das datas. O último PL de 2025 tem id maior que o primeiro de 2026. Avance: uma proposição de 2026 com id menor que o cursor nunca é carregada. Sem erro nenhum, sem log, sem alerta.
Só descobrimos ao comparar com a fonte, dia a dia: em março de 2024, 20 de 20 dias tinham buraco. Depois do novo backfill, as contagens batem com a API. As substantivas da Câmara mais que dobraram.
Para ler só o que é novo, o sistema guardava um cursor : o maior número de identificação já carregado. A premissa era que número maior significa proposição mais nova.
Não é. Na reta do slide, o último projeto de 2025 tem número maior que o primeiro de 2026. Um projeto de 2026 com número menor que o cursor nunca era carregado, e nada avisava: nenhum erro, nenhum alerta.
Como ler
A faixa cinza é tudo o que o cursor considera “já carregado”. O ponto vermelho é uma proposição real que caiu nessa faixa e ficou de fora.
A solução
Comparar o banco com a fonte, dia a dia, revelou os buracos: março de 2024 tinha 264 proposições no banco contra 571 na API. O histórico foi recarregado em fatias semanais, por data de apresentação, numa rotina que avança um passo por vez e sobrevive a quedas. As proposições substantivas da Câmara foram de 25.810 para 62.614.
Despacho 01 · Dados Deu errado commits 025e30b · d978fc0 · 0994f50
Metade do Congresso estava invisível
357 × 28.850
matérias do Senado contra as da Câmara, em 24/07. Só entrava no banco o que já tinha sido votado.
A Lei 15.474/2026, do spray de pimenta, não era achável no site.
Catálogo novo, por sigla e ano: 10.915 matérias em 62 s , zero falhas.
Métrica nova: conta texto com conteúdo, não registro. Hoje, 81% .
Senado: % de matérias com texto, por ano. O buraco antes de 2018 é da fonte, não nosso.
Até julho, o Senado só entrava no banco quando uma matéria era votada. Resultado: 357 matérias contra quase 29 mil da Câmara. Uma lei recém-sancionada, a do spray de pimenta, não aparecia na busca.
Três pegadinhas da API do Senado: devolve 400 a quem não manda User-Agent; o endpoint de textos foi descontinuado em fevereiro; e antes de 2018 o documento simplesmente não existe na origem. Avance para o gráfico: 3 documentos em 708 matérias de 2017, contra 455 em 739 de 2018.
Lição: documente o limite da fonte, senão ele parece falha sua. O teto real do Senado é 81,1% de conteúdo, e estamos em 81,0%.
Até julho, uma matéria do Senado só entrava no banco quando era votada. Resultado: 357 matérias do Senado contra 28.850 da Câmara, e uma lei recém-sancionada, a do spray de pimenta, nem aparecia na busca.
A correção foi montar um catálogo completo, consultando a API por sigla e por ano: 10.915 matérias em 62 segundos, sem nenhuma falha.
Como ler
O gráfico mostra, por ano de apresentação, a porcentagem de matérias do Senado com texto disponível. Antes de 2018 quase não há texto: não é falha do sistema, a própria fonte não publica o documento. Hoje, 81% das matérias têm texto, praticamente tudo o que existe na origem.
Por que importa
Medir a cobertura por “tem conteúdo” e não por “tem registro” mudou a conta. E documentar o limite da fonte evita que ele pareça um defeito do projeto.
Despacho 01 · Dados Deu errado commits 0f58a0a · c993933 · d48f5e6
Um PDF cheio de texto pode não ter nenhuma palavra
A busca por “estepe” devolvia um projeto sobre contribuição social.
PDFs com fontes sem mapa de caracteres viram uma sopa de glifos. Passava no teste de quantidade de texto, e o vetor do lixo ficava “parecido” com qualquer busca.
if imprimiveis(texto) < 0.80 : # glifos sem mapa
texto = ocr(pdf) # Tesseract
if parece_lixo(trecho):
descartar(trecho) # 2.785 trechos
Lixo na origem
A Câmara servia uma página do Diário Oficial no lugar de 33 MPs e mensagens. O “jabuti” com confiança 0,97 da MPV 1374/2026 era um acordo aéreo entre Brasil e Jamaica.
Quarentena
36 proposições com fonte inválida saem da busca e da IA. Os casos viraram um relatório de erros para a Câmara.
~13%
dos PDFs do Senado precisam de OCR. O documento de arquitetura dizia 70%: ninguém tinha medido.
PDF é formato de impressão, não de texto. Quando a fonte embutida não traz o mapa de caracteres, a extração devolve símbolos sem sentido, em quantidade suficiente para parecer texto.
A regra nova mede qualidade, não quantidade: se menos de 80% dos caracteres são imprimíveis, o arquivo vai para OCR. E um filtro joga fora os trechos de lixo que já tinham vetor.
A parte curiosa: pagávamos IA para ler lixo e responder, com confiança de 97%, que havia um jabuti. Era um acordo aéreo com a Jamaica num documento que nem era a MP. O detector de jabuti virou, sem querer, um detector de dado ruim.
PDF é um formato feito para impressão. Quando a fonte embutida no arquivo não traz o mapa entre o desenho de cada letra e o caractere correspondente, extrair o texto devolve uma sopa de símbolos: parece texto, tem o tamanho de um texto, mas não tem palavras.
Esse lixo virava vetores “parecidos” com qualquer pergunta: a busca por “estepe” trazia um projeto sobre contribuição social. E a Câmara chegou a servir uma página do Diário Oficial no lugar de 33 documentos.
A solução
A regra passou a medir qualidade, não quantidade: se menos de 80% dos caracteres são legíveis, o arquivo vai para OCR. Trechos de lixo foram descartados (2.785), e 36 proposições cuja fonte entregava o documento errado ficaram em quarentena, fora da busca e da IA.
Lição
“Carregou sem erro” não é “carregou tudo”. Confira contra a fonte , e documente o limite dela.
Feche o capítulo: os tropeços têm a mesma assinatura. Nenhum deu erro. Todos foram achados comparando o resultado com a origem.
Resumo do capítulo: nenhum dos problemas (o cursor, o Senado incompleto, os PDFs ilegíveis) gerou erro. Todos foram encontrados comparando o resultado com a origem dos dados.
“Carregou sem erro” não significa “carregou tudo”. E quando a própria fonte tem um limite, documentá-lo evita que ele pareça falha sua.
Despacho 02 · jun → jul 2026
Ninguém procura uma lei com as palavras da lei .
A pessoa digita “saúde mental nas escolas”. A proposição fala em “bem-estar psicológico de estudantes”. Como uma acha a outra?
Pergunta de abertura do capítulo. Deixe a frase no ar: quem já buscou lei no site da Câmara sabe que só acha se acertar a palavra exata.
O capítulo conta três etapas: busca por palavra, busca por sentido e a combinação das duas. Depois, dois tropeços de desempenho.
Abre o capítulo da busca. O problema central: quem procura usa as próprias palavras (“saúde mental nas escolas”), e a proposição usa as palavras da lei (“bem-estar psicológico de estudantes”). Uma busca por palavra exata não liga uma coisa à outra.
O capítulo conta três etapas, a busca por palavra, a busca por sentido e a combinação das duas, e depois dois tropeços de desempenho.
Despacho 02 · Busca Problema commits 8dde629 · d51f622
Procurar pedaço de texto não é procurar palavra
O primeiro ILIKE '%saúde%' só achava a grafia exata, com acento e tudo, no meio de qualquer palavra.
-- antes: pedaço literal de texto
WHERE title ILIKE '%spray pimenta%' -- 0
-- depois: full-text em português
WHERE search_vector @@ websearch_to_tsquery(
'portuguese' , 'spray pimenta' ) -- 3
O full-text guarda o radical de cada palavra, sem acento e sem “de”, “a”, “o”. Um índice GIN aponta, para cada radical, as proposições que o contêm.
9 → 3.793
resultados para “saúde” na API, em 21/06: substring contra full-text
0 → 3
“spray pimenta” no site, em 24/07: “spray de pimenta” tinha um “de” no meio
A primeira busca do site usava ILIKE, que procura um pedaço literal de texto. “saúde” sem acento não acha “saúde” com acento; “spray pimenta” não acha “spray de pimenta”.
Full-text do Postgres resolve isso: cada texto vira um tsvector, uma lista de radicais (saúd, pimen…) sem acento e sem palavras vazias. A consulta vira um tsquery. Um índice GIN, que é um índice invertido, faz a busca ser instantânea.
Os números são reais: 9 contra 3.793 na API em junho; 0 contra 3 no site em julho. Mas o full-text ainda só acha a palavra. Não acha o assunto. Isso leva ao próximo slide.
A primeira busca do site procurava pedaços literais de texto, com o operador ILIKE: só achava a grafia exata. “spray pimenta” não achava “spray de pimenta”, porque havia um “de” no meio.
A busca full-text do Postgres resolve isso: cada texto é reduzido aos radicais das palavras, sem acentos e sem palavras vazias como “de”, “a” e “o”, e um índice invertido encontra rapidamente as proposições que contêm cada radical.
Números
“saúde” passou de 9 para 3.793 resultados; “spray pimenta”, de 0 para 3.
O limite
A busca full-text acha palavras, não assuntos. Quem procura “saúde mental nas escolas” ainda não acha a proposição que fala em “bem-estar psicológico de estudantes”. É o próximo slide.
Despacho 02 · Busca Tecnologia voyage-3.5-lite · pgvector
Cada trecho vira um ponto num mapa de sentido
Comece no modo “palavra exata” (é o estado inicial). Pergunte à plateia: quantas proposições contêm saúde, mental e escola ao mesmo tempo? Nenhuma.
Avance (→): o modo muda para “sentido”. Um embedding é uma lista de 1.024 números que representa o significado de um texto. Textos com assunto parecido ficam perto nesse espaço, mesmo sem nenhuma palavra em comum. A proximidade é medida pelo cosseno do ângulo entre os vetores.
Avance de novo: outra consulta, outro bairro do mapa. Deixe claro que o desenho é uma ilustração: o espaço real tem 1.024 dimensões; aqui ele foi achatado para duas.
Um embedding é uma lista de 1.024 números que representa o sentido de um texto, gerada por um modelo de IA. Textos sobre assuntos parecidos ficam próximos nesse espaço de números, mesmo sem nenhuma palavra em comum.
A busca por sentido transforma a pergunta num vetor e procura os trechos de proposição mais próximos dele.
Como ler
O mapa achata 1.024 dimensões em duas, só para ilustrar. No modo “palavra exata”, a consulta não acha nada; no modo “sentido”, ela acha os vizinhos. Avance (→) ou troque a consulta e o modo nos botões. O número ao lado de cada resultado é a similaridade: quanto mais perto de 1, mais parecido.
Despacho 02 · Busca Tecnologia commits 17ae204 · 9c62562
O texto inteiro vira trechos, e cada trecho vira um vetor
Inteiro teor PDF da proposição até 80 mil caracteres
corta
Trechos
~2.000 caracteres 200 de sobreposição …
embeda
Vetor [0,012 · −0,087 · 0,231 · …] 1.024 números Voyage · input_type=document
indexa
Postgres pgvector + HNSW grafo de vizinhos acha os parecidos sem comparar com todos
Hoje: 367.356 trechos de texto, 12.655 resumos gerados por IA e 2.924 notícias, todos com vetor. Custo total de embeddings desde junho: US$ 6,93.
Por que trechos
Uma proposição de 80 páginas fala de muitas coisas. Um vetor só para tudo mistura os assuntos.
Por que sobreposição
Uma frase cortada ao meio ainda aparece inteira no trecho vizinho.
O primeiro tropeço
O plano grátis da Voyage aceitava 3 requisições por minuto: a ingestão quebrava e ganhou retry.
Explique a ingestão de embeddings da esquerda para a direita, avançando as etapas.
O texto completo (inteiro teor) vem em PDF. É cortado em trechos de cerca de 2.000 caracteres, com 200 de sobreposição entre um e outro. Cada trecho vai para a Voyage, que devolve 1.024 números. Os vetores ficam no próprio Postgres, com a extensão pgvector, num índice HNSW: um grafo em camadas que permite achar os vizinhos mais próximos sem comparar a consulta com os 367 mil trechos.
Detalhe útil para estudantes: tudo isso custou US$ 6,93 em embeddings desde junho. O caro em IA não foi o vetor.
Como o texto de uma proposição vira algo pesquisável por sentido.
O inteiro teor é cortado em trechos de cerca de 2.000 caracteres, com 200 de sobreposição, para que nenhuma frase fique perdida ao meio. Cada trecho vai para a Voyage, que devolve um vetor de 1.024 números. Os vetores ficam no próprio Postgres, com a extensão pgvector, num índice HNSW que encontra os vizinhos mais próximos sem comparar a pergunta com todos os 367 mil trechos.
Por que trechos
Um projeto longo fala de muitos assuntos. Um vetor único para o documento inteiro misturaria todos eles.
Custo
Todos os embeddings gerados desde junho custaram US$ 6,93. O caro em IA não foi o vetor.
Despacho 02 · Busca Tecnologia frontend/src/app/buscar · supabase/functions/busca
Palavra-chave primeiro; sentido só quando a palavra não basta
Clique nas quatro buscas, nesta ordem: “PL 2159/2021”, “reforma tributária”, “spray pimenta” e “saúde mental nas escolas”. Cada uma para num ponto diferente do caminho.
A regra é de custo e de precisão: palavra-chave é barata e exata. Se ela devolve 3 ou mais resultados, a busca termina ali. Só quando ela é fraca entra a busca por sentido, que exige gerar o vetor da consulta.
Na busca por sentido rodam dois braços em paralelo: o semântico (HNSW, 400 vizinhos, reordenados por cosseno) e o de palavra-chave. A fusão é o RRF: cada resultado ganha 1/(60 + posição) em cada lista. Quem aparece bem nas duas sobe. Não precisa calibrar pesos entre notas que estão em escalas diferentes.
O caminho que uma busca percorre, em ordem:
Se a consulta parece um número de proposição ou de lei, a busca vai direto ao registro. Depois vem a palavra-chave, rápida e exata: com 3 ou mais resultados, a busca termina ali. Só quando a palavra-chave não basta entra a busca por sentido, que gera o vetor da consulta (ou o reaproveita do cache) e roda dois braços em paralelo, combinados pela fusão RRF. Por fim, apelidos populares, como “PL da Devastação”, sobem para o topo.
Como ler
Clique nas quatro buscas de exemplo. Cada uma para num ponto diferente do caminho. As etapas acesas foram percorridas; o painel à esquerda explica a etapa em destaque, com os tempos medidos em produção.
Por que importa
Palavra-chave é barata e exata para nomes e números; sentido é abrangente, mas custa uma chamada de IA. A ordem economiza dinheiro sem perder resultado.
Despacho 02 · Busca Deu errado commits c04fd5e · d66ca48
O índice existia. A consulta é que não usava.
14/07 · busca semântica em 46 s
O vetor da consulta chegava como texto. A conversão de ~7 KB era refeita em cada linha , nos ~120 mil trechos, e o banco lia a tabela inteira.
-- converte UMA vez, numa variável tipada
v := p_embedding::vector(1024 );
46 s → 0,45 s
28/07 · palavra-chave em 156 ms
Um OR entre dois campos de texto, através de um LEFT JOIN, impedia o planejador de usar os dois índices GIN: varria 48.914 proposições.
-- dois braços, cada um no seu índice
SELECT … WHERE search_vector @@ q
UNION ALL
SELECT … WHERE ai_search_vector @@ q
156 → 17,5 ms 56.552 → 1.338 buffers
Efeito colateral: antes da correção, 2.008 erros de timeout no Sentry. A busca híbrida tinha média de 1,85 s e máximo de 7,99 s, encostando no limite de 8 s.
Esse é o slide do EXPLAIN. Dois casos em que o índice existia e a consulta não o usava.
Primeiro: o vetor chegava como texto e o Postgres convertia de novo em cada linha comparada. A correção foi uma linha: converter uma vez para uma variável tipada. De 46 segundos para 0,45.
Segundo: um OR entre dois campos de texto, através de um LEFT JOIN, fazia o planejador desistir dos dois índices. A correção foi separar em dois SELECTs unidos com UNION ALL, cada um com o seu índice. 156 ms para 17,5 ms, e 40 vezes menos páginas lidas.
Mensagem para estudantes: criar o índice é metade do trabalho. A outra metade é ler o EXPLAIN.
Dois casos em que o índice existia, mas a consulta não o usava.
No primeiro, o vetor da busca chegava como texto e era convertido de novo em cada uma das cerca de 120 mil linhas comparadas: a busca levava 46 segundos. Converter uma vez só, numa variável, levou o tempo a 0,45 s.
No segundo, um OR entre dois campos de texto impedia o banco de usar os dois índices, e ele lia 48.914 proposições a cada busca. Separar em duas consultas unidas (UNION ALL), cada uma usando o seu índice, levou de 156 ms para 17,5 ms.
Por que importa
O comando EXPLAIN mostra o plano que o banco escolheu. Criar o índice é metade do trabalho; a outra metade é confirmar que ele é usado.
Despacho 02 · Busca Deu errado commit 2f94bdf · search_query_cache
Um robô clicou em todas as ordens possíveis
143.512
linhas no cache para só 3.060 conjuntos de temas
36,3%
de acerto no cache (13/08)
65.308
chamadas à Voyage em 12/08 (eram 7 por dia)
99,0%
de acerto hoje, com os temas ordenados
O vetor de cada consulta é guardado num cache: buscar de novo a mesma coisa custa 20 ms em vez de 1,5 s na Voyage. A chave do cache era o texto da consulta, montado juntando os temas escolhidos na ordem dos cliques.
Demonstre: clique Saúde, depois Educação, Buscar. Depois Educação, Saúde, Buscar. Antes, são duas chaves e duas chamadas à Voyage. Depois da correção, uma só. Em seguida clique no botão do robô: as 6 ordens de 3 temas.
Foi exatamente isso: um robô percorreu o seletor de temas. 97,9% das chaves eram permutações. O cache chegou a 854 MB. Detalhe que vale contar: um VACUUM comum não devolveu o espaço; só o VACUUM FULL levou a tabela de 854 MB para 17 MB.
A correção ordena só os temas. O texto livre da pessoa não é ordenado, porque trocar a ordem das palavras muda o sentido.
Gerar o vetor de uma consulta custa cerca de 1,5 s e uma chamada paga à Voyage. Por isso o vetor de cada consulta fica guardado num cache , identificado por uma chave.
A chave era o texto da consulta, montado juntando os temas escolhidos na ordem em que foram clicados. Um robô percorreu o seletor de temas em todas as ordens possíveis: 143.512 linhas de cache para apenas 3.060 combinações diferentes, e 65.308 chamadas à Voyage num só dia (antes eram 7 por dia).
Como ler
Clique em temas em ordens diferentes e em “Buscar”: antes da correção, cada ordem vira uma chave nova; depois, os temas são ordenados e a chave é uma só. O botão do robô simula as 6 ordens possíveis de 3 temas.
A solução
Ordenar só os temas. O texto livre digitado pela pessoa não é ordenado, porque trocar a ordem das palavras muda o sentido. O acerto do cache foi de 36% para 99%.
Lição
Toda chave montada por concatenação precisa de ordem estável . Todo índice precisa de um EXPLAIN que prove que ele é usado.
Feche o capítulo com as duas lições. A Voyage custou centavos nesse incidente; o estrago foi em disco e em espaço.
As duas lições do capítulo de busca:
Toda chave de cache montada juntando partes precisa de uma ordem estável, senão a mesma pergunta vira muitas chaves. E todo índice precisa de um EXPLAIN que prove que ele é usado de fato.
Despacho 03 · jul → ago 2026
Ninguém lê 79 mil proposições. Um modelo lê, por meio centavo cada.
Resumo, temas, jabutis e impacto fiscal: o que dá para pedir numa chamada só, e o que sai caro.
Meio centavo de dólar: US$ 0,004 a 0,005 por proposição. O capítulo é sobre fazer IA caber num orçamento pequeno, e sobre o caso mais difícil que pedimos a ela: achar jabutis.
Abre o capítulo de inteligência artificial.
Nenhuma equipe consegue ler 79 mil proposições; um modelo de linguagem consegue, a menos de meio centavo de dólar cada. O capítulo mostra como fazer a IA caber num orçamento pequeno e o caso mais difícil que pedimos a ela: achar jabutis.
Despacho 03 · IA Tecnologia ingestion/analyze.py · llm.py
Uma chamada, um formulário que o modelo é obrigado a preencher
# um lote por noite, um pedido por proposição
client.messages.batches.create(requests=[{
"custom_id" : id_da_proposicao,
"params" : {
"model" : "claude-haiku-4-5" ,
"system" : [{"type" : "text" ,
"text" : PROMPT,
"cache_control" : {"type" : "ephemeral" }}],
"messages" : [{"role" : "user" ,
"content" : texto[:80_000 ]}],
"tools" : [REGISTRAR_ANALISE],
"tool_choice" : {"type" : "tool" ,
"name" : "registrar_analise" },
}} for ... in lote_de_150])
Batch API: responde em até 24 h, com 50% de desconto. Ninguém precisa do resumo às 3 da manhã.
Ferramenta forçada: o modelo não conversa, só preenche o formulário: resumo, título curto, 3 a 6 pontos-chave, temas, leis citadas, jabutis e impacto fiscal.
Uma leitura, sete saídas: o texto inteiro é pago uma vez só.
0,004
dólar por proposição, no máximo 0,005
13.611
analisadas até 06/10
1.648
têm impacto fiscal sem dizer de onde vem o dinheiro
Três escolhas de custo e confiabilidade numa chamada só.
Batch API: a resposta chega em até 24 horas e custa metade. Para trabalho noturno, é dinheiro grátis. Ferramenta forçada (tool_choice): em vez de pedir “responda em JSON” e torcer, o modelo é obrigado a chamar uma função com um esquema fixo. E uma leitura gera sete saídas: o caro é ler o texto, então tudo o que depende do texto sai da mesma chamada.
Modelo: Haiku 4.5, o menor da família. Para resumo e classificação, ele basta. O Sonnet fica para o que exige julgamento, como confirmar jabuti.
O último número é o impacto fiscal: 2.823 proposições criam gasto ou renúncia, e 1.648 delas não dizem de onde vem o dinheiro.
Para cada proposição, uma única chamada ao Claude Haiku , o modelo menor e mais barato, lê o texto e preenche um formulário fixo: resumo, título curto, pontos-chave, temas, leis citadas, possíveis jabutis e impacto fiscal.
Três escolhas de engenharia estão nessa chamada. A Batch API processa tudo num lote noturno, pela metade do preço. A ferramenta forçada obriga o modelo a responder preenchendo os campos do formulário, em vez de texto livre. E o texto, que é a parte cara, é lido uma vez só para produzir as sete saídas.
Como ler
O código é a requisição real, simplificada. Avance (→) para revelar os destaques e os números.
Números
US$ 0,004 a 0,005 por proposição; 13.611 analisadas até 06/10; 1.648 têm impacto fiscal sem indicar de onde vem o dinheiro.
Despacho 03 · IA Deu errado commits 4ac1bf2 · 873850f · 9f65bb8
O lote de 5.000 que acabava com o mês em dois dias e meio
Dias até esgotar o teto de US$ 50 por mês da análise.
O lote órfão
Um deploy no meio da espera perdia o id do lote. A Anthropic cobrava, e a próxima rodada pagava de novo: ~US$ 2,20 por lote. Agora o id vai para a tabela llm_batch antes de esperar.
O preço da paciência
13.611 de 79.187 substantivas têm análise (17%). Analisar o resto custaria uns US$ 250 de uma vez; a 150 por noite, leva mais de um ano. A fila prioriza o que virou lei e o que as pessoas visitam.
O primeiro lote de produção tinha 5.000 proposições: US$ 19 por noite. O teto mensal de US$ 50 acabava em dois dias e meio. Baixamos para 150 por noite: hoje custa uns 70 centavos.
O lote órfão: a Batch API é assíncrona. Se o processo morre esperando, por exemplo num deploy, o id do lote se perde, mas a cobrança não. A correção é a regra mais velha de sistemas distribuídos: grave o identificador antes de esperar.
E a troca honesta: com o teto, a cobertura anda devagar. Escolhemos priorizar o que virou lei e o que as pessoas acessam.
O primeiro lote de análise tinha 5.000 proposições por noite: US$ 19 por noite. O teto mensal de US$ 50 para análise acabava em 2,6 dias e travava o resto do mês. Com 150 por noite, o custo caiu para cerca de 70 centavos e o mês inteiro cabe no orçamento.
Como ler
As barras mostram quantos dias o teto mensal aguenta em cada ritmo.
O lote órfão
A Batch API responde depois. Se o servidor reiniciava no meio da espera, por exemplo num deploy, o identificador do lote se perdia, mas a cobrança não: a rodada seguinte pagava de novo, cerca de US$ 2,20 por lote. Agora o identificador é gravado no banco antes de esperar.
A troca honesta
Nesse ritmo, 17% das proposições substantivas têm análise, e o resto levaria mais de um ano. A fila prioriza o que virou lei e o que as pessoas visitam.
Despacho 03 · IA Deu errado commit 428a092 · 12/07 → 07/08
O esquema pedia uma lista. Às vezes vinha uma string.
// o esquema pedia: lista de strings
"key_points" :
"<item>Cria o...</item><item>..."
# converte E conta, em vez de pular calado
pontos, coagido = _as_str_list(bruto)
if coagido:
resumo["coerced_lists" ] += 1
Três camadas de defesa tratavam o caso pulando o campo, sem avisar ninguém. O erro visível virou perda silenciosa.
10.623
temas de volta à busca
Ainda acontece: de 10 a 24 vezes por lote de 150, em outubro. A diferença é que agora aparece no resumo de cada rodada.
Opcional. Mesmo com ferramenta forçada, o Haiku às vezes devolvia a lista como uma string com marcação XML dentro. O código tinha três defesas, e todas faziam a mesma coisa: ignoravam o campo e seguiam.
A lição não é sobre IA, é sobre defesa: tratar o dado ruim sem contar quantas vezes isso acontece transforma um erro barulhento em perda silenciosa. A correção converte o formato e conta cada conversão.
Mesmo com o formulário obrigatório, o modelo às vezes devolvia uma lista (de pontos-chave ou de temas) como um texto único, com marcações dentro, em vez de uma lista de verdade.
Três partes do código tratavam o caso ignorando o campo, sem avisar ninguém. Durante 26 dias, 1.768 análises ficaram com dados faltando, e ninguém notou.
A solução
Converter o formato e contar cada conversão, para que o problema apareça no resumo de cada rodada. O reparo devolveu 10.623 temas à busca. O problema ainda acontece, de 10 a 24 vezes por lote de 150, mas agora é visível.
Por que importa
Tratar dado ruim sem registrar quantas vezes isso acontece transforma um erro visível em perda silenciosa.
Despacho 03 · IA Problema commits 611a8aa · cb7549a · 03/08
Jabuti ou não? Vote antes de a carta virar.
Jabuti é o trecho estranho ao tema que pega carona num projeto. “Jabuti não sobe em árvore: se está lá, alguém pôs.”
Momento de participação. Leia cada carta e peça para a plateia levantar a mão: jabuti ou não? Avance para virar uma carta por vez.
Carta 1, o PL do programa Mover, sobre carros mais eficientes, com a taxação das compras internacionais de até 50 dólares no meio: jabuti clássico, a “taxa das blusinhas”. Ela estava no caractere 59 mil, e o limite de texto era 48 mil; subimos para 80 mil para alcançá-la.
Cartas 2 e 3: os dois falsos positivos que o modelo acusava. Citar outro setor não faz jabuti. Fonte de custeio e divulgação servem ao objeto. Por isso o critério virou um teste de função: o trecho serve ao objeto do projeto? Então não é jabuti. Com o critério novo, o Sonnet foi de 1 para 3 acertos em 4 casos de teste.
Jabuti é um trecho estranho ao tema de um projeto, que pega carona para ser aprovado junto. O exemplo clássico é a “taxa das blusinhas”: a taxação de compras internacionais de até US$ 50, inserida num projeto sobre carros mais eficientes (o Programa Mover).
Como ler
Leia cada carta e decida: jabuti ou não? Clique na carta, ou avance com →, para virá-la e ver a resposta.
O critério
As cartas 2 e 3 são falsos positivos que o modelo acusava: citar outro setor não faz um jabuti. Se o trecho serve ao objetivo do próprio projeto, como uma fonte de custeio ou a divulgação da política, ele não é jabuti. O critério virou um teste de função, e o modelo passou de 1 para 3 acertos em 4 casos de teste.
Despacho 03 · IA Deu errado commit 152cc6a · 06/07 → 01/08
Um corte fixo mede o tamanho do documento, não o desvio
A primeira ideia parecia elegante: já temos o vetor de cada trecho. O trecho que mais se afasta do resto do texto é o suspeito. Basta um corte de similaridade.
Com corte em 0,72: zero suspeitos. O trecho mais estranho de toda a base estava em 0,7501. Avance: subindo o corte para 0,80, o jabuti real das blusinhas (0,79) entra, junto com uma enxurrada de documentos longos e normais. Avance de novo: piora.
O motivo é estatístico: o menor valor entre muitos trechos é sempre menor do que entre poucos. Qualquer corte fixo mede o comprimento do documento. Testamos seis métodos semânticos; nenhum separava jabuti de não-jabuti. O gráfico é uma simulação que reproduz o efeito, não os dados reais.
A primeira ideia para achar jabutis: comparar o vetor de cada trecho com o resto do documento. O trecho mais “distante” seria o suspeito, e um corte fixo de similaridade separaria os jabutis do resto.
Não funciona. Quanto mais trechos um documento tem, mais baixo tende a ser o seu valor mínimo, só por estatística. Um corte fixo acaba medindo o tamanho do documento, não o desvio do trecho. Seis métodos diferentes foram testados; nenhum separou os casos.
Como ler
É uma simulação ilustrativa. Cada ponto é um documento: o eixo horizontal é o número de trechos, o vertical, a similaridade do trecho mais estranho. Arraste o controle do corte, ou avance com →. Os pontos vermelhos são os marcados como suspeitos. O jabuti real (ponto amarelo, 0,79) só é pego junto com muitos documentos longos e normais.
Despacho 03 · IA Solução commits 1fe1205 · fb70c65 · 3552022
Um detector para não deixar passar, outro para não acusar à toa
1 · não deixar passar
texto inteiro (até 80 mil)
Haiku, dentro da análise custo extra: zero
suspeito
2 · não acusar à toa
trechos com vetor
os 6 mais estranhos ordenação, não corte fixo
Sonnet 5 confirma em lote, 20 por noite
jabuti
12× mais achados por proposição
Confirmar suspeitos: 48 jabutis em 153 proposições (31%), por US$ 2,21. Varrer às cegas: 4 em 150 (2,7%), por US$ 3,97.
Os limites, sem enfeite
Precisão estimada em ~55%, numa amostra de 9. O recall nunca foi medido. Hoje, 419 proposições têm o selo.
NULL vem primeiro
ORDER BY recente DESC -- NULL no topo!
ORDER BY coalesce (recente, false ) DESC 0 das 20 primeiras da fila eram recentes, até 05/10.
A saída foi trocar o limiar por ordenação e por uma segunda opinião. Detector 1: o Haiku já lê o texto inteiro na análise de cada noite, então apontar suspeitos custa zero a mais. Ele cuida do recall. Detector 2: os seis trechos mais estranhos de cada proposição, comparando cada um com o resto do documento, vão para o Sonnet 5 confirmar. Ele cuida da precisão.
Números honestos: confirmar suspeitos acha 12 vezes mais jabutis por proposição examinada do que varrer às cegas, e ainda custa menos por rodada. Mas a precisão estimada é de uns 55%, e o recall nunca foi medido.
Bônus para quem escreve SQL: no Postgres, NULL vem primeiro em ordenação decrescente. A fila “recentes primeiro” nunca funcionou: 1.557 proposições antigas, com o campo nulo, furavam a fila. Um coalesce resolveu.
A solução troca o corte fixo por duas opiniões.
O primeiro detector é o próprio Haiku, que já lê o texto inteiro na análise de cada noite e aponta suspeitos sem custo extra: ele cuida de não deixar passar. O segundo pega os 6 trechos mais estranhos de cada proposição, ordenados e sem corte fixo, e pede ao Sonnet, um modelo mais forte, que confirme: ele cuida de não acusar à toa.
Como ler
Avance (→) para revelar o segundo detector e os números.
Números
Confirmar suspeitos achou 48 jabutis em 153 proposições; varrer às cegas achou 4 em 150. São 12 vezes mais achados por proposição examinada. A precisão estimada é de cerca de 55%, numa amostra pequena, e o recall nunca foi medido.
Detalhe técnico
No Postgres, valores nulos (NULL) vêm primeiro numa ordenação decrescente. A fila “mais recentes primeiro” nunca funcionou por causa disso, até a correção de 05/10.
Lição
Troque o limiar por ordenação e segunda opinião . E toda economia precisa de alguém de olho no gatilho de voltar atrás.
Em 01/08, por custo, paramos de varrer proposições nunca checadas, com um gatilho combinado: “se a imprensa noticiar um jabuti que não pegamos”. Em 03/08, de 10 jabutis noticiados, tínhamos pegado zero. O ramo só voltou em 24/08.
A troca de custo era razoável: varrer tudo custaria uns US$ 1.900 contra US$ 100 de confirmar suspeitos. O erro foi de processo: o gatilho para voltar atrás disparou dois dias depois, e ninguém estava olhando. Quem percebeu, três semanas depois, foi um alerta de outro sistema.
A lição do capítulo: quando um corte fixo não separa os casos, ordenar e pedir uma segunda opinião funciona melhor.
E toda economia precisa de alguém de olho no gatilho de voltar atrás. Em 01/08, para economizar, a detecção parou de varrer proposições nunca checadas, com uma regra combinada: voltar atrás se a imprensa noticiasse um jabuti que o sistema não tivesse pegado. Em 03/08 isso aconteceu (0 de 10 jabutis noticiados tinham sido detectados), mas a varredura só voltou em 24/08.
Despacho 04 · jul 2026
O que é importante no Congresso hoje?
Em 12/07, o “Em destaque” do site era uma fila de renovações de concessão de rádio.
Pergunte à plateia como ela decidiria o que é importante entre 79 mil proposições. A resposta intuitiva, “o que andou recentemente”, foi exatamente a nossa primeira versão. E deu rádio.
Abre o capítulo sobre o que destacar no site.
Em 12/07, a seção “Em destaque” estava cheia de renovações de concessão de rádio: proposições que tramitam rápido e sem polêmica, mas que importam para pouca gente. Como um sistema decide o que é importante entre 79 mil proposições?
Despacho 04 · Relevância Deu errado commits 6c12cec · 6c04d88 · dbec258
Sinal de procedimento mede movimento, não importância
13,6 → 55,0
nota da PEC 221/2019, fim da escala 6x1
nº 12.135
posição do PDL de rádio mais bem colocado hoje, entre 22.066
A IA lê só a ementa e classifica o alcance. Média da nota por tipo: estruturante 0,88 · setorial 0,59 · rotineiro 0,23 · honorífico 0,04. Custo: US$ 0,0008 por ementa.
Comece no “Antes”: oito sinais, todos de procedimento. Andou, foi votado, avançou de estágio. Uma renovação de concessão de rádio tramita rápido e sem polêmica, e por isso subia. A PEC da escala 6x1, parada há meses, decaía para perto de zero com meia-vida de 90 dias.
Avance: entram três sinais de conteúdo. O tipo da proposição, de graça. A importância lida por IA na ementa, a menos de um décimo de centavo. E a mídia: o que a imprensa está discutindo esta semana, com o maior peso de todos. A meia-vida subiu para 243 dias, e a importância a estica até três vezes.
Os pesos do gráfico são os do código; os valores de cada sinal são ilustrativos. Os números à direita são reais.
A primeira nota de relevância usava apenas sinais de procedimento : tramitou, foi votado, avançou de etapa. Isso mede movimento, não importância, e uma renovação de rádio se move muito.
Entraram três sinais de conteúdo : o tipo da proposição, a importância estimada por IA a partir da ementa e a presença na imprensa. E a meia-vida da nota subiu de 90 para 243 dias, para que projetos importantes, mas parados há meses, não sumissem.
Como ler
Alterne “Antes” e “Depois” nos botões, ou avance com →: a lista se reordena. Os pesos são os reais do código; os valores de cada sinal são ilustrativos para cada tipo de proposição.
Números reais
A PEC do fim da escala 6x1 subiu de 13,6 para 55,0 pontos. O PDL de rádio mais bem colocado hoje está na posição 12.135, entre 22.066. A importância por IA custa US$ 0,0008 por ementa.
Lição
O fácil de medir era movimento. O que importava era conteúdo , e para isso alguém precisa ler.
Ponte para o próximo capítulo: com busca e ranking prontos, o site estava certo. Mas estava lento.
A lição do capítulo: era fácil medir movimento, porque os dados de tramitação já estavam no banco. Mas o que importava era o conteúdo, e para medir conteúdo alguém, ou algum modelo, precisa ler.
Despacho 05 · jul → ago 2026
A busca estava certa. Só estava lenta.
Duas causas que nenhum EXPLAIN mostra: memória e geografia.
No capítulo 2, o EXPLAIN resolveu dois casos. Aqui o plano de execução estava bom, e mesmo assim a busca levava segundos.
Abre o capítulo de desempenho.
A busca dava as respostas certas, mas levava segundos. As causas não apareciam no plano das consultas (o EXPLAIN do capítulo 2): estavam na memória do servidor e na distância física entre as peças do sistema.
Despacho 05 · Latência Deu errado commit 89295da · EPIC 11–13/08
O índice maior que a memória, duas vezes
A barra verde é o cache de páginas do Postgres na RAM (shared_buffers). A barra escura é o índice HNSW dos vetores. Índice de vetor só é rápido se cabe na memória: o grafo é percorrido aos saltos, e cada salto fora da RAM vira leitura de disco.
28/07: o índice tinha 1,3 GB para um cache de 256 MB. Toda busca lia disco, e estourou a cota de I/O do Supabase. Avance: 29/07, quantização binária, 69 MB (próximo slide). Avance: 11/08, o backfill dobrou o acervo e o índice voltou a passar do cache. Avance: a decisão foi pagar por mais RAM. Avance: hoje.
O erro do assistente nesse episódio: projetou que o acervo dobraria a cada 13 dias. Era o backfill terminando. O ritmo real é de uns 5 MB de índice por mês.
Um índice de vetores só é rápido se cabe na memória. A busca percorre o grafo do índice aos saltos, e cada salto fora da memória vira uma leitura de disco, muito mais lenta.
Em 28/07, o índice tinha 1.304 MB e o cache de memória do banco, 256 MB: toda busca lia disco. Em 29/07, ele caiu para 69 MB (próximo slide). Em 11/08, a carga do histórico dobrou o acervo, e o índice voltou a passar do cache. A decisão foi pagar por mais memória.
Como ler
A barra verde é o cache do Postgres; a barra escura é o índice. A parte vermelha é a que não cabe e vem do disco. Avance (→) ou clique nas datas.
Um erro de projeção
O assistente de IA projetou que o acervo dobraria a cada 13 dias. Era só a carga do histórico terminando: o crescimento real é de uns 5 MB de índice por mês.
Despacho 05 · Latência Solução migration 20260729180000 · 60 consultas reais
1 bit por número para achar, o vetor inteiro para ordenar
Representação Recall@20 Índice
meia precisão (halfvec) 100% 438 MB · não cabe
512 dimensões 80% menor
binário, sem reordenar 49,8% 69 MB
binário + reordenar 400 99,3% 69 MB
Recall@20: dos 20 resultados da busca exata, quantos a busca rápida encontra. Medido em 60 consultas reais antes de escolher.
-- 1) 400 candidatos, distância de Hamming
SELECT chunk_id FROM bill_chunk_half
ORDER BY embedding_bin
<~> binary_quantize(v)
LIMIT 400
-- 2) reordena os 400 pelo cosseno,
-- com o vetor em meia precisão
Preencher a coluna nova com UPDATE: 5 linhas/s , 9 horas. Um INSERT em tabela à parte: 3.333 linhas/s.
O assistente de IA sugeriu cortar para 512 dimensões antes de medir. Teria perdido 20% do recall.
Índice novo construído em 100 s. Busca híbrida: 8,4 ms de mediana.
Quantização binária: cada um dos 1.024 números do vetor vira um bit, positivo ou negativo. O vetor cai de 4 KB para 128 bytes, e comparar dois vetores vira contar bits diferentes (distância de Hamming), que o processador faz muito rápido.
Sozinha, ela erra metade (49,8% de recall). O truque é usar em dois estágios: o índice binário acha 400 candidatos, e só esses 400 são reordenados pelo vetor completo. Recall de 99,3%, num índice 19 vezes menor.
O ponto metodológico: medimos as quatro opções em consultas reais antes de escolher. A sugestão intuitiva, cortar dimensões, era a pior das que cabiam. E um detalhe de banco que vale ouro: atualizar uma coluna em 168 mil linhas é muito mais lento do que inserir numa tabela nova.
Para caber na memória, cada um dos 1.024 números do vetor virou um único bit, positivo ou negativo. O vetor encolhe 32 vezes, e comparar dois vetores vira contar quantos bits são diferentes.
Sozinho, isso erra metade dos resultados. O truque é usar dois estágios: o índice binário acha 400 candidatos rapidamente, e só esses 400 são reordenados com o vetor completo.
Como ler
A tabela compara as opções, medidas em 60 consultas reais antes da escolha. Recall@20: dos 20 resultados corretos, quantos a busca rápida encontra. A opção escolhida (em verde) acha 99,3% num índice 19 vezes menor.
Tropeços
Preencher uma coluna nova com UPDATE levaria 9 horas (5 linhas por segundo), contra 3.333 linhas por segundo inserindo numa tabela nova. E a sugestão de cortar dimensões, feita antes de medir, perderia 20% do recall.
Despacho 05 · Latência Deu errado commits b634b60 · dd67ac3 · 12–14/08
Lenta por geografia, não por SQL
Duas armadilhas: medir a distância até o banco pelo hostname deu 3 ms, mas quem respondia era a borda da Cloudflare; pela porta do Postgres, eram 184 ms. E no deploy a busca por sentido falhava calada e caía na palavra-chave, respondendo 200: a chave da Voyage só vale em outro endereço. Hoje a resposta avisa quando está degradada.
Medimos: a busca levava 1,45 s, e o Postgres fazia 18 ms disso. O resto era viagem. O site e o banco estão em São Paulo, mas a API estava nos EUA, porque o Railway não tem região na América do Sul. Cada busca cruzava o equador três vezes, e cada chamada ao banco custava uns 215 ms.
Avance: a busca foi para uma Edge Function do Supabase, no mesmo datacenter do banco. Só vai aos EUA para gerar o vetor da consulta, e só quando ele não está no cache, o que acontece em 1% das vezes. A API antiga ficou como rota de escape.
As duas armadilhas são lições de medição: meça na porta que o tráfego usa de verdade, e trate “funcionou, mas pelo caminho reserva” como erro, não como sucesso.
A busca levava 1,45 s, e o banco de dados fazia apenas 18 ms disso. O resto era viagem.
O site e o banco ficam em São Paulo, mas a API ficava nos EUA, porque o Railway não tem servidores na América do Sul. Cada busca cruzava o equador três vezes. A busca foi para uma Edge Function do Supabase, no mesmo datacenter do banco.
Como ler
Alterne “Antes” e “Depois”, ou avance com →, e acompanhe o pacote amarelo. Depois da mudança, a viagem aos EUA só acontece para gerar o vetor da consulta, e só quando ele não está no cache (1% das vezes).
Duas armadilhas
Medir a distância até o banco pelo endereço público deu 3 ms, mas quem respondia era um servidor intermediário; pela porta real do banco, eram 184 ms. E um erro de configuração fazia a busca por sentido falhar calada e cair na palavra-chave. Hoje a resposta avisa quando está degradada.
Lição
Meça antes de otimizar: recall antes de cortar, latência na porta certa. Às vezes, a otimização certa é pagar por RAM .
O dono do projeto decidiu subir a instância do banco em 11/08. Engenharia também é saber quando o dinheiro resolve melhor do que mais uma semana de otimização.
A lição do capítulo: medir antes de otimizar (o recall antes de cortar dimensões, a latência na porta que o tráfego usa de verdade) e reconhecer quando gastar com mais memória resolve melhor do que mais uma semana de otimização.
Despacho 06 · jul → out 2026
Ninguém aperta o botão. 33 crons apertam.
De madrugada, o banco acorda a API, que acorda o worker, que acorda a IA. Quem confere se alguém acordou?
Capítulo de operação. A pergunta do título é o capítulo inteiro: automatizar é fácil; saber se a automação fez o trabalho é a parte difícil.
Abre o capítulo de automação.
Ninguém aperta botão: 33 tarefas agendadas fazem todo o trabalho, quase sempre de madrugada. A pergunta do capítulo é quem confere se esse trabalho de fato aconteceu.
Despacho 06 · Crons Tecnologia cron.job · lido em 06/10
Um dia na vida do sistema, em horário de Brasília
Cada risco é um cron diário ou semanal; os arcos são janelas que se repetem até dar certo. Passe o mouse para ver o que cada um faz. Filtre por grupo nos botões.
A ordem da madrugada importa: a importância é calculada antes da relevância; o texto é atualizado antes do detector de jabuti; a ingestão diária roda das 3h às 7h e insiste a cada 10 minutos até conseguir. Os posts são gerados às 7h e publicados ao meio-dia e às 19h.
À direita, o que roda toda hora. No centro do mostrador, o reaper, que roda todo minuto. Guarde o 01:40: é o horário de uma corrida que vamos ver daqui a pouco.
Um dia na vida do sistema, no horário de Brasília.
Cada risco no relógio de 24 horas é uma tarefa diária ou semanal; os arcos são janelas que se repetem até a tarefa conseguir. A ordem importa: a importância é calculada antes da relevância; o texto é atualizado antes do detector de jabuti; a ingestão diária roda das 3h às 7h; os posts são gerados às 7h e publicados ao meio-dia e às 19h.
Como ler
Passe o mouse ou clique num risco para ver o que a tarefa faz. Os botões filtram por grupo. O mostrador menor mostra o que roda toda hora; no centro dele, o reaper, que roda a cada minuto.
Despacho 06 · Crons Tecnologia pg_cron + pg_net + FastAPI
O cron mede se o pedido saiu, não se o trabalho foi feito
pg_cron 33 agendas dentro do banco
gatilho SQL tem fila? o diário já rodou?
net.http_post assíncrono: não espera a resposta
API · FastAPI uma tarefa por casa 202 aceito · 409 não
ingestion_run running → fim pulso a cada 30 s
resultado linhas novas no banco
até aqui, o pg_cron já registrou “succeeded”
409: o disparo se perde, sem erro em lugar nenhum
o trabalho só existe aqui
Por que o banco agenda tudo: o primeiro agendador rodava no Railway e pulou um dia depois de uma rajada de deploys. Agenda dentro do banco não reinicia com o deploy da API.
A arquitetura de execução, da esquerda para a direita. O pg_cron roda um SELECT numa função. A função decide se dispara e faz um POST HTTP pelo pg_net, que é assíncrono: enfileira o pedido e termina.
Avance: o pg_cron registra “succeeded” quando o SELECT termina. Ele nunca sabe se a API aceitou. Se a API devolve 409, porque já há uma tarefa da mesma casa rodando, o disparo some sem erro em lugar nenhum.
Avance: o único lugar onde dá para saber se o trabalho aconteceu é o resultado no banco. Guarde isso, é a base do capítulo 7.
Como uma tarefa agendada acontece: o agendador do banco (pg_cron) roda uma função, que decide se dispara e faz uma chamada HTTP assíncrona, sem esperar a resposta, para a API. A API aceita (202) ou recusa porque já há uma tarefa da mesma casa rodando (409). O trabalho é registrado numa tabela, com um sinal de vida a cada 30 segundos, até gravar o resultado.
Como ler
Avance (→). A marca vermelha mostra até onde o agendador enxerga: ele registra “succeeded” assim que a chamada sai. Se a API recusar, o disparo se perde sem erro em lugar nenhum. A marca verde mostra o único lugar onde dá para saber se o trabalho aconteceu: o resultado no banco.
Por que o banco agenda
O primeiro agendador rodava junto com a API e pulou um dia depois de uma sequência de atualizações do código. A agenda dentro do banco não reinicia quando a API reinicia.
Despacho 06 · Crons Deu errado migration 20261005150000 · 20/09 → 05/10
Uma corrida decidida por segundos
A trava
O gatilho do detector de jabuti não disparava se houvesse qualquer tarefa rodando. A atualização de textos termina por volta de 01:40.
O placar
Rodou em 16 de 66 dias . O pg_cron registrou “succeeded” nos 66. Casamento perfeito com o horário em que a outra tarefa terminava.
O diagnóstico errado
Em agosto, o assistente de IA culpou um 409 da API, e liberamos a API. O pedido nem saía do banco: eram duas travas, e soltamos só uma.
Cada ponto é um dia. A altura é o horário em que a atualização de textos terminou. A linha vermelha é 01:40, quando o gatilho do detector de jabuti olha. Verde: rodou. Vermelho: o gatilho viu outra tarefa ainda rodando e voltou sem disparar.
Só rodou nos dois dias em que a outra tarefa terminou às 01:39. Uma corrida decidida por segundos, por 16 dias seguidos, com o painel do cron todo verde.
A ironia: a trava existia para evitar um 409 da API. Em agosto, a API já tinha sido liberada para essa tarefa. A trava não protegia mais nada e passou a causar a perda que queria evitar. A correção de 05/10 foi apagar quatro linhas.
Uma corrida decidida por segundos.
O detector de jabuti era disparado às 01:40, mas a sua função de disparo desistia se houvesse qualquer outra tarefa rodando. A tarefa de atualização de textos termina, quase todo dia, exatamente por volta de 01:40. Resultado: o detector rodou em 16 de 66 dias, enquanto o painel do agendador mostrava “sucesso” em todos os 66.
Como ler
Cada ponto é um dia; a altura é a hora em que a outra tarefa terminou. A linha vermelha é 01:40. Verde: o detector rodou, porque a outra tarefa acabou antes. Vermelho: não rodou.
O diagnóstico errado
Em agosto, o assistente de IA atribuiu o problema a uma recusa da API, que foi então liberada. Mas o pedido nem saía do banco: havia duas travas, e só uma foi solta. A correção de 05/10 removeu a trava do banco.
Despacho 06 · Crons Solução commits 097efa1 · eb93508 · 9f65bb8
Todo push mata o que está rodando
um run de texto, visto do banco
pulso a cada 30 s
git push → redeploy processo morto
no banco, continua “running”
reaper, todo minuto 3 min sem pulso → failed
fila segue
45 → 3 min
para destravar a fila depois de uma queda
52 de 70
tinham um commit em main nos 20 min anteriores
80%
dos ~54 mil disparos mensais do pg_cron são o reaper
O registro de tarefas da API fica em memória. Todo push em main redeploya o Railway e mata o processo, mas no banco o run continua “running” para sempre, e as tarefas seguintes esperavam 45 minutos.
A solução: um pulso (heartbeat) a cada 30 segundos, numa thread separada, e um faxineiro (reaper) que roda todo minuto e marca como falho quem está sem pulso há 3 minutos. Vivo ou morto se decide pelo frescor do pulso, não pela idade da tarefa.
Tropeço dentro da solução: a primeira versão do pulso usava upsert, o INSERT violava uma restrição, o erro era engolido e o pulso nunca avançava. E a regra que ficou para o time: conferir o que está rodando antes de dar push.
Todo envio de código novo (push) reinicia o servidor da API, e o processo que estava trabalhando morre. No banco, porém, a tarefa continuava marcada como “rodando” para sempre, e as seguintes esperavam 45 minutos.
A solução: cada tarefa grava um pulso a cada 30 segundos, e um faxineiro (reaper) roda todo minuto e marca como falha quem está sem pulso há 3 minutos.
Como ler
A linha do tempo mostra os pulsos, o deploy que mata o processo e o reaper liberando a fila. Avance (→) para cada etapa.
Números
98 execuções zumbis no histórico; em 52 de 70 delas, houve um envio de código nos 20 minutos anteriores. O reaper responde por 80% dos cerca de 54 mil disparos mensais do agendador.
Despacho 06 · Crons Deu errado commits f324e55 · 4d030bf · a856e90
Uma trava global onde a regra era por casa
Uma tarefa pesada por vez, para não estourar a API. Mas a trava era global : um backfill da Câmara de 4 dias bloqueou o Senado e a ingestão diária, a única fonte de proposições novas.
O diário lia 2 dias para trás. Ficou 3 dias bloqueado: só não perdemos dado porque caiu num fim de semana.
Uma trava por casa : os hosts são independentes.
O diário reserva prioridade por 2 h e insiste a cada 10 min.
Janela de 2 para 4 dias. Desde 29/07, nenhum dia sem diário.
Duração média da ingestão diária, por semana (minutos).
Opcional. A mesma família de bug apareceu em seis camadas: uma trava global onde a regra deveria ser por recurso.
A correção usa preempção cooperativa: o diário grava “tenho prioridade pelas próximas 2 horas” numa tabela de configuração, e os backfills cedem a vez. O gráfico mostra o efeito acumulado de várias melhorias: a ingestão diária caiu de 4 horas e meia para menos de uma hora.
Para não sobrecarregar as APIs, só uma tarefa pesada rodava por vez. Mas a trava era global : uma carga do histórico da Câmara, que levou 4 dias, bloqueou o Senado e a ingestão diária, a única fonte de proposições novas.
A ingestão diária olhava 2 dias para trás e ficou 3 dias bloqueada. Só não houve perda porque caiu num fim de semana.
A solução
Uma trava por casa, porque as duas APIs são independentes. A ingestão diária reserva prioridade por 2 horas e insiste a cada 10 minutos. E passou a olhar 4 dias para trás.
Como ler
O gráfico mostra a duração média da ingestão diária por semana: de 4 horas e meia em julho para menos de uma hora em setembro, efeito acumulado de várias melhorias.
Lição
“O cron rodou” e “o trabalho foi feito” são perguntas diferentes .
Foram cinco corridas de cron no projeto. Todas com o painel verde.
Ponte para o próximo capítulo: se o mecanismo mente, o monitoramento precisa olhar para o resultado.
A lição central da operação: “o agendador rodou” e “o trabalho foi feito” são perguntas diferentes. Houve cinco corridas desse tipo no projeto, todas com o painel verde.
Despacho 07 · ago → out 2026
O mecanismo diz que está tudo bem. O resultado diz a verdade.
Em vez de vigiar 33 crons, passamos a vigiar 8 resultados.
Capítulo curto: como o sistema avisa quando quebra, e por que até um bom alerta mente às vezes.
Abre o capítulo de monitoramento.
Vigiar se cada tarefa rodou não funcionou, como mostrou o capítulo anterior. A saída foi vigiar o resultado: em vez de 33 tarefas, 8 perguntas sobre o dado.
Despacho 07 · Alertas Tecnologia Sentry · /health · ingestion_run · expectativas
Quatro camadas de olho, do erro ao resultado
1 · Erro
Sentry No site e na API, com source maps. Foi ele que mostrou os 2.008 timeouts da busca. O rastreio de desempenho amostra 2% das visitas.
2 · Saúde
/health O site responde se está vivo. A API responde ok, velho ou degradado: velho é dado com mais de 36 h.
3 · Execução
ingestion_run Cada tarefa vira uma linha com início, pulso e fim. O painel admin cruza disparo com run e mostra os 409.
4 · Resultado
Expectativas Oito perguntas sobre o dado, não sobre o processo. Violou? E-mail, três vezes por dia.
Nenhuma das três primeiras camadas avisou sobre as corridas do capítulo anterior: em nenhuma delas houve erro.
Vá da esquerda para a direita, do mais técnico ao mais útil.
Sentry pega exceção. /health pega serviço fora do ar. A tabela de runs mostra o que rodou. Nenhuma das três pega o caso mais comum deste projeto: nada quebrou, nenhum erro, e mesmo assim o trabalho não aconteceu.
A quarta camada inverte a pergunta. Em vez de “o job rodou?”, ela pergunta “o dado novo chegou?”.
As quatro camadas de monitoramento do projeto:
(1) o Sentry registra os erros do site e da API; (2) os endereços /health dizem se cada serviço está no ar e se o dado está fresco; (3) a tabela de execuções registra cada tarefa, com início, pulso e fim; (4) as expectativas perguntam se o dado novo chegou.
Como ler
Avance (→) para revelar cada camada.
Por que importa
As três primeiras camadas olham o processo. As corridas do capítulo anterior não geraram erro nenhum, então nenhuma delas avisou. Só a quarta, que olha o resultado, pega esse tipo de falha.
Despacho 07 · Alertas Solução migration 20260808090000
Cada expectativa diz quando algo aconteceu e quando é tarde
with expectativa(nome, ultimo, max_horas)
as (values
('news' ,
(select max(published_at) from news), 6 ),
('tema-em-alta' ,
(select max(discovered_at) from bill_hot),
192 ),
...)
select nome from expectativa
where ultimo < now()
- make_interval(hours => max_horas)
O limite sai do ciclo de cada coisa: horário tolera 6 h; diário, 36 a 48 h; semanal, 8 dias.
Silêncio de 12 h por alerta: alerta que vira ruído ninguém lê.
A consulta mora no corpo da função, não numa tabela: SQL guardado e executado com privilégio de dono é porta de injeção.
Expectativa Limite Último alerta
news 6 h 19/09
embed-news 12 h nunca
analise-llm 36 h 06/10
tramitacao 36 h 05/10
texto-proposicao 48 h 10/08
rascunho-social 96 h 05/10
tema-em-alta 8 dias 24/09
tema-popular 8 dias 28/08
Cada expectativa é uma linha: um nome, uma consulta que diz quando a coisa aconteceu pela última vez, e um limite em horas. O detector não precisa saber por que falhou. 409, orçamento estourado, agenda errada, worker morto e bug novo dão todos o mesmo sintoma: resultado velho.
Os limites não são palpite. Uma coisa semanal tolera 8 dias: 7 é o normal, 8 só acontece se uma semana inteira foi perdida.
O detalhe de segurança é para os devs: a versão “flexível” guardaria o SQL de cada checagem numa tabela e executaria com EXECUTE. Numa função que roda com privilégio de dono, isso é injeção esperando para acontecer. Mais rígido e mais seguro: adicionar expectativa é escrever uma migration.
Cada expectativa tem três partes: um nome, uma consulta que diz quando a coisa aconteceu pela última vez e um limite de idade. Se a notícia mais recente tem mais de 6 horas, ou o tema em alta mais recente tem mais de 8 dias, o sistema manda um e-mail.
Os limites vêm do ciclo de cada coisa: algo que roda de hora em hora tolera 6 horas; algo diário, de 36 a 48 horas; algo semanal, 8 dias.
Como ler
À esquerda, o formato real da consulta; à direita, as 8 expectativas, seus limites e a data do último alerta enviado por cada uma.
Dois detalhes
Cada alerta só se repete depois de 12 horas, para não virar ruído. E as consultas ficam escritas dentro da própria função, não numa tabela: executar SQL guardado numa tabela, com privilégios de dono do banco, abriria uma porta para injeção.
Despacho 07 · Alertas Solução commits 5725cd2 · cbc566f · ingestion_run
A rede de segurança redispara sozinha
Temas em alta descobertos por rodada. Segunda, 03:37, é o horário agendado. Os disparos das 04:41 em outros dias são a rede de segurança: “o tema mais novo tem mais de 8 dias? Dispara de novo.”
Antes da rede, a descoberta ficou três semanas muda com o painel verde: perdeu a corrida do minuto :00, depois bateu no teto de orçamento, depois cortou a resposta no limite de tokens e gravou zero. Ainda há rodadas que pagam, acham zero e terminam como sucesso. A causa não está confirmada.
Opcional. Este é o primeiro estágio que ganhou rede de segurança, e o padrão depois foi generalizado nas expectativas.
Leia o gráfico pelos zeros. Em setembro, duas segundas seguidas (14 e 21) acharam zero. Nos dois casos a rede redisparou e recuperou: 17 temas em 16/09, 23 em 25/09. Passe o mouse nas barras para ver o custo e o motivo de cada zero.
Honestidade: ainda não sabemos por que algumas rodadas pagam 60 a 74 centavos, acham zero e terminam como sucesso. A rede cobre o sintoma; a causa está aberta.
O primeiro estágio a ganhar uma rede de segurança foi a descoberta semanal de “temas em alta” na imprensa. Ela ficou três semanas muda com o painel verde: primeiro perdeu uma corrida no minuto :00, depois esbarrou no teto de orçamento, depois a resposta da IA veio cortada pelo limite de tamanho e gravou zero.
A rede é uma tarefa diária que pergunta: o tema mais novo tem mais de 8 dias? Se sim, dispara a descoberta de novo.
Como ler
Cada barra é uma execução, com o número de temas encontrados. Segunda-feira às 03:37 é o horário agendado; as execuções às 04:41 em outros dias são a rede redisparando. Passe o mouse para ver o motivo de cada zero.
Despacho 07 · Alertas Deu errado e-mails de 05/10 e 06/10
O alerta é tão bom quanto a coluna que ele lê
05/10 · “tramitacao há 3 dias”
Mediu o Congresso, não a gente A coluna é o último evento no Congresso . No fim de semana do 1º turno, houve zero tramitações. Na segunda, 184. E o Senado tem essa coluna vazia em todas as proposições: o alerta só cobre a Câmara.
06/10 · “analise-llm há 72 horas”
A coluna só muda no INSERT A análise rodou nas duas noites: 150 por noite. Mas ela atualiza linhas que outro estágio cria, e esse outro estágio não rodou. A data lida é a de criação da linha.
E quando acertou
17/08: tema em alta 10 dias sem dado novo. 18/08: rascunho social parado, que levou à corrida do detector de jabuti. 05/10: rascunho social de novo, que revelou três bugs reais, assunto do capítulo 9.
Dois falsos positivos recentes, e os dois têm a mesma raiz: o alerta lê a coluna errada.
O primeiro mede um evento externo. Num fim de semana de eleição, o Congresso não tramita nada, e o alerta grita. O segundo mede a data de criação da linha, quando o trabalho é uma atualização.
O lado bom: os acertos. O alerta de rascunho social de 05/10 é o fio que puxa o capítulo de redes sociais.
Dois alarmes falsos recentes, com a mesma raiz: o alerta lia a coluna errada.
O de tramitação mede o último evento no Congresso, não a última execução do sistema: no fim de semana do 1º turno das eleições, o Congresso não tramitou nada, e o alerta disparou. O de análise por IA lia a data de criação de cada linha, mas a análise atualiza linhas que outro estágio cria.
E quando acertou
Os alertas pegaram a descoberta de temas parada (17/08), o gerador de posts parado (18/08, o que levou à corrida do detector de jabuti) e os três bugs de 05/10, assunto do capítulo 9.
Lição
Vigie o resultado, tire o limite do ciclo e desconfie da coluna que você está lendo.
Ponte: agora que o sistema roda sozinho e avisa quando para, a pergunta seguinte é quanto isso custa.
A lição do capítulo: vigiar o resultado, tirar o limite de idade do ciclo natural de cada coisa e desconfiar da coluna que o alerta lê.
Despacho 08 · jul → set 2026
A maior conta não veio da IA. Veio dos robôs.
Em agosto: 171 visualizações de página feitas por gente, em 8 dias. E 355.823 execuções de função, em 24 horas.
Capítulo com os custos reais, sem arredondar para baixo. O projeto é pequeno, então cada dólar aparece.
A frase do título surpreende estudante: a IA, que todo mundo acha cara, custou menos do que servir páginas para robôs.
Abre o capítulo dos custos reais.
A conta mais alta não veio da inteligência artificial, mas de servir páginas para robôs. Em agosto, houve 171 visualizações de página feitas por pessoas em 8 dias, contra 355.823 execuções de função em 24 horas.
Despacho 08 · Custos Problema Vercel FOCUS · 28/07 → 04/10
A fatura da hospedagem, semana a semana
Uso semanal da Vercel em US$, sem a assinatura de US$ 20 por mês.
US$ 114,93
1º ciclo, 28/07 a 27/08
US$ 34,43
2º ciclo, 28/08 a 27/09
Para comparar, a IA (Claude) por mês: julho US$ 67,95, agosto US$ 39,40, setembro US$ 45,73. Em agosto, a hospedagem custou quase o triplo da IA.
Contexto: até julho o site cabia no plano gratuito da Vercel, com 4 horas de CPU por mês. Em 28/07 tínhamos usado 10h46. O dono subiu para o plano Pro, de 20 dólares, com a meta de voltar ao gratuito. Não voltamos.
O gráfico é o uso além da assinatura. O pico de agosto, 94 dólares em duas semanas, foi quase todo robô. Depois do bloqueio de 24/08, a semana custa uns 6 dólares. No fim de setembro, sobe de novo: é o próximo problema, no fim da palestra.
Total transparente: uns 170 dólares de uso de hospedagem desde 28/07, mais a assinatura de 20 por mês; uns 160 dólares de IA desde julho, pela estimativa do próprio sistema; e 7 dólares de embeddings.
A fatura de hospedagem da Vercel, semana a semana, sem a assinatura fixa de US$ 20 por mês.
O pico de agosto (US$ 51,80 e US$ 42,03 em duas semanas) foi quase todo causado por robôs. Depois do bloqueio de 24/08, a semana passou a custar uns US$ 6. No fim de setembro, uma nova onda de robôs fez a conta subir de novo.
Como ler
Cada barra é uma semana; passe o mouse para ver o que aconteceu. À direita, o total de cada ciclo de cobrança e, para comparar, o gasto mensal com IA.
Contexto
Até julho, o site cabia no plano gratuito da Vercel, de 4 horas de CPU por mês. Em 28/07, o uso tinha chegado a 10h46, e o projeto passou para o plano pago, com a meta de voltar ao gratuito.
Despacho 08 · Custos Tecnologia anatomia de 24/08 · Fluid Compute
Invocação é barata. Paga-se tempo e bytes.
O 1º ciclo até 23/08, por item. A observabilidade, cobrada à parte, somava mais US$ 23,49.
É a mesma coisa medida de três jeitos.
2,22 mi execuções × ~27 KB ≈ 59 GB
2,22 mi execuções × ~1 s × 2 GB ≈ 1,2 mil GB·h
~70% do tempo pago = esperar banco e rede
Página lenta custa dinheiro mesmo sem fazer nada: no Fluid Compute, a memória é cobrada pelo tempo de relógio. Quando o robô sobrecarregava o banco, cada página esperava mais e custava mais.
Para quem vai usar serverless: a intuição diz que se paga por requisição. Aqui, a invocação foi o item mais barato, US$ 1,33 em 2,22 milhões.
Paga-se CPU ativa, memória provisionada pelo tempo que a função fica aberta e os bytes que saem dela. Os três itens grandes são o mesmo fenômeno: muitas páginas distintas, sem cache, cada uma esperando o banco.
Por isso, otimizar o banco também reduziu a conta de hospedagem.
Na hospedagem serverless da Vercel, o que se paga não é o número de cliques. Paga-se o tempo de CPU, a memória pelo tempo em que a função fica aberta e os bytes enviados.
Até 23/08, as 2,22 milhões de execuções em si custaram só US$ 1,33. O resto, cerca de US$ 84, foi CPU, transferência e memória.
Como ler
As barras mostram cada item da fatura. À direita, a conta que mostra que os três itens grandes são o mesmo fenômeno: muitas páginas diferentes, sem cache, cada uma esperando o banco. Cerca de 70% do tempo pago era espera.
Por que importa
Uma página lenta custa dinheiro mesmo sem fazer nada. Quando os robôs sobrecarregavam o banco, cada página esperava mais e ficava mais cara.
Despacho 08 · Custos Deu errado commit aabc416 · 08–09/08
Declarar cache não é ter cache
Antes · a mesma página, três vezes x-vercel-cache: MISS
x-vercel-cache: MISS
x-vercel-cache: MISS
cache-control: private, no-store
Depois x-vercel-cache: HIT
tempo até o 1º byte: 0,18–0,21 s
// sozinho, o revalidate é ignorado
export const revalidate = 86400 ;
// lista vazia: ISR sob demanda, sem
// pré-gerar 78 mil páginas no build
export async function generateStaticParams() {
return [];
}
16 h de CPU
num único dia (08/08): US$ 9,26, e o crédito do ciclo acabou ali
O analytics viu 26 visualizações em 2 dias. Os logs, 45 renderizações em 4 minutos: robôs percorrendo as 16.838 URLs do sitemap. Os commits de julho diziam “ISR ligado”. Ninguém tinha conferido o cabeçalho.
ISR é o recurso do Next.js que gera a página uma vez, guarda em cache e regenera de tempos em tempos. As páginas de proposição declaravam revalidação de 24 horas. Mas, numa rota com parâmetro, o Next só liga o ISR se a página também exporta generateStaticParams. Sem ela, a rota é dinâmica e o revalidate é ignorado em silêncio.
A prova estava no cabeçalho HTTP: MISS, MISS, MISS e “private, no-store”. A correção é uma função que devolve uma lista vazia: nada é gerado no build, e cada página entra no cache na primeira visita.
Erro do assistente: dois commits de julho afirmavam “ISR ligado” sem verificar. Lição: confira o símbolo da rota no build e o cabeçalho x-vercel-cache.
As páginas de proposição declaravam que deviam ficar em cache por 24 horas. Mas, numa rota com parâmetro, o Next.js só liga esse cache se a página também declarar a função generateStaticParams. Sem ela, a configuração era ignorada em silêncio, e cada visita, quase sempre de robô, gerava a página de novo.
Num único dia (08/08), isso somou 16 horas de CPU e US$ 9,26.
Como ler
À esquerda, os cabeçalhos HTTP de antes: MISS (não veio do cache) três vezes seguidas e “private, no-store”. À direita, depois: HIT. O código mostra a correção, uma função que devolve uma lista vazia.
O erro
Dois commits de julho afirmavam que o cache estava ligado, sem que ninguém tivesse conferido o cabeçalho da resposta.
Despacho 08 · Custos Problema commits 57dd60b · ec3cd52 · regras de firewall
Três adversários, três eixos de bloqueio
Requisições por hora no /buscar, fase a fase. Vermelho: o robô sem bloqueio.
1 · nome meta-externalagent/1.1 Ignorava o robots.txt genérico. Bloqueado pelo nome, voltou com outro: meta-webindexer.
2 · classe bot | crawl | spider | indexer | meta- | … Bloqueia qualquer um que se declare robô. A Meta não escapa trocando de crachá.
3 · endereço “Chrome 133” · 47.79.201.58 · Alibaba Cloud Disfarçado de navegador, com referer falso do Google, a 7 por minuto: abaixo do rate limit. Só a faixa de IP denunciava.
Como achamos o culpado: os logs da Vercel não guardam user-agent nem IP. A página de busca foi instrumentada por algumas horas para registrar os cabeçalhos: 28 de 28 amostras eram o robô da Meta, que ignorava a regra genérica do robots.txt.
Três adversários, três eixos. Pelo nome: funciona até o robô trocar de nome, e a Meta trocou em uma semana. Pela classe: uma expressão regular que pega qualquer um que se declare robô. Pelo endereço: o scraper paciente, disfarçado de Chrome, abaixo do limite de taxa. Esse só se pega pela faixa de IP do datacenter.
Decisão do dono: bloquear só a rota de busca, que é ferramenta. Ser lido e citado por IA nas páginas de conteúdo é objetivo do projeto.
Três tipos de robô exigiram três formas de bloqueio.
Pelo nome: o robô da Meta ignorava o pedido genérico do robots.txt e foi bloqueado pelo nome; voltou em uma semana com outro nome. Pela classe: uma regra que bloqueia qualquer um que se declare robô (bot, crawl, spider…). Pelo endereço: um programa disfarçado de navegador Chrome, num datacenter da Alibaba, a 7 pedidos por minuto, abaixo do limite de taxa. Só a faixa de IP o denunciava.
Como ler
As barras mostram os pedidos por hora na página de busca em cada fase; vermelho é o robô ainda sem bloqueio. Avance (→) para revelar os três cartões.
Como achamos o culpado
Os registros da Vercel não guardam o User-Agent. A página de busca foi instrumentada por algumas horas para registrar os cabeçalhos: 28 de 28 amostras eram o robô da Meta.
Despacho 08 · Custos Solução vercel firewall · regras reais, em ordem
robots.txt é etiqueta. Firewall é porta.
Escolha requisições e veja qual regra pega cada uma. Sugestão de ordem: a pessoa no Chrome (passa), os dois robôs da Meta (classe), o Googlebot (também bloqueado no /buscar, coerente com o robots.txt), o Chrome falso da Alibaba (endereço) e a rajada (limite de taxa). Depois troque o caminho para a página de proposição: nenhuma regra vale lá, de propósito.
Bloqueio no firewall acontece antes da função rodar, então não gera custo de execução. O robots.txt é só um pedido educado.
Três pegadinhas que valem para quem configura firewall: o operador “inc” testava igualdade exata e a regra não pegava ninguém; uma duração de bloqueio de 5 minutos tirava o site inteiro de quem estourava o limite; e limite de taxa se testa com rajada concorrente. 75 curls em sequência deram falso negativo.
Um simulador das três regras reais do firewall da Vercel, na ordem em que são avaliadas: limite de 60 pedidos por minuto por IP; bloqueio de robôs pela classe do User-Agent; bloqueio da faixa de IP do datacenter.
O bloqueio acontece antes de o código do site rodar, então não gera custo de execução. Já o robots.txt é só um pedido educado: quem não quiser, ignora.
Como ler
Escolha uma requisição e veja qual regra a pega, e por quê. Depois troque o caminho para a página de proposição: as regras valem só para a busca, porque ser lido por buscadores e IAs nas páginas de conteúdo é objetivo do projeto. Os IPs de pessoas são ilustrativos.
Despacho 08 · Custos Deu errado commits 6aa0835 · cacf2b9 · 31/08
O sitemap convidava os robôs para 26.507 páginas vazias
A consulta do sitemap checava se existia uma linha de análise, não se havia análise nela. 26.507 linhas estavam vazias: sem resumo, sem tema, sem jabuti.
9.878
depois do filtro (−73%)
23%
da fatura do 2º ciclo era escrita de cache
Página sem análise ganhou noindex, follow , que se desfaz sozinho quando a análise chega.
Decisão do dono · 31/08
Não migrar para a Cloudflare Ela não cobra transferência, mas a troca custaria semanas de porte do ISR e das imagens de prévia para economizar uns 10 a 20 dólares por mês. O custo era superfície criada pelo próprio projeto, e o projeto podia desfazê-la.
Opcional. Sitemap é a lista de páginas que você convida os buscadores a visitar. Cada URL anunciada é uma renderização paga quando o robô aparece, e uma escrita de cache a cada revalidação.
O erro foi de consulta: “existe linha” no lugar de “tem análise”. Com o filtro, o sitemap caiu 73%, de 6,6 MB para 1,8 MB.
A decisão de não migrar é a lição de arquitetura: antes de trocar de fornecedor por causa da fatura, confira se a conta não vem de algo que você mesmo criou.
O sitemap é a lista de páginas que o site convida os buscadores a visitar. A consulta que o montava conferia se existia uma linha de análise, não se a análise tinha conteúdo: 26.507 das 36.385 páginas anunciadas estavam vazias, sem resumo, tema ou jabuti.
Cada uma era uma página gerada e paga quando um robô chegava. O filtro reduziu o sitemap em 73%, e páginas sem análise passaram a pedir para não ser indexadas, até a análise chegar.
A decisão
Não migrar para outro provedor de hospedagem: a economia seria de US$ 10 a 20 por mês, contra semanas de trabalho, e o custo vinha de algo criado pelo próprio projeto.
Lição
Paga-se tempo e bytes, não cliques. E toda URL que você anuncia é um convite pago .
Ponte para o último capítulo técnico: com o custo sob controle, faltava o dado chegar em quem não abre o site.
A lição do capítulo: em hospedagem serverless, paga-se tempo e bytes, não cliques. E cada endereço que o site anuncia é um convite, que custa dinheiro quando é aceito.
Despacho 09 · jul → out 2026
Dado parado não fiscaliza ninguém. Precisa chegar nas pessoas.
Newsletter, Instagram, Facebook e Bluesky, sem ninguém apertando “publicar”.
Último capítulo técnico, e o mais recente: a auto-publicação entrou no ar em 30/09. É o capítulo com mais bugs por dia de vida.
Abre o capítulo sobre levar a informação até as pessoas: newsletter, Instagram, Facebook e Bluesky.
A publicação automática entrou no ar em 30/09. É o capítulo mais recente, e o que teve mais bugs por dia de vida.
Despacho 09 · Alcance Tecnologia ingestion/social.py · /social/autopublish
Do rascunho às três redes, sem clique humano
de manhã · gerar
07:04 · cron um formato por dia
candidatos SQL ranqueado
Sonnet 5 escreve até 280 caracteres
rascunho na fila, pendente
e-mail ao admin só pode vetar
lê a fila
o dia todo · publicar
pg_cron a cada 10 min
planejador SQL slots de 12h e 19h
post vencido? só então acorda a Vercel
imagem + carrossel IA faz só o fundo
Bluesky · IG · FB e relê o banco
Pular a revisão da Meta
Bluesky pelo protocolo aberto, de graça. Instagram e Facebook por um agregador, o Zernio, cujo app já é aprovado pela Meta.
Texto desenhado, não gerado
Modelo de imagem erra letra. A IA gera só o fundo; título e números são desenhados por código, com @vercel/og.
Barato por design
US$ 0,014 pelo texto de cada post. O planejador roda em SQL e só acorda a Vercel quando há post para sair.
Duas metades. De manhã, um gerador por formato (jabuti de segunda a sexta, votação e resumo na terça, pauta na quarta, curiosidade na quinta) escolhe candidatos em SQL, o Sonnet 5 escreve o texto e o rascunho entra na fila. O admin recebe um e-mail e só pode vetar.
Avance: o dia todo, a cada 10 minutos, o pg_cron roda o planejador dentro do banco. Ele só acorda a Vercel se houver post vencido, para não gerar execução à toa, que é o assunto do capítulo anterior. A Vercel gera a imagem e o carrossel e publica nas três redes.
Escolhas de custo e prazo: a revisão de app da Meta leva semanas, então usamos um agregador já aprovado. O X ficou de fora: a API custaria uns 60 dólares por mês.
O caminho de um post, em duas metades.
De manhã, um gerador por formato escolhe candidatos no banco, o Claude Sonnet escreve o texto (até 280 caracteres) e o rascunho entra na fila; o responsável recebe um e-mail e só pode vetar. O dia todo, a cada 10 minutos, um planejador no banco distribui os rascunhos nos horários das 12h e das 19h e só acorda o site quando um post venceu. Então o site gera a imagem e o carrossel e publica nas três redes.
Como ler
Avance (→) para revelar a segunda metade do fluxo e as escolhas de projeto.
Escolhas
O Bluesky é acessado pelo seu protocolo aberto, de graça. Instagram e Facebook passam por um agregador que já tem a aprovação da Meta, para evitar semanas de revisão. A IA gera só o fundo da imagem; o texto é desenhado por código, porque modelos de imagem erram letras.
Despacho 09 · Alcance Tecnologia social_auto_plan() · replay de 30/09 a 08/10
A primeira semana real da fila automática
Replay dos eventos reais, um por avanço. O algoritmo é o mesmo da função SQL: o melhor de cada lote (mesmo formato, mesmo dia) pega o próximo horário bom; as sobras preenchem os horários livres, o que naturalmente cai no fim de semana.
Repare: na quarta, a segunda pauta é rejeitada no Studio antes das 19h. O veto humano funciona. Na quinta, entram 3 jabutis e 3 curiosidades, e a fila se espalha até sábado. A borda vermelha marca a MPV 1393: a mesma medida provisória, uma vez por casa. Saiu duas vezes no Bluesky.
Na sexta e na segunda, o gerador de jabuti cria zero rascunhos. Isso dispara o e-mail de alerta de 05/10, assunto dos próximos slides. Os ✗ são o Zernio sem pagamento: o post saiu só no Bluesky.
Um replay dos eventos reais da primeira semana da fila automática, de 30/09 a 08/10.
A regra: o melhor rascunho de cada lote (mesmo formato, mesmo dia) pega o próximo horário bom; as sobras preenchem os horários livres seguintes, o que naturalmente cai no fim de semana.
Como ler
Avance (→) evento a evento. Cada cartão é um post num horário (12h ou 19h). A estrela marca o melhor do lote; ✓ marca o que foi publicado; B, F e I são Bluesky, Facebook e Instagram, com ✗ quando o agregador estava sem pagamento. A borda vermelha marca a mesma medida provisória publicada duas vezes, uma por casa.
Despacho 09 · Alcance Deu errado commit f01802c · 05/10
Limitar antes de filtrar: zero rascunhos, para sempre
O gerador pedia 3 candidatos ao banco e só então pulava os que já tinham rascunho. Os 3 mais recentes já tinham. Então toda rodada pulava os 3 e criava zero, com 14 jabutis elegíveis logo abaixo do corte. Sem erro: o run terminava como sucesso.
Avance: a correção pede uma janela de 50, tira duplicados (inclusive a mesma proposição nas duas casas, pela designação) e só depois aplica a cota de 3. Note as duas linhas vermelhas: o post veio cortado. É o bug do próximo slide.
Padrão para levar para casa: LIMIT antes de WHERE, em qualquer linguagem, é bug esperando dado.
O gerador de jabutis pedia 3 candidatos ao banco e só depois descartava os que já tinham rascunho. Como os 3 mais recentes já tinham, todo dia ele descartava os 3 e criava zero rascunhos, com 14 candidatos válidos logo abaixo do corte. A tarefa terminava como sucesso.
Como ler
Alterne “Antes” e “Depois”, ou avance com →. Depois da correção, o gerador pede 50, descarta duplicados (inclusive a mesma proposição nas duas casas) e só então aplica a cota de 3. As linhas vermelhas são posts cortados pelo limite de tamanho: o assunto do próximo slide.
Por que importa
Limitar antes de filtrar é um erro clássico, em qualquer linguagem: funciona até o dia em que os primeiros itens deixam de servir.
Despacho 09 · Alcance Deu errado commit 0081376 · 05/10
O raciocínio do modelo come o orçamento da resposta
No Sonnet 5, o pensamento adaptativo sai do mesmo max_tokens da resposta. Pensou demais, o JSON veio cortado.
Mesma causa derrubou a canonicalização mensal de temas em 01/10, com o JSON cortado no caractere 3.212. Teto não é custo: paga-se o que o modelo usa. Depois da correção, o maior post gastou 1.343 tokens.
Os posts mais complexos faziam o modelo pensar mais, e o pensamento consumia o teto de 1.200 tokens antes de o JSON terminar. O código descartava a resposta cortada e seguia: os dois candidatos de maior rank depois do primeiro eram justamente os pulados.
Avance: com teto de 8.000, todas cabem. Teto alto não encarece: cobra-se o que é usado. Mas teto baixo, com raciocínio adaptativo, desperdiça a chamada inteira.
Foram 19 de 56 gerações em 30 dias, 34%, batendo exatamente no teto. A mesma causa já tinha derrubado outro estágio quatro dias antes.
No Claude Sonnet 5, o raciocínio interno do modelo consome o mesmo limite de tamanho da resposta. Com o limite em 1.200 tokens, quando o modelo pensava mais, o post em formato JSON vinha cortado no meio e era descartado: 19 de 56 gerações em 30 dias.
Como ler
Cada barra é uma chamada: azul é o raciocínio, verde é o post. A linha preta é o limite. Troque o limite nos botões, ou avance com →: com 8.000, todas cabem.
Por que importa
Limite alto não encarece, porque se paga o que o modelo usa. Mas limite baixo, com raciocínio adaptativo, desperdiça a chamada inteira. A mesma causa já tinha derrubado outro estágio quatro dias antes.
Despacho 09 · Alcance Solução migration 20260930120000
Um UPDATE que casa zero linhas não é erro. Devia ser.
update public.social_post
set posted = posted || ...,
status = 'posted'
where id = p_id
and (public.is_admin()
or auth.role() = 'service_role' );
-- ↑ a correção: aceitar a chave de serviço
O robô publica com a chave de serviço. Nela não há usuário: is_admin() é falso, o UPDATE casa zero linhas e ninguém reclama.
O post sairia no Bluesky sem ficar registrado, e cada nova tentativa publicaria de novo. O Bluesky não tem proteção contra duplicata.
Depois de publicar, o robô relê o banco . Se não gravou, para tudo.
Pegar a tarefa é atômico: FOR UPDATE SKIP LOCKED , trava de 6 min, até 5 tentativas.
Achado antes de ir ao ar, no ensaio feito de dentro do banco.
Opcional, mas é o bug que mais gosto de contar para dev. A função que marca “publicado” exigia ser admin. O robô roda com a chave de serviço, sem usuário logado. A checagem dava falso, o UPDATE não alterava nenhuma linha, e não havia erro nenhum.
Consequência, se fosse ao ar: post publicado e não registrado, e cada nova tentativa, até cinco, publicaria de novo. Foi achado no ensaio, antes de ligar a chave.
A defesa ficou em duas camadas: aceitar explicitamente a chave de serviço e, depois de publicar, reler o banco. Se o registro não persistiu, o robô para.
A função que marca um post como publicado exigia um usuário administrador. O robô de publicação usa uma chave de servidor, sem usuário. A checagem dava falso, a atualização não alterava nenhuma linha e não havia erro.
Se fosse ao ar assim, o post sairia no Bluesky sem ficar registrado, e cada nova tentativa, até cinco, publicaria de novo.
A solução
Aceitar explicitamente a chave de servidor e, depois de publicar, reler o banco: se o registro não persistiu, o robô para. Pegar a tarefa da fila é atômico, para que dois processos nunca publiquem o mesmo post. O problema foi achado num ensaio, antes de ir ao ar.
Despacho 09 · Alcance Problema banco e feed público · 06/10
A automação está pronta. A audiência, ainda não.
0
seguidores no Bluesky: 14 posts, 2 curtidas no total
2
assinantes confirmados da newsletter, 11 edições compostas por IA
10
contas de usuário no site
10
posts automáticos até 06/10: 7 nas três redes, 3 só no Bluesky
O sistema publica sozinho, todo dia. O próximo problema não é de engenharia: é de distribuição.
O slide mais honesto da palestra. Construímos ingestão, busca, IA, monitoramento e publicação automática. A newsletter tem 2 assinantes confirmados; o Bluesky, nenhum seguidor.
Isso não é motivo de vergonha, é um dado: a engenharia andou mais rápido do que a audiência. Por isso, a métrica principal do projeto passou a ser usuários do app, e não volume de posts.
Se houver tempo, convide a plateia: o site é aberto, e a newsletter também.
Os números honestos da audiência em 06/10: nenhum seguidor no Bluesky (14 posts, 2 curtidas no total), 2 assinantes confirmados da newsletter e 10 contas de usuário no site.
A engenharia andou mais rápido do que a audiência. O próximo problema não é técnico, é de distribuição. Por isso a métrica principal do projeto passou a ser usuários do site, não volume de posts.
Lição
Automatizar é fácil. Difícil é não duplicar, não sumir e não falhar calado .
Os bugs do capítulo, juntos: limitar antes de filtrar, a mesma proposição em duas casas, o teto de tokens e o UPDATE que não alterava nada. Nenhum deles gerou erro. Todos apareceram pelo resultado.
A lição do capítulo: automatizar é fácil; o trabalho está em não duplicar, não sumir e não falhar calado. Nenhum dos bugs do capítulo gerou erro. Todos apareceram pelo resultado.
O que fica Lição nove despachos, um resumo
Seis padrões que apareceram em quase todos os despachos
1 Valide pelo resultado, não pelo mecanismo “Succeeded” não é “feito”: as cinco corridas de cron, a descoberta muda, o gerador de posts.
2 Confira contra a fonte O cursor que pulava ids, o Senado invisível, o placar sem ausentes.
3 Meça antes de otimizar EXPLAIN antes do índice, recall antes de cortar dimensão, latência na porta certa.
4 Erro silencioso é o pior erro A lista que virou string, o UPDATE de zero linhas, a busca degradada que respondia 200.
5 Configurar não é ter ISR declarado e desligado, índice que existia e não era usado, cache que ninguém conferiu.
6 Toda economia precisa de um gatilho com dono O detector de jabuti mais barato, o teto de IA, a meta de voltar ao plano grátis.
Recapitule rápido, um padrão por avanço. A ideia é que a plateia leve isso para qualquer projeto, com ou sem IA.
Se tiver de escolher um só: o primeiro. Quase todos os incidentes deste projeto aconteceram com tudo verde no painel. O que pegou cada um foi alguém, ou algum alerta, olhando para o resultado.
Seis padrões que se repetiram em quase todos os capítulos:
validar pelo resultado, não pelo mecanismo; conferir contra a fonte; medir antes de otimizar; tratar o erro silencioso como o pior erro; lembrar que configurar não é ter; e dar a toda economia um gatilho de reversão com um responsável.
Como ler
Avance (→) para revelar um padrão por vez. Os exemplos citados estão nos capítulos anteriores: a visão geral (tecla O) leva direto a eles.
O que fica Transparência registrados em commits, tasks e memória do projeto
O assistente de IA acelerou tudo. E errou com convicção.
436 dos 571 commits foram escritos em par com um assistente de IA (Claude). Alguns erros dele, todos registrados:
“O acervo dobra a cada 13 dias.” Era o backfill terminando.
“Corte para 512 dimensões.” Antes de medir. Perderia 20% do recall.
“Não é robô, é um monitor em loop.” O painel escondia a query string.
“ISR ligado.” Dito em dois commits, sem conferir o cabeçalho.
“É um 409 da API.” O pedido nem saía do banco.
“O cache expulsa o índice da RAM.” A medição mostrou 100% de acerto.
“Recentes primeiro.” Com NULL no topo, nunca funcionou.
Um grep numa lista de variáveis despejou uma chave privada no log local. Regra nova: nunca listar segredos.
IA acelera. A verificação continua sendo sua: todo diagnóstico precisa de um número que o confirme.
Slide importante para estudantes. O assistente escreveu muito código bom e rápido, e também deu diagnósticos errados com total confiança. O padrão dos erros é o mesmo: uma explicação plausível aceita antes de uma medição.
O que funcionou como antídoto: pedir o número. “Mostre o EXPLAIN”, “mostre o cabeçalho”, “compare com a API”. Toda vez que o diagnóstico veio acompanhado de uma medição, ele se sustentou.
436 dos 571 commits do projeto foram escritos em par com um assistente de IA (Claude). Ele acelerou muito o trabalho e também deu diagnósticos errados com total convicção.
O padrão dos erros foi o mesmo: aceitar uma explicação plausível antes de uma medição. O antídoto que funcionou foi pedir o número: o plano da consulta, o cabeçalho da resposta, a comparação com a fonte.
Como ler
Avance (→) para revelar cada erro. Todos estão registrados em commits, tarefas e na memória do projeto.
O que fica Em aberto achados de 06/10, ao preparar esta palestra
Em tramitação: o que achamos preparando esta palestra
A última atividade congelou Uma atualização cai por timeout desde ~20/07, sem alarde. Das 912 proposições novas desde agosto, só 375 entram no ranking.
Dois detectores no papel, um na prática 334 dos 419 selos de jabuti vêm só do primeiro detector, sem a confirmação do segundo.
Cache de prompt configurado, nunca usado Zero leituras de cache em 46 mil chamadas do Haiku. Provável causa: o prefixo é menor que o mínimo do modelo.
Os robôs voltaram Nova onda no /buscar, de outros endereços e com Chrome em versões rotativas. E um robô de SEO com 12 mil acessos por dia nas páginas de proposição.
Um estágio que nunca rodou A canonicalização mensal de temas nunca terminou: perdeu a vez em agosto e falhou em setembro e outubro. A próxima tentativa é em 01/11.
Opcional, e confira os números antes de apresentar: são achados da pesquisa de 06/10, ainda não validados nem corrigidos.
A mensagem: o projeto continua em tramitação. Todos os padrões da palestra reaparecem aqui. A última atividade congelada é um erro silencioso. Os dois detectores são “configurar não é ter”. O cache de prompt também. Os robôs são a fatura voltando.
Problemas encontrados durante a preparação desta palestra, em 06/10, ainda não corrigidos: uma atualização que cai por tempo esgotado e deixa proposições novas fora do ranking; selos de jabuti sem a confirmação do segundo detector; o cache de prompt configurado e nunca usado; uma nova onda de robôs; e um estágio mensal que nunca terminou com sucesso.
Todos repetem os padrões do slide anterior. O projeto continua “em tramitação”.
Situação: em tramitação
Obrigado. Perguntas?
Tudo o que você viu aqui roda num navegador: esta apresentação é um único arquivo HTML, com os dados reais embutidos.
olhonalei.com · 571 commits · 30/04 → 06/10/2026
Abra para perguntas. Perguntas prováveis e onde está a resposta: “quanto custa por mês?” (capítulo 8: num mês calmo, uns 35 dólares de uso da Vercel mais 20 de assinatura, e 40 a 70 de IA; banco e servidor da API ficaram fora das contas desta palestra); “por que não usar só busca semântica?” (capítulo 2: palavra-chave é mais barata e exata para nomes e números); “a IA erra jabuti?” (capítulo 3: sim, precisão estimada de 55%); “dá para usar os dados?” (as APIs de dados abertos são públicas, e o site também).
Tecla O abre a visão geral para voltar direto a qualquer slide.
Fim da parte técnica.
Esta apresentação é um único arquivo HTML: todos os gráficos e simuladores rodam no navegador, com os dados reais embutidos. O site do projeto é olhonalei.com.
Para rever qualquer capítulo, use a visão geral (tecla O), que também traz o glossário completo.
? Entenda