<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Vibe-Coding | AkitaOnRails.com</title><link>https://www.akitaonrails.com/tags/vibe-coding/</link><description>Blog do Fabio Akita do Canal do YouTube 'Akitando' falando sobre tecnologia, carreira e coisas geek.</description><generator>Hugo -- gohugo.io</generator><language>pt-BR</language><lastBuildDate>Thu, 22 Oct 2026 22:06:22 +0000</lastBuildDate><atom:link href="https://www.akitaonrails.com/tags/vibe-coding/index.xml" rel="self" type="application/rss+xml"/><item><title>Engenharia Reversa com IA: a Adobe morreu?</title><link>https://www.akitaonrails.com/2026/10/13/engenharia-reversa-com-ia-a-adobe-morreu/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/10/13/engenharia-reversa-com-ia-a-adobe-morreu/</guid><pubDate>Tue, 13 Oct 2026 00:00:00 -0300</pubDate><description>&lt;p&gt;O PhotoCraft existe. O REA existe. Recomp e decomp de jogo antigo saem todo dia. Logo: a Adobe morreu, nenhum binário é sagrado, e programador acabou.&lt;/p&gt;
&lt;p&gt;Essa foi a semana no X, e se você só lê manchete, é uma conclusão razoável. Este texto é pra quem quer saber o que cada uma dessas coisas faz de verdade.&lt;/p&gt;
&lt;p&gt;Explodiu no dia 9, quando a conta abaixo resumiu o que um desenvolvedor de Atlanta, Brandon Thomas, vinha publicando desde o fim de setembro: sete clones de código aberto dos produtos principais da Adobe, escritos em Rust com ajuda do Claude Opus 5.5. Photoshop, Illustrator, Premiere, Lightroom, After Effects, InDesign, Acrobat. Tudo de graça, tudo feito em &amp;ldquo;sala limpa&amp;rdquo;, sem uma linha de código da Adobe. A conta que viralizou foi esta:&lt;/p&gt;</description><content:encoded><![CDATA[<p>O PhotoCraft existe. O REA existe. Recomp e decomp de jogo antigo saem todo dia. Logo: a Adobe morreu, nenhum binário é sagrado, e programador acabou.</p>
<p>Essa foi a semana no X, e se você só lê manchete, é uma conclusão razoável. Este texto é pra quem quer saber o que cada uma dessas coisas faz de verdade.</p>
<p>Explodiu no dia 9, quando a conta abaixo resumiu o que um desenvolvedor de Atlanta, Brandon Thomas, vinha publicando desde o fim de setembro: sete clones de código aberto dos produtos principais da Adobe, escritos em Rust com ajuda do Claude Opus 5.5. Photoshop, Illustrator, Premiere, Lightroom, After Effects, InDesign, Acrobat. Tudo de graça, tudo feito em &ldquo;sala limpa&rdquo;, sem uma linha de código da Adobe. A conta que viralizou foi esta:</p>
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">AI is now being used to recreate Adobe's software, a developer has released seven free, open-source apps as clean-room alternatives to Adobe's main tools:<br><br>- PhotoCraft → Photoshop<br>- VectorCraft → Illustrator<br>- FilmCraft → Premiere Pro<br>- LightCraft → Lightroom<br>- EffectCraft → After Effects<br>- DesignCraft → InDesign<br>- PrintCraft → Acrobat Pro<br><br>Built in Rust with help from Claude Opus 5.5, they run on Windows, Linux, and macOS under MIT/Apache licenses without using Adobe's source code.</p>&mdash; Pirat_Nation (@Pirat_Nation) <a href="https://twitter.com/Pirat_Nation/status/2108437553615425881">October 9, 2026</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>
<p>Na mesma semana, o <a href="https://github.com/morluto/rea"target="_blank" rel="noopener">REA</a>, &ldquo;Reverse Engineer Anything&rdquo;, passou de 100 mil estrelas no GitHub, ganhando dezenas de milhares por dia. A promessa da página: &ldquo;viu uma feature num app que você quer no seu produto? Peça pro seu agente investigar com o REA e construir uma versão pro seu projeto&rdquo;.</p>
<p>E quem acompanha retrogaming sabe que 2026 é o ano que não para de dar. Tem uma <a href="https://docs.google.com/spreadsheets/d/1MT_xMPlKNbyhANRUMzOH94J3ybeF0b1Ws8F0kb-_598/edit"target="_blank" rel="noopener">planilha comunitária</a> tentando listar os ports não oficiais pra PC, com uns 100 jogos de Ship of Harkinian a Skate 3, e ela está sempre atrasada. O canal <a href="https://www.youtube.com/@VideoGameEsoterica"target="_blank" rel="noopener">Video Game Esoterica</a> publica port novo quase todo dia: Time Crisis decompilado do arcade, Daytona USA, Bloodborne.</p>
<p>A versão de Xbox One do <a href="https://almost-there-games.itch.io/azimuth"target="_blank" rel="noopener">Forza Horizon 2</a>, presa lá desde 2014, roda em Windows. O <a href="https://github.com/boykopovar/AnyPS5"target="_blank" rel="noopener">AnyPS5</a> promete converter executável de PS5 em binário nativo de PC, e emuladores de PS5 como o KytyPS5 e o SharpEmu já <a href="https://www.invenglobal.com/articles/26860/astro-bot-boots-on-pc-as-ps5-emulation-makes-rapid-progress"target="_blank" rel="noopener">rodam Astro Bot</a> com gameplay de verdade. Boa parte disso com IA no meio, alguns abertamente vibe-coded.</p>
<p>Daí o consenso: programar, em geral, acabou. Todo binário é reversível e reutilizável, basta apontar o agente. Minha posição é que ficou muito mais fácil mesmo, que ainda está longe da bala de prata que as pessoas imaginam, e que o motor por trás disso, o LLM, <a href="/2026/10/12/llms-agentes-competente-o-suficiente/">parou de crescer exponencialmente</a>. O resto do texto são os motivos.</p>
<blockquote>
  <p><strong>TL;DR:</strong> engenharia reversa é uma disciplina de 40 anos com ferramenta madura, e a IA acelerou o trabalho braçal dela, com destaque pro retrô, onde o binário está disponível, é minúsculo e o alvo não se move. Os clones da Adobe são alpha com centenas de bugs abertos e print bonito. O REA é uma caixa de ferramentas que exige o binário na mão, Ghidra ou IDA instalados, e muito token. O que vale dinheiro hoje nem binário tem: é SaaS, nuvem, parceria com fabricante e décadas de ciência de cor. Reescrever o Bun, com o autor do projeto e 64 agentes, custou US$ 165 mil. Pule pra <a href="#engenharia-reversa-n%c3%a3o-nasceu-semana-passada">linha do tempo</a>, pro <a href="#o-que-o-rea-faz-de-verdade">REA</a>, pros <a href="#photocraft-print-bonito-alpha-por-dentro">clones da Adobe</a>, pro <a href="#o-caso-bun-quanto-custa-reescrever-software-grande">caso Bun</a>, por que <a href="#por-que-o-retr%c3%b4-anda-t%c3%a3o-r%c3%a1pido">o retrô anda rápido</a> ou pras <a href="#respondendo-a-lista-do-come%c3%a7o">respostas no final</a>.</p>

</blockquote>
<h2>Antes de tudo: eu não gosto da Adobe<span class="hx:absolute hx:-mt-20" id="antes-de-tudo-eu-não-gosto-da-adobe"></span>
    <a href="#antes-de-tudo-eu-n%c3%a3o-gosto-da-adobe" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra ficar claro de onde eu falo. Eu detesto as práticas comerciais da Adobe tanto quanto qualquer um. Fui cliente pagante por anos, usei Photoshop e Premiere extensivamente, mais Lightroom e After Effects, e saí pelos mesmos motivos que todo mundo sai: assinatura compulsória, cancelamento com multa, preço subindo sem contrapartida.</p>
<p>E é exatamente por ter usado a fundo que eu sei o tamanho do que está dentro daquelas caixas. Quem acha que Photoshop é camada, crop e texto nunca precisou de prova de cor pra gráfica, nunca abriu um RAW de câmera cujo perfil de cor a Adobe mantém há vinte anos, nunca fez a conformação de material de cinema, o trabalho de reunir o material bruto da câmera pra finalização. Vou chegar lá, depois do básico.</p>
<h2>Engenharia reversa não nasceu semana passada<span class="hx:absolute hx:-mt-20" id="engenharia-reversa-não-nasceu-semana-passada"></span>
    <a href="#engenharia-reversa-n%c3%a3o-nasceu-semana-passada" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todo emulador que você já usou existe porque alguém abriu hardware, desmontou binário, farejou protocolo e, em alguns casos, quebrou criptografia. A disciplina tem jurisprudência, ferramenta comercial e uma história de décadas. Uma linha do tempo resumida, com os marcos que mais importam:</p>
<ul>
<li><strong>1982 a 1984</strong>: a Compaq reescreve a BIOS do IBM PC por engenharia reversa, e em 1984 a Phoenix vende uma BIOS licenciável feita em sala limpa, com um time que só lia a especificação. Nasce a indústria de PC compatível. <a href="https://en.wikipedia.org/wiki/Phoenix_Technologies"target="_blank" rel="noopener">Fonte</a>.</li>
<li><strong>1992</strong>: Andrew Tridgell cria o Samba farejando pacotes SMB. No mesmo ano, <a href="https://www.copyright.gov/fair-use/summaries/segaenters-accolade-9thcir1992.pdf"target="_blank" rel="noopener">Sega v. Accolade</a> e <a href="https://www.copyright.gov/fair-use/summaries/atari-nintendo-fedcir1992.pdf"target="_blank" rel="noopener">Atari v. Nintendo</a> firmam nos EUA que desmontar código pra chegar em elementos funcionais é <em>fair use</em>, uso justo (a Atari perdeu o caso dela por outro motivo: mentiu pro Copyright Office pra conseguir o fonte).</li>
<li><strong>1993</strong>: começa o Wine, que vira a base do Proton e de todo jogo de Windows que roda no Steam Deck hoje.</li>
<li><strong>1999</strong>: UltraHLE roda jogos comerciais de N64 em velocidade real; o bleem! emula PS1, a Sony processa e perde as liminares, e o bleem! ainda assim fecha em 2001; o DeCSS quebra a proteção do DVD.</li>
<li><strong>2000</strong>: <a href="https://law.resource.org/pub/us/case/reporter/F3/203/203.F3d.596.99-15852.html"target="_blank" rel="noopener">Sony v. Connectix</a> decide que copiar a BIOS do PS1 pra construir um emulador é uso justo.</li>
<li><strong>2002 e 2003</strong>: nascem PCSX2 e Dolphin, os dois emuladores que vinte anos depois ainda são referência.</li>
<li><strong>2007</strong>: o iPhone é aberto onze dias depois do lançamento. A Comissão Europeia obriga a Microsoft a documentar seus protocolos de rede, e a <a href="https://fsfe.org/news/2007/news-20071220-01.html"target="_blank" rel="noopener">equipe do Samba recebe a documentação</a> por 10 mil euros.</li>
<li><strong>2010</strong>: o grupo fail0verflow mostra no 27C3 que a Sony assinou o firmware do PS3 com um nonce fixo, expondo a chave privada (o nonce é o número que deveria ser aleatório em cada assinatura). Dias depois, já em 2011, o geohot publica a chave, a Sony processa e faz acordo em março.</li>
<li><strong>2011 e 2013</strong>: nascem RPCS3 (PS3) e Xenia (Xbox 360), os dois emuladores mais difíceis já feitos, por motivos que vou explicar.</li>
<li><strong>2019</strong>: a NSA libera o <a href="https://github.com/NationalSecurityAgency/ghidra"target="_blank" rel="noopener">Ghidra</a> como código aberto. A decompilação do Super Mario 64 vaza e vira pública, e no ano seguinte aparece o port pra PC.</li>
<li><strong>2021 e 2022</strong>: Ocarina of Time 100% decompilado depois de quase dois anos de trabalho; o Ship of Harkinian vira o port de referência.</li>
<li><strong>2024</strong>: Mr-Wiseguy lança o <a href="https://github.com/Zelda64Recomp/Zelda64Recomp/releases"target="_blank" rel="noopener">Zelda 64: Recompiled</a>, a primeira vitrine da recompilação estática de N64.</li>
<li><strong>2025</strong>: Sonic Unleashed Recompiled traz o mesmo truque pro Xbox 360, convertendo PowerPC pra C++ e shaders pra HLSL.</li>
</ul>
<p>Repare que a fase atual, decomp e recomp em escala, começa em 2019, bem antes de qualquer agente de código. A IA entrou numa linha de montagem que já existia.</p>
<h3>As ferramentas<span class="hx:absolute hx:-mt-20" id="as-ferramentas"></span>
    <a href="#as-ferramentas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O ferramental também é maduro, e boa parte é gratuita:</p>
<ul>
<li><a href="https://hex-rays.com/ida-pro"target="_blank" rel="noopener">IDA Pro</a> (Hex-Rays), o disassembler e decompilador comercial de referência, de mil a quase nove mil euros por ano, com uma versão gratuita limitada a x86.</li>
<li><a href="https://github.com/NationalSecurityAgency/ghidra"target="_blank" rel="noopener">Ghidra</a>, a resposta aberta da NSA, com decompilador e scripting, Apache 2.0.</li>
<li><a href="https://binary.ninja/"target="_blank" rel="noopener">Binary Ninja</a>, com uma edição gratuita e licenças de 200 a 3.500 dólares.</li>
<li><a href="https://github.com/radareorg/radare2"target="_blank" rel="noopener">radare2</a> e o <a href="https://github.com/rizinorg/cutter"target="_blank" rel="noopener">Cutter</a>, interface do rizin, que é um fork do radare2: a linha de comando e a interface abertas.</li>
<li><a href="https://github.com/x64dbg/x64dbg"target="_blank" rel="noopener">x64dbg</a> e <a href="https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/"target="_blank" rel="noopener">WinDbg</a> no Windows, <a href="https://www.sourceware.org/gdb/"target="_blank" rel="noopener">GDB</a> no resto.</li>
<li><a href="https://frida.re/"target="_blank" rel="noopener">Frida</a>, pra injetar código em processo vivo e observar o que ele faz.</li>
<li><a href="https://github.com/skylot/jadx"target="_blank" rel="noopener">JADX</a> pra Android e <a href="https://github.com/icsharpcode/ILSpy"target="_blank" rel="noopener">ILSpy</a> pra .NET.</li>
</ul>
<p>Guarde essa lista. O REA, que a gente vai ver daqui a pouco, é um embrulho em volta dela.</p>
<h2>Como se desmonta um binário, na prática<span class="hx:absolute hx:-mt-20" id="como-se-desmonta-um-binário-na-prática"></span>
    <a href="#como-se-desmonta-um-bin%c3%a1rio-na-pr%c3%a1tica" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Em princípio, todo binário é reversível. O processador precisa conseguir executar aquilo, então as instruções estão lá. O caminho é sempre o mesmo:</p>
<ol>
<li><strong>Desmontar</strong>: o binário vira assembly, uma instrução de máquina por linha. Isso é mecânico.</li>
<li><strong>Decompilar</strong>: o assembly vira algo parecido com C. Ghidra e IDA fazem isso razoavelmente bem, com muito ruído.</li>
<li><strong>Recuperar o que o compilador jogou fora</strong>: nome de variável, nome de função, tipos, estruturas, comentários. Nada disso sobrevive à compilação. Esse é o trabalho de verdade, e é humano.</li>
<li><strong>Refatorar</strong> até virar código que uma pessoa consegue ler, manter e portar.</li>
</ol>
<p>Os projetos de decomp de N64 fazem o passo 4 até o fim: o código em C deles, recompilado, gera byte a byte a ROM original. É assim que o Super Mario 64 virou um projeto com 135 mil linhas de C de código de jogo (uns 100 mil de código efetivo, mais meio milhão de linhas de dados de fase e modelo gerados), e o Ocarina of Time passou de 430 mil linhas. A recompilação estática pula parte disso: traduz as instruções do processador original pra C genérico, sem tentar entender, e troca só as partes que falam com o hardware. É mais rápido e o resultado é menos legível.</p>
<h3>&ldquo;Mas a IA não pode ler o binário direto?&rdquo;<span class="hx:absolute hx:-mt-20" id="mas-a-ia-não-pode-ler-o-binário-direto"></span>
    <a href="#mas-a-ia-n%c3%a3o-pode-ler-o-bin%c3%a1rio-direto" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Alguém sempre pergunta: se é IA, pra que passar por C? Deixa o modelo ler o binário e construir em cima. A resposta curta é que LLM só sabe o que estava no corpus de treino, e corpus de binário pareado com código fonte é escasso, porque nenhum humano escreve assembly de produção há décadas.</p>
<p>A resposta longa é que existe literatura, e ela é instrutiva justamente por ser modesta. A linhagem vai de redes neurais de 2018 a modelos como o <a href="https://arxiv.org/abs/2305.12520"target="_blank" rel="noopener">SLaDe</a> (200 milhões de parâmetros, um por arquitetura e nível de otimização), o <a href="https://arxiv.org/abs/2403.05286"target="_blank" rel="noopener">LLM4Decompile</a> (até 33 bilhões) e o <a href="https://arxiv.org/abs/2311.13721"target="_blank" rel="noopener">Nova</a>. A Meta publicou um <a href="https://arxiv.org/abs/2407.02524"target="_blank" rel="noopener">LLM Compiler</a> que traduz assembly pra IR do LLVM. Os melhores resultados, em funções isoladas de benchmark, giram em torno de 60 a 65% de código que roda de novo e dá o mesmo resultado, caindo forte quando o compilador otimizou com -O3.</p>
<p>Dois estudos recentes mostram o estado real. O <a href="https://arxiv.org/abs/2505.11340"target="_blank" rel="noopener">DecompileBench</a>, com 23 mil funções reais, achou que decompiladores baseados em LLM produzem código mais legível e com metade da correção funcional dos decompiladores comerciais. E um <a href="https://arxiv.org/abs/2609.05370"target="_blank" rel="noopener">trabalho de setembro de 2026</a> mediu que o refinamento por LLM da saída do Ghidra sobe a taxa de compilação de 75% pra 90% e derruba a fidelidade de comportamento de 74% pra 62%, com até uns 10% das funções com vulnerabilidade conhecida deixando de reproduzir a falha, ou seja, o bug sumiu sem ninguém pedir.</p>
<blockquote>
  <p>Resumindo o que existe: modelos pequenos, função por função, quase só x86-64 Linux, e os melhores são camadas de refinamento em cima do Ghidra, o que mantém o decompilador clássico no loop. Nenhum emulador, nenhum jogo, nenhum programa inteiro foi recompilado neuralmente. Enquanto alguém não inventa isso, você trabalha com a realidade, e a realidade inclui a camada intermediária de linguagem de programação e compilador.</p>

</blockquote>
<p>Os assistentes de IA que existem pra isso em produção confirmam o desenho: o <a href="https://plugins.hex-rays.com/aidapal"target="_blank" rel="noopener">aiDAPal</a>, plugin de IDA da Atredis, o Sidekick do Binary Ninja, os servidores MCP pro Ghidra. Todos são um LLM dando nome a variável e explicando função em cima do que o decompilador tradicional produziu.</p>
<h2>O que o REA faz de verdade<span class="hx:absolute hx:-mt-20" id="o-que-o-rea-faz-de-verdade"></span>
    <a href="#o-que-o-rea-faz-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu clonei o <a href="https://github.com/morluto/rea"target="_blank" rel="noopener">REA</a> e li o código. Ele é um servidor MCP e uma CLI em TypeScript que embrulha as ferramentas da lista acima e as expõe pro seu agente de código: Ghidra, Hopper, IDA, JADX, Frida, Wakaru pra JavaScript ofuscado, Playwright pra capturar o que um site faz. O agente pede, o REA roda a ferramenta e devolve pseudocódigo, grafo de chamadas, strings e traces de IPC, cada resultado com um registro de evidência e uma lista de limitações.</p>
<p>Esse fluxo eu já tinha feito na mão, meses antes de o REA viralizar. Em julho eu <a href="/2026/07/08/fable-5-analisar-codigo-tiktok-clash-of-kings-govbr-fingerprint/">coloquei o Fable 5 pra analisar o código do TikTok, do Clash of Kings e do gov.br</a>: baixei os APKs, descompactei (APK é um zip), rodei o JADX nos <code>.dex</code> e deixei o modelo ler o Java que saiu do outro lado. Deu pra ver os endpoints, as permissões, os SDKs embutidos e como cada app monta o fingerprint do aparelho, aquele identificador persistente que sobrevive a limpar dados e reinstalar. O TikTok saiu como coleta de grau industrial, blindada contra auditoria; o gov.br, como exemplo de como fazer direito.</p>
<p>As três lições daquele post valem pro REA inteiro. Android é fácil demais de descompilar, e mesmo assim o resultado serve só pra análise estática, porque recompilar o app fica fora de alcance: os nomes vêm embaralhados pela ofuscação, faltam recursos e chaves de assinatura. A parte mais sensível mora em código nativo, <code>.so</code> em C e C++, que descompila em nada legível, e no TikTok é justamente aí que fica o miolo. E o que saiu daquele trabalho foi um relatório sobre o que o app faz, com ressalvas, e nenhum clone do TikTok. O REA embala esse mesmo fluxo num servidor MCP; as paredes continuam no mesmo lugar.</p>
<p>Quem fornece o julgamento é o LLM que você já paga. O REA fornece o encanamento determinístico e a proveniência. A frase &ldquo;automatiza as heurísticas de engenharia reversa&rdquo;, que circula por aí, não aparece em lugar nenhum do repositório; a descrição que o próprio projeto usa é engenharia reversa dirigida por agente, com evidência rastreável.</p>
<p>O que ele exige: o artefato na sua máquina (binário nativo, app Electron, assembly .NET, APK, firmware, captura de rede), Node 22.19 ou superior, e as ferramentas pesadas instaladas por sua conta. Ghidra é grátis; IDA, Hopper e JEB o README manda você comprar. Não tem chave de API do REA porque ele roda local, e o custo de token cai inteiro na sua assinatura de agente.</p>
<p>E esse custo é o detalhe que ninguém cita. O catálogo de ferramentas MCP do REA tem 2,3 MB de JSON, que entra no contexto do agente. O próprio projeto publica um <a href="https://github.com/morluto/rea/tree/main/experiments/analysis-efficiency"target="_blank" rel="noopener">experimento</a> em que analisar o SQLite compilado em WASM, no caminho padrão, levou 42 chamadas de ferramenta, cinco minutos e 300 mil tokens de entrada. Um inventário completo de um app Electron como o Obsidian devolve quase 1 MB de JSON.</p>
<p>Tem issue aberta reclamando que inspecionar um app de Mac comum devolve de 80 a 230 mil tokens de uma vez.</p>
<p>O projeto é sério e vai longe: seis meses, 2.600 commits, mil arquivos de teste, versão 6.4 lançada em 11 de outubro. Também é instável: 190 issues abertas, várias mudanças incompatíveis a cada versão menor, subsistemas marcados como experimentais.</p>
<p>Os limites estão escritos no próprio repositório. O CONTRIBUTING exclui quebra de DRM, anticheat, licença e proteção comercial. O DISCLAIMER avisa que decompilação, reconstrução e análise assistida por IA &ldquo;podem ser incompletas ou incorretas&rdquo;. E até as instruções internas pra gerar as figuras do site dizem, com todas as letras: &ldquo;nunca implique que o REA clona um programa automaticamente&rdquo; e &ldquo;nunca diga reconstrução completa, clone exato ou recuperação automática de código fonte&rdquo;.</p>
<blockquote>
  <p>O REA é uma boa caixa de ferramentas pra quem já sabe fazer engenharia reversa e quer um agente segurando o Ghidra. Ele não é o botão &ldquo;copia esse app pra mim&rdquo;. Quem escreveu o projeto é o primeiro a dizer isso.</p>

</blockquote>
<h2>PhotoCraft: print bonito, alpha por dentro<span class="hx:absolute hx:-mt-20" id="photocraft-print-bonito-alpha-por-dentro"></span>
    <a href="#photocraft-print-bonito-alpha-por-dentro" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O <a href="https://github.com/storytold/photocraft"target="_blank" rel="noopener">repositório do PhotoCraft</a> foi criado em 30 de setembro. Em duas semanas, 54 mil estrelas, 8 versões lançadas e, no momento em que escrevo, mais de 800 issues abertas. O selo no README diz &ldquo;early alpha&rdquo;, e o próprio Brandon Thomas, citado pela Gizmodo, disse que não está nem perto de pronto. A imprensa que testou chegou na mesma conclusão: a Gizmodo chamou de &ldquo;Photoshop cheio de bug, mas real&rdquo;, e o OMG Ubuntu de &ldquo;fac-símile superficial com falhas pronunciadas&rdquo;.</p>
<p>As issues contam a história técnica:</p>
<ul>
<li>Qualquer imagem acima de 8.192 pixels sai do compositor de GPU e cai pra CPU, com pico de 6 GB de memória, porque o limite de textura do egui-wgpu é esse (<a href="https://github.com/storytold/photocraft/issues/49"target="_blank" rel="noopener">issue 49</a>, já fechada, e as que vieram depois, <a href="https://github.com/storytold/photocraft/issues/1015"target="_blank" rel="noopener">1015</a> e <a href="https://github.com/storytold/photocraft/issues/4017"target="_blank" rel="noopener">4017</a>).</li>
<li>Uma foto de 12 megapixels precisa de quase 800 MB só pra abrir, ajustar e exportar pela linha de comando (<a href="https://github.com/storytold/photocraft/issues/1637"target="_blank" rel="noopener">issue 1637</a>).</li>
<li>O gerenciamento de cor é rudimentar: espaço de trabalho só global, em vez de por documento, e sem transformação de visualização (<a href="https://github.com/storytold/photocraft/issues/321"target="_blank" rel="noopener">issue 321</a>). Dezoito issues mencionam CMYK, quinze mencionam perfil ICC.</li>
<li>Quase cem issues abertas mencionam PSD. Camadas de matiz e saturação compõem diferente do Photoshop, e salvar por cima sobrescreve a imagem mesclada correta do arquivo original (<a href="https://github.com/storytold/photocraft/issues/4089"target="_blank" rel="noopener">issue 4089</a>).</li>
<li>Cinquenta issues abertas mencionam &ldquo;crash&rdquo;.</li>
</ul>
<p>A interface lembra o Photoshop, então o print é compartilhável, e quem vê o print assume que é cópia carbono. Isso é esperado numa alpha de duas semanas. Com interesse, tempo e contribuição, parte disso vai sendo resolvida. E aí os contribuidores vão descobrir uma coisa que os projetos de N64 nunca enfrentaram.</p>
<blockquote>
  <p>O PhotoCraft hoje é aquela réplica de Ferrari montada em cima de um Fusca: a carroceria tem a forma certa, você tira uma foto e a maioria das pessoas diz &ldquo;uau, uma Ferrari&rdquo;. Aí você liga e sai o motor de Fusca. Se você se empolgou com a foto, o problema foi seu.</p>

</blockquote>
<h2>O alvo se move<span class="hx:absolute hx:-mt-20" id="o-alvo-se-move"></span>
    <a href="#o-alvo-se-move" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Decompilar o Zelda de 1998 é mirar num alvo parado. A Nintendo não vai lançar o Ocarina of Time 2026 com formato de arquivo novo. Copiar um produto vivo é outra conversa, e a gente já viu esse filme.</p>
<p>Nos anos 90 e 2000 o alvo era o Microsoft Office. O StarOffice virou OpenOffice em 2000 e LibreOffice em 2010, e todos eles passaram anos correndo atrás de um formato binário, o .doc, que mudava de versão pra versão e só ganhou especificação pública em 2008, quando o jogo já tinha acabado. A parte comprovada dessa história está nos processos: a Comissão Europeia multou a Microsoft em <a href="https://ec.europa.eu/commission/presscorner/detail/en/ip_04_382"target="_blank" rel="noopener">497 milhões de euros em 2004</a> por recusar informação de interoperabilidade, e em <a href="http://news.bbc.co.uk/2/hi/business/7266629.stm"target="_blank" rel="noopener">899 milhões em 2008</a> por descumprir a ordem. O .docx só virou padrão ISO em 2008, numa <a href="https://en.wikipedia.org/wiki/Standardization_of_Office_Open_XML"target="_blank" rel="noopener">votação tão controversa</a> que países protestaram formalmente.</p>
<p>Levou mais de dez anos. E quando o formato finalmente abriu, o Office já não importava do mesmo jeito. O valor mudou de lugar: hoje são <a href="https://office365itpros.com/2024/01/31/office-365-reaches-400-million/"target="_blank" rel="noopener">400 milhões de assentos pagos de Office 365</a>, <a href="https://www.googlecloudpresscorner.com/2025-12-04-Google-Workspace-Study-Reveals-More-Than-90-of-Rising-Leaders-Want-AI-With-Personalization"target="_blank" rel="noopener">3 bilhões de usuários de Google Workspace</a> e uns <a href="https://prodesigntools.com/number-of-creative-cloud-subscribers.html"target="_blank" rel="noopener">41 milhões de assinantes de Creative Cloud</a>. Ninguém quer instalar aquilo no desktop, e por isso ninguém tem o binário.</p>
<blockquote>
  <p>Fazer engenharia reversa do Google Docs é reescrever o produto do zero olhando só pra interface, porque o código que importa nunca sai do servidor. Depois que o mundo migrou pra SaaS, a maior parte do software proprietário virou jardim murado sem binário. Sem binário, não tem o que desmontar.</p>

</blockquote>
<p>Dá pra fazer versões disso, claro. O <a href="https://immich.app/"target="_blank" rel="noopener">Immich</a> substitui o Google Photos pra uma pessoa ou uma família: backup do celular, reconhecimento de rosto, busca semântica, tudo no seu servidor. O que ele não replica é a infraestrutura e o corpus de treino que fazem a busca do Google ser a busca do Google. E a Adobe fez a mesma migração, em pedaços.</p>
<h2>Photoshop é muito mais que camada e crop<span class="hx:absolute hx:-mt-20" id="photoshop-é-muito-mais-que-camada-e-crop"></span>
    <a href="#photoshop-%c3%a9-muito-mais-que-camada-e-crop" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui entra a parte que eu conheço por ter pagado. As features que vendem assinatura hoje nem rodam no seu computador.</p>
<p>O Generative Fill e o Generative Expand rodam no Firefly, na nuvem, e consomem <a href="https://helpx.adobe.com/creative-cloud/apps/generative-ai/generative-credits-faq.html"target="_blank" rel="noopener">créditos generativos</a>. Vários Neural Filters, segundo a <a href="https://helpx.adobe.com/photoshop/using/neural-filters.html"target="_blank" rel="noopener">própria documentação</a>, precisam de internet porque o processamento é no servidor da Adobe. A seleção de objeto por IA tem, desde 2020, uma opção de processar na nuvem pra refinar o resultado. Isso tudo é código que não existe na sua máquina pra ninguém desmontar.</p>
<p>Depois vem o que está no binário, mas levou décadas e parceria pra existir. O Camera Raw carrega <a href="https://helpx.adobe.com/camera-raw/desktop/dng-and-file-formats/supported-lenses.html"target="_blank" rel="noopener">perfis de correção de lente</a> e perfis de cor por câmera pra centenas de modelos, construídos ao longo de vinte anos e atualizados a cada lançamento. Clone nenhum adivinha isso. Ou tem o dado, ou não tem.</p>
<p>E tem a ciência de cor. O exemplo que eu sempre lembro é o GIMP e o CMYK.</p>
<p>O GIMP nasceu em 1995. O primeiro suporte nativo a arquivo CMYK chegou em <a href="https://www.gimp.org/news/2022/08/27/gimp-2-99-12-released/"target="_blank" rel="noopener">2022</a>, e só de importação e exportação, com o pixel sempre em RGB. O <a href="https://www.cgchannel.com/2025/03/gimp-3-0-is-out/"target="_blank" rel="noopener">GIMP 3.0, de 2025</a>, ainda sem modo de edição em CMYK. Está no <a href="https://developer.gimp.org/core/roadmap/"target="_blank" rel="noopener">roadmap</a> como trabalho em andamento.</p>
<p>Trinta anos de projeto aberto, com gente boa e motivada, e a gráfica ainda pede o Photoshop. Isso é o tamanho do buraco da ciência de cor, e nenhum agente de código inventa ciência de cor: ele reproduz o que está no treino.</p>
<p>Por que demorou tanto, se era a feature mais pedida? Porque o núcleo do GIMP nasceu com RGB de 8 bits por canal embutido em cada camada, modo de mesclagem e filtro. Suportar outro modelo de cor exigiu reescrever o motor de pixel inteiro, a migração pro GEGL, que levou a maior parte de uma década e só ficou pronta no <a href="https://www.gimp.org/release-notes/gimp-2.10.html"target="_blank" rel="noopener">GIMP 2.10, em 2018</a>. O CMYK só pôde começar depois disso.</p>
<p>E CMYK é uma família de perfis de dispositivo, com quatro tintas, perfil ICC por impressora, limite de tinta e regra de geração de preto, e editar nele significa que toda a cadeia precisa calcular num modelo subtrativo de quatro canais e ir e voltar pelos perfis sem desviar a cor. Um desenvolvedor sozinho, por melhor programador que seja, sem experiência nessa área específica, vai sofrer pra implementar isso, porque o conhecimento está na gráfica e no laboratório de cor, e quase nada dele está em corpus de treino. No GIMP, quem levou a coisa adiante foram contribuidores que entendem de impressão, e mesmo assim foram anos. Ferramenta nova nenhuma atalha esse tipo de conhecimento.</p>
<p>E tem a camada que nem é técnica: licença comercial. Cor de gráfica é Pantone, e Pantone é uma biblioteca proprietária. Projeto aberto nenhum pode embarcar as paletas; os desenvolvedores do Inkscape <a href="https://lists.inkscape.org/hyperkitty/list/inkscape-devel@lists.inkscape.org/message/DOXDMUAPHORC4HEXPF37LYBQH6SSQ2M5"target="_blank" rel="noopener">chegaram nessa conclusão em 2005</a>, e a <a href="https://www.linux.com/news/pantone-and-free-software/"target="_blank" rel="noopener">Linux.com explicou o risco jurídico</a> na mesma época. A própria Adobe sentiu isso em 2022: quando a Pantone mudou o modelo de negócio, as bibliotecas <a href="https://petapixel.com/2022/10/28/you-have-to-pay-a-subscription-to-use-pantone-colors-in-photoshop-now"target="_blank" rel="noopener">saíram do Photoshop, do Illustrator e do InDesign</a>, arquivos antigos passaram a abrir com as cores Pantone trocadas por preto, e quem precisa delas paga uma assinatura separada de plugin. Se nem a Adobe, pagando, escapou, imagine um clone que vive de boa vontade.</p>
<h3>Vídeo é pior<span class="hx:absolute hx:-mt-20" id="vídeo-é-pior"></span>
    <a href="#v%c3%addeo-%c3%a9-pior" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>No Premiere, no After Effects e no DaVinci Resolve, o problema multiplica. Câmera de cinema não entrega vídeo, entrega dado bruto numa curva logarítmica própria: LogC4 da ARRI, IPP2 da RED. Pra transformar aquilo em imagem, você precisa das transformadas do fabricante, e a RED entrega as dela num <a href="https://community.acescentral.com/t/red-ipp2-color-workflows/1055"target="_blank" rel="noopener">SDK</a> que os editores integram e atualizam a cada versão. O ACES, o padrão neutro da indústria, depende de transformadas de entrada fornecidas por cada fabricante.</p>
<p>A Blackmagic dá o Resolve de graça e ele decodifica ARRIRAW e R3D. Faz isso porque paga o SDK e a parceria por você. Um clone feito por agente, &ldquo;adivinhando&rdquo; como a imagem da Alexa deve ficar, produz uma imagem que não bate com o monitor de referência, e no cinema isso significa que não serve. Esse conhecimento não está em corpus de treino nenhum: está em contrato.</p>
<h2>O caso Bun: quanto custa reescrever software grande<span class="hx:absolute hx:-mt-20" id="o-caso-bun-quanto-custa-reescrever-software-grande"></span>
    <a href="#o-caso-bun-quanto-custa-reescrever-software-grande" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra ter noção de ordem de grandeza, o melhor dado público que existe é o do Bun. Em maio de 2026, Jarred Sumner reescreveu o runtime inteiro de Zig pra Rust usando agentes, e em julho <a href="https://bun.com/blog/bun-in-rust"target="_blank" rel="noopener">publicou os números</a>:</p>
<ul>
<li>535 mil linhas de Zig em 1.448 arquivos viraram um diff de um milhão de linhas de Rust, em 11 dias.</li>
<li>Até 64 instâncias de Claude em paralelo, 24 horas por dia, cada linha revisada por dois agentes adversariais e um corretor.</li>
<li>5,9 bilhões de tokens de entrada não cacheados, 690 milhões de saída, 72 bilhões de leituras de cache: <strong>uns US$ 165 mil</strong> a preço de API.</li>
<li>16 mil erros de compilador corrigidos por 64 agentes antes do CI ficar verde em seis plataformas.</li>
<li>A suíte de testes existente, com mais de um milhão de asserções, passou 100% antes do merge.</li>
</ul>
<p>Lê de novo o que viabilizou isso:</p>
<ul>
<li>o autor original do projeto pilotando</li>
<li>um código fonte completo e funcional como referência</li>
<li>uma suíte de testes de um milhão de asserções servindo de oráculo</li>
<li>orçamento ilimitado, porque o Bun pertence à Anthropic desde dezembro de 2025, e uma versão pré-lançamento do Claude Fable 5</li>
</ul>
<p>O Sumner estima que três engenheiros com contexto total levariam um ano.</p>
<p>Agora tira cada uma dessas vantagens. O PhotoCraft não tem o fonte do Photoshop pra portar, tem que inventar. Não tem suíte de testes que diga &ldquo;isso aqui bate com o Photoshop&rdquo;. Não tem o autor do Photoshop.</p>
<p>E o Photoshop é um produto de 36 anos, cujos requisitos de disco a Adobe lista entre <a href="https://helpx.adobe.com/photoshop/desktop/get-started/technical-requirements-installation/adobe-photoshop-on-desktop-technical-requirements.html"target="_blank" rel="noopener">10 e 100 GB</a>, contra um runtime de JavaScript. Mesmo sem saber o tamanho do código da Adobe, estamos falando de um alvo maior, sem referência, sem oráculo e sem dono. Ordens de grandeza acima de US$ 165 mil, e ainda assim seria uma aproximação.</p>
<h2>Por que o retrô anda tão rápido<span class="hx:absolute hx:-mt-20" id="por-que-o-retrô-anda-tão-rápido"></span>
    <a href="#por-que-o-retr%c3%b4-anda-t%c3%a3o-r%c3%a1pido" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se é tão difícil, por que sai port de jogo antigo todo dia? Porque o retrô acumula todas as vantagens que o resto não tem.</p>
<p><strong>O binário está disponível e é minúsculo.</strong> Super Mario Bros. do NES: 40 KB. Super Mario 64: 8 MB. Ocarina of Time e Majora&rsquo;s Mask: 32 MB cada. Um CD inteiro de PS1: 650 MB.</p>
<p>Compare com <a href="https://store.steampowered.com/app/1086940/Baldurs_Gate_3/"target="_blank" rel="noopener">Baldur&rsquo;s Gate 3</a>, 150 GB, ou com o <a href="https://support.activision.com/black-ops-6/articles/pc-system-requirements-for-black-ops-6"target="_blank" rel="noopener">Call of Duty mais recente</a>, acima de 100 GB. São três ou quatro ordens de grandeza, e o escopo encolhe junto: um jogo de 1996 tem um punhado de sistemas, não um motor de física com rede, loja e anticheat.</p>
<p><strong>A tecnologia é antiga e documentada.</strong> Vinte e cinco anos de comunidade já mapearam o hardware do N64, do PS1 e do SNES instrução por instrução. O agente não descobre nada; ele acelera um trabalho com mapa pronto. É o mesmo padrão que eu <a href="/2026/09/28/llms-sao-boas-em-reproduzir-o-que-existe-e-coisas-novas/">vi nos meus projetos de retrocomputação</a>: onde tem referência e oráculo, a IA voa; onde não tem, ela gira em força bruta.</p>
<p><strong>O alvo não se move.</strong> Ninguém vai atualizar o Zelda de 1998.</p>
<p><strong>E o jurídico está resolvido há trinta anos.</strong> Sega v. Accolade e Sony v. Connectix deram o contorno. Os ports exigem que você forneça a sua ROM.</p>
<h3>Console moderno é um PC<span class="hx:absolute hx:-mt-20" id="console-moderno-é-um-pc"></span>
    <a href="#console-moderno-%c3%a9-um-pc" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A segunda metade da explosão, os ports de PS4, PS5, Xbox One e Switch, tem outra explicação: esses consoles são PCs ou celulares com outro invólucro. O <a href="https://www.playstation.com/en-us/ps4/tech-specs/"target="_blank" rel="noopener">PS4</a> usa um AMD Jaguar x86-64, o <a href="https://blog.playstation.com/2020/03/18/unveiling-new-details-of-playstation-5-hardware-technical-specs/"target="_blank" rel="noopener">PS5</a> um Zen 2, o <a href="https://news.xbox.com/en-us/2020/03/16/xbox-series-x-tech/"target="_blank" rel="noopener">Xbox Series</a> outro Zen 2, o Switch um <a href="https://www.techinsights.com/blog/nintendo-switch-teardown"target="_blank" rel="noopener">Tegra X1</a> ARM da NVIDIA. O processador do seu PC roda aquele código nativamente. Então ninguém precisa emular a CPU.</p>
<p>O trabalho que sobra é traduzir o sistema operacional e a GPU. O <a href="https://github.com/shadps4-emu/shadPS4"target="_blank" rel="noopener">shadPS4</a> reimplementa as bibliotecas do sistema do PS4 e recompila os shaders: os jogos saem da fábrica com código de máquina da GPU AMD, que o emulador decodifica instrução a instrução e converte pra SPIR-V, o formato do Vulkan.</p>
<p>O <a href="https://almost-there-games.itch.io/azimuth"target="_blank" rel="noopener">Azimuth</a>, que faz o Forza Horizon 2 rodar no Windows, traduz chamadas WinRT do Xbox pra Win32 e DirectX 11.X pra DirectX 12 desktop, com o código x86 do jogo rodando direto. O AnyPS5 religa o executável: pega o binário do PS5 e gera um binário nativo de Linux ou Windows, ligado a bibliotecas de sistema reimplementadas. Nos três casos, Proton, DXVK, VKD3D e MoltenVK já tinham resolvido a camada de tradução gráfica anos antes.</p>
<p>A prova inversa são os consoles que não eram PC. O PS3 usa o Cell, um PowerPC com sete coprocessadores ativos (os SPEs), cada um com memória local própria e sem cache, e o RPCS3 está nisso <a href="https://rpcs3.net/about"target="_blank" rel="noopener">desde 2011</a>, com a emulação dos SPUs sendo gargalo até hoje. O Xbox 360 é PowerPC também, e por isso o Sonic Unleashed precisou do XenonRecomp traduzir PowerPC pra C++. Engenharia reversa é caso a caso, e a arquitetura decide se o caso é fácil.</p>
<h3>A IA está no meio, e já tem briga<span class="hx:absolute hx:-mt-20" id="a-ia-está-no-meio-e-já-tem-briga"></span>
    <a href="#a-ia-est%c3%a1-no-meio-e-j%c3%a1-tem-briga" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Que os ports de 2026 usam IA, boa parte deles diz na cara. O <a href="https://github.com/SlickAmogus/silent-hill-decomp"target="_blank" rel="noopener">decomp do Silent Hill</a> escreve no README que o desenvolvimento é &ldquo;fortemente assistido por IA (Claude Opus 4.6, 4.7 e o Fable mais novo), e somos abertos sobre isso. Não é tradução automatizada do jogo&rdquo;. O recomp do Pokémon Stadium <a href="https://retrododo.com/a-pokemon-stadium-recomp-has-finally-arrived-but-lists-claude-as-a-contributor/"target="_blank" rel="noopener">lista o Claude como contribuidor</a>.</p>
<p>Os devs dos recomps de Zelda e Banjo, que passaram anos fazendo isso à mão, <a href="https://kotaku.com/zelda-and-banjo-recompiled-devs-go-to-war-with-ai-vibe-coded-slopcomps-2000739936"target="_blank" rel="noopener">entraram em guerra</a> com o que chamam de &ldquo;slopcomps&rdquo;, ports gerados por agente com qualidade de alpha.</p>
<p>E vale separar manchete de estado real também aqui. O AnyPS5 tem uma versão 0.1 de setembro que, segundo quem testou, roda um jogo indie 2D; a alegação viral de que &ldquo;o PS5 está 80% portado pra PC&rdquo; é dos fãs, e nenhum jogo 3D roda. Astro Bot roda, só que em emuladores (KytyPS5, SharpEmu), que fazem o caminho tradicional. O Forza Horizon 2 roda, e o Azimuth é um binário fechado de um estúdio que se declara &ldquo;AI assisted&rdquo; sem dizer mais nada.</p>
<h2>&ldquo;Mas daqui a dois anos o token custa nada&rdquo;<span class="hx:absolute hx:-mt-20" id="mas-daqui-a-dois-anos-o-token-custa-nada"></span>
    <a href="#mas-daqui-a-dois-anos-o-token-custa-nada" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A resposta padrão pra tudo isso é: hoje é difícil, em dois anos é trivial, porque IA cresce exponencialmente. Os números dizem outra coisa.</p>
<p>O preço da ponta mais cara caiu pouco. O GPT-4 de março de 2023 custava US$ 30 e 60 por milhão de tokens de entrada e saída; o Claude 3 Opus de 2024, 15 e 75; os modelos de ponta de hoje, Fable 5.1 e GPT-6 Astra, <a href="https://claude.com/pricing"target="_blank" rel="noopener">custam</a> <a href="https://developers.openai.com/api/docs/pricing"target="_blank" rel="noopener">10 e 50</a>. Em três anos e meio, a fronteira ficou entre 1,2 e 3 vezes mais barata, dependendo de qual ponta você olha.</p>
<p>O que caiu de verdade foi o custo de uma qualidade fixa: a Epoch AI mediu quedas <a href="https://epoch.ai/blog/llm-inference-price-trends"target="_blank" rel="noopener">de 9 a 900 vezes por ano</a> dependendo da tarefa, e numa <a href="https://epoch.ai/gradient-updates/how-persistent-is-the-inference-cost-burden"target="_blank" rel="noopener">revisão de 2026</a> resume em &ldquo;muito grosseiramente 5 a 10 vezes por ano&rdquo;, com o aviso de que pode desacelerar. Isso é real e é ótimo.</p>
<p>Só que aplica a conta no Bun. US$ 165 mil divididos por 10 dá US$ 16 mil. Por 100, US$ 1.650. Isso é pra portar um runtime com fonte, testes e autor na mão.</p>
<p>Pra um amador tentando clonar o Photoshop sem nada disso, a conta começa ordens de grandeza acima e cai na mesma proporção. Barato não é o mesmo que trivial.</p>
<p>E a premissa do &ldquo;exponencial&rdquo; morreu em 2024. O Ilya Sutskever disse no NeurIPS que <a href="https://www.globalbankingandfinance.com/ai-with-reasoning-power-will-be-less-predictable-ilya-sutskever-says/"target="_blank" rel="noopener">o pré-treino como conhecemos vai acabar</a>, porque só existe uma internet. A Epoch projeta que o escalonamento dos modelos de raciocínio <a href="https://epoch.ai/gradient-updates/how-far-can-reasoning-models-scale"target="_blank" rel="noopener">converge pra taxa geral da fronteira</a> em cerca de um ano.</p>
<p>Eu já escrevi sobre isso três vezes nas últimas semanas: <a href="/2026/09/28/llms-sao-boas-em-reproduzir-o-que-existe-e-coisas-novas/">os limites</a> do que o modelo reproduz, <a href="/2026/10/12/llms-agentes-competente-o-suficiente/">a convergência</a> dos modelos crus com o ganho vindo do harness, e <a href="/2026/10/11/existem-linguagens-melhores-pra-programar-com-llms/">o benchmark de linguagens</a> em que os modelos de fronteira empataram no teto. A ferramenta que temos é a ferramenta que vamos ter por um bom tempo, com melhoria incremental. Planeje com ela.</p>
<p>Vale lembrar o número que abre o texto dos limites: pelo <a href="https://github.blog/news-insights/octoverse/octoverse-a-new-developer-joins-github-every-second-as-ai-leads-typescript-to-1/"target="_blank" rel="noopener">Octoverse 2025</a>, relatório oficial do GitHub, 81,5% das contribuições na plataforma aconteceram em repositórios privados. É o código que importa de verdade, o que paga salário, e nenhum LLM viu uma linha dele. O modelo reproduz o que estava no treino, e esse código nunca esteve lá.</p>
<p>Agora estenda isso pra todo produto SaaS e pra cada componente que ele usa. O código da Adobe que realmente vale, o motor de cor, os perfis, os filtros na nuvem, não está disponível em lugar nenhum, nem como binário. Quem quiser reproduzir vai ter que refazer a pesquisa e o desenvolvimento, caro e demorado, pelos mesmos anos que a Adobe levou. E no fim ainda esbarra na propriedade intelectual licenciada de terceiros, Pantone, SDK de câmera, perfil de fabricante, que você nunca vai conseguir ter.</p>
<h2>Respondendo a lista do começo<span class="hx:absolute hx:-mt-20" id="respondendo-a-lista-do-começo"></span>
    <a href="#respondendo-a-lista-do-come%c3%a7o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><strong>O PhotoCraft existe?</strong> Existe, é uma alpha de duas semanas com mais de 800 issues abertas, gerenciamento de cor rudimentar, travando acima de 8K e abrindo PSD errado. Parece Photoshop no print. Pode melhorar muito, e vai descobrir que o alvo se move e que as partes caras do Photoshop estão na nuvem ou em contrato com fabricante.</p>
<p><strong>O REA existe?</strong> Existe, é bom, e é uma caixa de ferramentas pra quem já sabe usar Ghidra, exige o binário na mão, as ferramentas instaladas e centenas de milhares de tokens por análise. O próprio projeto evita dizer que ele clona programa.</p>
<p><strong>Recomp e decomp aceleraram?</strong> Sim, de verdade, e a IA tem parte nisso. Aceleraram onde o binário existe, é pequeno, está documentado há 25 anos e não se move. Ou onde o console é um PC disfarçado e o trabalho é tradução de API, resolvida pelo Wine e pelo Vulkan anos antes.</p>
<p><strong>A Adobe morreu?</strong> A Adobe tem uns 40 milhões de assinantes, pela estimativa mais citada, as features que vendem assinatura rodam no servidor dela, e trinta anos de GIMP ainda deixam a gráfica pedindo Photoshop. Eu queria que fosse diferente. Os dados dizem que o que vai acontecer é o que sempre aconteceu com clone de produto vivo: anos correndo atrás, e quando alcança, o valor já mudou de lugar.</p>
<p><strong>Programador morreu?</strong> Reescrever o Bun precisou do autor do Bun. Os decomps que funcionam têm gente que conhece o hardware pilotando o agente, e os que não têm viraram &ldquo;slopcomp&rdquo; com nome e tudo. A engenharia reversa ficou mais barata e mais acessível, como ficou com o Ghidra em 2019, e continua sendo trabalho de engenheiro. Sem nós, a ferramenta devolve print bonito.</p>
<p>Software proprietário com binário na mão vai sofrer pressão, e deve. A maior parte do software que você usa não tem binário nenhum, e o que tem já depende de serviço na nuvem que não desmonta. Engenharia de software com IA tem um caminho ótimo pela frente. Manchete de &ldquo;acabou&rdquo; rende clique e não compila.</p>
]]></content:encoded><category>inteligencia-artificial</category><category>vibe-coding</category><category>retrocomputacao</category><category>open-source</category></item><item><title>Parem de inventar desculpas e façam mais deploy! Com a IA, a premissa mudou. Entendam.</title><link>https://www.akitaonrails.com/2026/09/22/parem-de-inventar-desculpas-e-facam-mais-deploy-a-premissa-mudou/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/09/22/parem-de-inventar-desculpas-e-facam-mais-deploy-a-premissa-mudou/</guid><pubDate>Tue, 22 Sep 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;Hoje eu soltei uma sequência de posts no X batendo nessa tecla e, como sempre que eu falo de deploy rápido, um bocado de gente leu &amp;ldquo;esse cara está dizendo pra subir qualquer porcaria em produção sem testar&amp;rdquo;. Não é isso. Deixa eu explicar com calma, porque o argumento é mais estreito e mais chato do que a versão que espalha pânico.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;É melhor deployar mais rápido, porque corrigir depois ficou rápido e barato.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Hoje eu soltei uma sequência de posts no X batendo nessa tecla e, como sempre que eu falo de deploy rápido, um bocado de gente leu &ldquo;esse cara está dizendo pra subir qualquer porcaria em produção sem testar&rdquo;. Não é isso. Deixa eu explicar com calma, porque o argumento é mais estreito e mais chato do que a versão que espalha pânico.</p>
<blockquote>
  <p>É melhor deployar mais rápido, porque corrigir depois ficou rápido e barato.</p>

</blockquote>
<p>Essa frase é o artigo inteiro resumido. O resto é só eu justificando por que ela é verdade agora, mesmo não sendo verdade há alguns anos, e mostrando como eu mesmo ajo em cima dela desde fevereiro.</p>
<h2>A premissa mudou, e a maioria não percebeu<span class="hx:absolute hx:-mt-20" id="a-premissa-mudou-e-a-maioria-não-percebeu"></span>
    <a href="#a-premissa-mudou-e-a-maioria-n%c3%a3o-percebeu" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Comecei a falar isso <a href="https://x.com/AkitaOnRails/status/2102436061083267441"target="_blank" rel="noopener">no X</a> assim:</p>
<blockquote>
  <p>Corrigir ficou barato. Não segurem coisas mais. Só sobe e corrige depois. &ldquo;Ain mas corro o risco de subir com bug&rdquo;. A premissa errada é achar que vc ficar olhando linha a linha vai fazer qualquer diferença num volume grande. E código parado não melhora sozinho. Só código usado dá pra descobrir se precisa melhorar.</p>

</blockquote>
<p>E completei <a href="https://x.com/AkitaOnRails/status/2102476036709450066"target="_blank" rel="noopener">no segundo post</a>, porque a primeira leitura de qualquer coisa que eu escrevo em 280 caracteres sempre vem raivosa e literal demais:</p>
<blockquote>
  <p>Entendam o conceito (é post de X, não de blog, parem de ler literalmente). Não estou dizendo que IA não gera bugs. Gera, óbvio. Mas a esta altura do campeonato, não mais do que você/humano gera também. [&hellip;] Antigamente a desculpa era que quando sobe em produção com bug, ele fica lá mais tempo do que deveria, porque nunca vira prioridade na sprint pra resolver. Agora NÃO TEM MAIS ISSO, a premissa mudou: corrigir o bug depois é RÁPIDO e BARATO.</p>

</blockquote>
<p>Esse é o ponto inteiro. Não é sobre IA ser infalível. É sobre o custo de corrigir um erro ter despencado, e a maioria dos times ainda estar operando o processo de revisão como se esse custo continuasse alto.</p>
<p>Com os modelos de fronteira de hoje, Opus, Sol, K3, e companhia, o LLM erra menos do que a maioria dos desenvolvedores humanos que eu já vi passar por um code review. Quando erra, na esmagadora maioria das vezes o erro não é do modelo. É de quem dirigiu o harness. O LLM faz o que você pede, e não faz o que você não pediu.</p>
<p>Se você não sabe o que pedir, não vai produzir nada útil, não importa quão bom seja o modelo. É exatamente por isso que só Engenheiro de Software de verdade consegue tirar software de produção de verdade de um agente: alguém tem que saber decompor o problema, validar o resultado, e reconhecer quando o pedido em si estava mal formulado. IA não substitui esse julgamento. Ela multiplica o que esse julgamento já sabia fazer.</p>
<h2>Isso já era pra ser assim há 25 anos<span class="hx:absolute hx:-mt-20" id="isso-já-era-pra-ser-assim-há-25-anos"></span>
    <a href="#isso-j%c3%a1-era-pra-ser-assim-h%c3%a1-25-anos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O incômodo que eu sinto com o processo de review burocrático não é novo, e a indústria já devia ter resolvido isso há muito tempo. Kent Beck e Ron Jeffries inventaram Extreme Programming em 1997, no projeto Chrysler C3, e Beck publicou o livro em 1999 com integração contínua, TDD e entrega em lotes pequenos como práticas centrais. Em fevereiro de 2001, dezessete desenvolvedores se trancaram num resort em Snowbird, Utah, e saíram de lá com o Manifesto Ágil. Isso faz vinte e cinco anos.</p>
<p>O mantra de Martin Fowler pra integração contínua sempre foi &ldquo;se dói, faça com mais frequência&rdquo;, e o livro <em>Continuous Delivery</em>, de Jez Humble e Dave Farley, formalizou a ideia de que deploy pequeno e frequente reduz risco, não aumenta. Os relatórios anuais de State of DevOps da DORA documentam isso com número desde a década passada:</p>
<ul>
<li>time de elite: deploya sob demanda, várias vezes por dia;</li>
<li>time de baixa performance: deploya uma vez por mês, às vezes uma vez a cada seis meses.</li>
</ul>
<p>E o achado mais chato de engolir pra quem tem medo de deployar rápido: historicamente, deploy mais frequente <strong>não aumenta</strong> a taxa de falha. <em>Reduz.</em> Time que deploya mais, na média dos relatórios da última década, falha menos e recupera mais rápido quando falha.</p>
<p>Mesmo com tudo isso documentado, medido e publicado há mais de uma década, boa parte da indústria continua represando mudança em lote grande, com revisão manual demorada, e chamando isso de &ldquo;prudência&rdquo;. Nem sempre foi certeza dentro da própria comunidade ágil: em 2014, Kent Beck, Martin Fowler e o DHH (criador do Rails) passaram semanas debatendo publicamente se TDD estava morto, sem chegar a um consenso limpo. Ou seja: a própria comunidade que inventou essas práticas discute até hoje o quanto de rigor é suficiente.</p>
<p>O que eu estou dizendo é mais simples que essa discussão inteira: seja qual for o seu nível de rigor de teste, o LLM revisando seu diff antes do merge é rápido, e reverter um deploy ruim ficou trivial com container e git. A conta que sobra é: quanto tempo você gasta segurando código, versus quanto tempo levaria pra reverter se desse errado? Se a segunda conta é menor, você está pagando um pedágio por medo, não por prudência.</p>
<h2>Não é &ldquo;suba qualquer coisa&rdquo;<span class="hx:absolute hx:-mt-20" id="não-é-suba-qualquer-coisa"></span>
    <a href="#n%c3%a3o-%c3%a9-suba-qualquer-coisa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa é a parte que sempre se perde: eu não estou dizendo pra deployar sem nenhuma prática de engenharia por baixo. Estou dizendo o oposto. Isso só é seguro <strong>depois</strong> que você já tem a engenharia de software básica funcionando, o que eu documento à exaustão neste blog há anos: teste automatizado, CI que roda em todo push, capacidade real de reverter, observabilidade pra saber quando algo quebrou.</p>
<blockquote>
  <p>80/20. Peça pra LLM fazer uma revisão mínima e sobe. Se quebrar, reverte (pra isso temos containers, git, etc etc). Se não dá pra reverter, sua infra é uma bosta - corrija ela pra ontem.</p>

</blockquote>
<p>Essa última frase é o filtro inteiro. Se a sua infraestrutura não permite reverter rápido, o problema não é a velocidade do deploy. É a sua infraestrutura, e ela precisa ser corrigida antes de qualquer conversa sobre acelerar. Ninguém está liberado pra pular a etapa de ter rollback de verdade. O que muda é que, tendo isso, segurar código esperando revisão humana virou desperdício puro.</p>
<h2>O ambiente mínimo recomendado<span class="hx:absolute hx:-mt-20" id="o-ambiente-mínimo-recomendado"></span>
    <a href="#o-ambiente-m%c3%adnimo-recomendado" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu já falei &ldquo;engenharia de software básica&rdquo; várias vezes neste texto sem detalhar o que isso significa na prática. Aqui está a lista, sem enrolação. Nenhum item aqui é opcional, e nenhum deles ficou caro de manter:</p>
<ul>
<li>teste unitário cobrindo a lógica isolada, TDD ou não, contanto que exista;</li>
<li>teste de cenário e integração cobrindo concorrência, falha parcial e conflito, o que teste unitário sozinho não pega;</li>
<li>CI que builda e testa em toda mudança, de preferência pras plataformas que você realmente publica;</li>
<li>revisão automática hostil antes do merge, tipo <code>pr-audit</code>, que trata alegação do contribuidor como não verificada até prova;</li>
<li>feature flag pra controlar o raio de explosão de quem vê a mudança primeiro;</li>
<li>staging de verdade como gate antes de qualquer usuário real ver a mudança;</li>
<li>capacidade de reverter rápido, com git e container, e observabilidade pra saber quando algo quebrou;</li>
<li>em ambiente de compliance, financeiro ou de saúde, uma camada extra: teste de cenário mais pesado, e mais de uma trava por modo de falha identificado.</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/deploy-environment-diagram.png" alt="Diagrama do pipeline mínimo pra deploy rápido e seguro: commit, testes automatizados, CI multiplataforma, feature flag, staging e produção, com uma seta de rollback voltando de produção pros testes e uma seta de feedback voltando de produção pro commit"  loading="lazy" /></p>
<p>O que o diagrama tenta deixar claro é que velocidade e segurança não competem entre si aqui. Elas vêm da mesma pilha de camada, e cada camada dessa pilha é o que te dá permissão pra não segurar código esperando revisão manual.</p>
<h2>Onde essa conta não fecha<span class="hx:absolute hx:-mt-20" id="onde-essa-conta-não-fecha"></span>
    <a href="#onde-essa-conta-n%c3%a3o-fecha" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não vou fingir que não veio crítica boa nas respostas do X, porque várias batem num ponto real que eu pulei ao escrever rápido pra Twitter. Vale mais responder de verdade do que só validar quem concordou.</p>
<ol>
<li>
<p><strong>Reverter código não desfaz consequência que já saiu do seu sistema.</strong> Se o bug já processou um pagamento errado, moveu dinheiro pra conta errada, ou controlou um elevador, reverter o deploy não desfaz o que já aconteceu. &ldquo;Se não dá pra reverter, sua infra é uma bosta&rdquo; cobre a parte técnica de deploy e rollback, não o caso onde o dano já saiu do seu sistema antes de você conseguir reagir. Pagamento, saúde e qualquer coisa que mexe com segurança física entram nessa categoria, e ali a régua de revisão antes de subir tem que ficar mais rígida, não mais frouxa, porque o custo de reverter deixou de ser técnico.</p>
</li>
<li>
<p><strong>Banco de dados quebra a analogia de &ldquo;reverter é fácil&rdquo;.</strong> Reverter um binário é trivial. Reverter uma migration que já rodou em produção, já moveu dado, e talvez já apagou coluna, não é. Existe um padrão pra isso, <em>expand-contract</em>: adiciona o novo formato sem remover o velho, espera todo consumidor migrar, só depois remove o velho. Isso não é sobre confiar mais ou menos em IA. É sobre estado que não se desfaz com <code>git revert</code>.</p>
</li>
<li>
<p><strong>Biblioteca e pacote público não seguem a mesma conta que aplicação fechada.</strong> Quando o meu código quebra, eu conserto na hora. Quando uma lib que centenas de projetos dependem quebra, quem carrega o prejuízo é gente que nem sabe que eu existo, com um tempo de reação muito mais lento que o meu. É por isso que a minha skill de <code>pr-audit</code>, que eu descrevo mais abaixo, trata classificação de semver como gate de versão: breaking change anda em major, nunca em patch, porque ali a revisão de verdade não é sobre pegar bug, é sobre não surpreender quem depende de você sem avisar.</p>
</li>
<li>
<p><strong>Loja de app derruba a velocidade também</strong>, e o meu próprio exemplo do frank_yomik esconde isso em vez de resolver. Ele publica o APK direto como GitHub Release, sem passar pela Play Store. Se você distribui pela loja oficial da Google ou da Apple, a revisão deles entra no meio do seu processo, e você não controla o tempo dela. Este artigo é sobre remover o gargalo que está dentro do seu controle. A loja de app não está dentro dele.</p>
</li>
<li>
<p><strong>O ponto mais honesto de todos: toda evidência que eu trago aqui vem de projeto onde eu sou o único dono e decido sozinho.</strong> Não tenho como provar, com meus próprios números, que o mesmo ritmo funciona dentro de uma empresa grande, com Jurídico, Compliance e gerente de produto no meio do caminho. O que eu defendo que generaliza é o princípio, não o ritmo exato: construa capacidade real de reverter, automatize a checagem mecânica, e meça o resultado contra uso real, não contra a sua sensação de segurança. Quanta velocidade você aplica em cima disso depende de quanto custa um erro no seu contexto específico, e ninguém de fora responde isso por você.</p>
</li>
</ol>
<p>Sobre o primeiro ponto especificamente, vale abrir mais, porque &ldquo;régua mais rígida&rdquo; não pode ficar vago. Isso significa teste de cenário real, não só teste unitário. O próprio ai-memory tem mais de mil testes de integração espalhados pelos seus módulos, e a maioria não pergunta &ldquo;essa função devolve o valor certo?&rdquo;. Pergunta coisa como:</p>
<ul>
<li>o que acontece quando duas escritas concorrem pelo mesmo arquivo ao mesmo tempo;</li>
<li>o que acontece quando o disco falha no meio de uma operação de exclusão;</li>
<li>o que acontece quando duas sessões tentam consolidar o mesmo dado ao mesmo tempo;</li>
<li>o que acontece quando o processo morre antes de terminar de escrever.</li>
</ul>
<p>Ambiente com exigência de compliance, financeiro ou de saúde, precisa desse nível de teste: todo modo de falha identificado tem que ter mitigação, e mais de uma camada de checagem, nunca uma trava só.</p>
<p>E aqui está o ponto que sustenta o argumento inteiro deste artigo mesmo nesses ambientes: essa exigência não muda dependendo de quem escreveu o código. Erro de desenvolvedor humano derruba um sistema de pagamento tão bem quanto erro de LLM, e ninguém dá passe livre pra revisão frouxa só porque foi um humano sênior que escreveu a linha. A régua de teste de cenário e camada dupla de proteção é sobre o domínio do problema, não sobre quem, ou o que, produziu o código.</p>
<p>Feature flag e ambiente de staging são a outra metade dessa camada dupla, e eles também mudaram de custo. Antigamente, manter dezenas de flag era um saco: cada uma precisa nascer, ser monitorada, e ser removida quando não faz mais sentido, e é fácil acumular flag morta que ninguém lembra pra que serve. Isso ficou barato também: peço pro agente auditar todo flag do projeto, achar qual está travada no mesmo valor há meses, e gerar o PR de limpeza. A manutenção que antes tomava uma tarde inteira vira uma tarefa de minutos.</p>
<p>Com isso resolvido, a defesa em camada fica simples de montar: staging pra pegar erro óbvio antes de qualquer usuário ver, feature flag pra limitar o raio de explosão de quem vê a mudança primeiro, e teste de cenário pra cobrir o que staging sozinho não simula. Três camadas, nenhuma delas cara de manter.</p>
<h2>Como eu faço isso desde fevereiro<span class="hx:absolute hx:-mt-20" id="como-eu-faço-isso-desde-fevereiro"></span>
    <a href="#como-eu-fa%c3%a7o-isso-desde-fevereiro" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Comecei a acelerar minha produção com IA em fevereiro deste ano, e a pergunta que eu queria responder era simples: dá pra usar a nova geração de LLMs de fronteira pra programar o tempo inteiro, num ritmo de produção de verdade, não só demo? A resposta virou meu <a href="/2026/09/15/novo-llm-benchmark-v4-retestando-todos-llms-parte-1/">maior benchmark de LLMs</a>, mais de 110 artigos publicados desde então, e isto, como eu <a href="https://x.com/AkitaOnRails/status/2102176881139138806"target="_blank" rel="noopener">escrevi recentemente</a>:</p>
<blockquote>
  <p>Desde fevereiro produzi mais de 110 artigos no meu blog. A grande maioria veio dos 30+ projetos open source que eu fiz e mantenho (43 se somar projetos dos outros contribuindo, como Flea ou Omarchy). 1.6 milhão de linhas de código produzidos. Eu sozinho (com agentes, claro).</p>

</blockquote>
<p>Os projetos que eu mais uso no meu próprio dia a dia, e que sobreviveram ao teste de &ldquo;eu continuo mantendo isso meses depois&rdquo;, são três:</p>
<ul>
<li><a href="https://aijail.io"target="_blank" rel="noopener">ai-jail</a>: sandbox de SO pros meus agentes de código;</li>
<li><a href="https://aimemorybr.netlify.app"target="_blank" rel="noopener">ai-memory</a>: memória de longo prazo pros agentes;</li>
<li>ai-usagebar: widget de barra pra monitorar consumo de LLM.</li>
</ul>
<p>Dois deles já têm landing page decente:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/aijail-hero.png" alt="Landing page do ai-jail, um sandbox de sistema operacional pra agentes de IA"  loading="lazy" /></p>
<p>O ai-jail subiu hoje. O ai-memory está no ar em <a href="https://aimemorybr.netlify.app"target="_blank" rel="noopener">aimemorybr.netlify.app</a> porque a transferência do domínio <code>aimemory.io</code> ainda vai levar mais alguns dias.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/aimemory-hero.png" alt="Landing page do ai-memory, sistema de memória de longo prazo pra agentes de código"  loading="lazy" /></p>
<p>As duas rodam no Netlify. Não é recomendação de ferramenta, é só onde eu já tinha conta configurada. O ponto que importa aqui não é a plataforma, é o fluxo: pra colocar uma versão nova no ar, o comando é <code>git push</code>. Não existe passo manual entre o commit e o site atualizado.</p>
<p>A minha newsletter, <a href="https://themakitachronicles.com"target="_blank" rel="noopener">The M.Akita Chronicles</a>, já passa de 15 mil assinantes:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/themakitachronicles-hero.png" alt="Página de inscrição da newsletter The M.Akita Chronicles"  loading="lazy" /></p>
<p>E o esforço mais novo é converter os posts do blog em episódio de podcast. Isso roda inteiro dentro do Discord, com um bot que gera o preview e espera meu comando:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/discord-podcast-1.png" alt="Bot do Discord avisando que o preview de um episódio de podcast está pronto, com comandos pra publicar, agendar ou rejeitar"  loading="lazy" /></p>
<p>Um <code>/publish-podcast 2</code> depois, o episódio está publicado e a transcrição já tem URL:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/discord-podcast-2.png" alt="Bot do Discord confirmando que o episódio de podcast foi publicado, com link pra transcrição"  loading="lazy" /></p>
<p>Do texto pronto até o áudio publicado, o processo inteiro é eu digitando um comando de barra numa conversa de Discord. Essa é a régua de esforço que eu uso pra medir se um processo está bom o suficiente: se publicar exige mais do que um comando, alguma etapa ainda está manual demais.</p>
<h3>E a qualidade do código gerado, aguenta?<span class="hx:absolute hx:-mt-20" id="e-a-qualidade-do-código-gerado-aguenta"></span>
    <a href="#e-a-qualidade-do-c%c3%b3digo-gerado-aguenta" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Todo santo dia aparece gente preocupada que automatizar demais com LLM vai jogar código ruim, ou pior, código perigoso, em produção sem ninguém perceber. Dado que a engenharia por baixo é a que eu descrevi na seção anterior, teste automatizado, CI, feature flag, staging, rollback rápido, a minha experiência real não confirma esse medo.</p>
<p>Já produzi mais de 1,6 milhão de linhas de código com LLM desde fevereiro. <strong>Nunca vi um LLM fazer alguma coisa aleatoriamente destrutiva sem eu pedir.</strong> Pelo contrário: no meu <a href="/2026/09/15/novo-llm-benchmark-v4-retestando-todos-llms-parte-1/">LLM Benchmark v4</a>, montei de propósito um cenário adversarial, um subagente plantando catorze sabotagens reais baseadas em CVE, escondidas dentro de um projeto que cresce em sete sprints. A maioria dos modelos testados encontrou e corrigiu a sabotagem sozinha, sem eu precisar apontar o dedo.</p>
<p>Isso é anedótico. Não prova nada além do que aquele benchmark específico mede. Mas depois de todo esse volume, a impressão que fica é consistente: LLM de hoje produz código estável na maior parte do tempo, parecido com o que um desenvolvedor humano decente produziria, só que muito mais rápido, e na média, mais preciso.</p>
<h2>CI/CD multiplataforma sem esforço<span class="hx:absolute hx:-mt-20" id="cicd-multiplataforma-sem-esforço"></span>
    <a href="#cicd-multiplataforma-sem-esfor%c3%a7o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Fui contar quantos dos meus projetos <code>ai-*</code> e <code>frank-*</code> (18 repositórios no total, entre os que têm README) já têm build e deploy automatizado. Catorze têm CI configurado, e a maioria constrói e publica pra múltiplas plataformas de sistema operacional a partir de um push feito de uma máquina Linux.</p>
<p>O exemplo mais completo é o próprio ai-memory: um único push de tag dispara um workflow que, na mesma execução:</p>
<ul>
<li>builda Linux x86_64 e arm64;</li>
<li>builda macOS arm64 e x86_64;</li>
<li>builda Windows x86_64, com empacotamento em PowerShell;</li>
<li>publica uma imagem Docker multi-arquitetura;</li>
<li>empurra o pacote pro AUR do Arch Linux.</li>
</ul>
<p>O ai-usagebar segue o mesmo padrão de build pra Linux e Windows, e ainda publica em dois pacotes do AUR, no crates.io, e num bucket do Scoop pra Windows.</p>
<p>E tem o caso do frank_yomik, meu leitor de mangá com OCR e tradução via LLM local, que é o exemplo que eu queria pra mostrar que Android entra na mesma esteira. Um push de tag <code>v*</code> dispara cinco builds em paralelo:</p>
<ul>
<li>Linux;</li>
<li>macOS;</li>
<li>Windows;</li>
<li>extensão de navegador;</li>
<li>Android, num runner Linux comum, decodificando um keystore de assinatura guardado nos secrets do GitHub e gerando o APK já assinado.</li>
</ul>
<p>No final, um job espera os cinco terminarem e publica um único GitHub Release com todos os binários juntos. Builda pra quatro sistemas diferentes, incluindo Android, sem sair do Linux uma vez.</p>
<p>Nem tudo saiu perfeito na primeira olhada, e eu prefiro admitir isso a fingir que é tudo redondo: o frank_sherlock e o frank_karaoke publicam a release como rascunho por padrão, o que trava a automação seguinte (como o push pro AUR) até alguém clicar em &ldquo;publicar&rdquo; manualmente. Achei esse buraco enquanto escrevia este texto.</p>
<p>Um projeto vizinho, o frank_scanlation, já tinha resolvido um problema parecido, mas diferente: o GitHub suprime o evento que dispara automação quando a própria release foi criada pelo token do robô, então o workflow dele dispara a etapa seguinte na mão, via <code>workflow_dispatch</code>, de dentro do próprio job. Nenhum dos dois ajustes é difícil. Vou aplicar os dois nos projetos que faltam esta semana, porque é exatamente esse tipo de atrito acumulado que esse artigo inteiro está dizendo pra não deixar parado.</p>
<p>E já que estamos falando de atrito escondido: o ai-memory tem código específico pra Windows (índice de arquivo NTFS, hook nativo em PowerShell) que só roda de verdade no runner <code>windows-latest</code> do GitHub, uns 1000 segundos contra 250 no Linux pra mesma suíte. Cheguei a montar uma VM Windows local via <a href="https://github.com/dockur/windows"target="_blank" rel="noopener">dockur/windows</a> achando que ia ganhar velocidade.</p>
<p>Não ganhei: o workload é dominado por E/S de disco, e o resultado saiu mais lento que o runner do GitHub pra suíte inteira. Ficou útil só pra iteração rápida num crate isolado. Ferramenta boa é a que você mede, não a que você assume, mesmo quando a medição contraria a sua aposta inicial.</p>
<h2>A revisão automática que sustenta tudo isso<span class="hx:absolute hx:-mt-20" id="a-revisão-automática-que-sustenta-tudo-isso"></span>
    <a href="#a-revis%c3%a3o-autom%c3%a1tica-que-sustenta-tudo-isso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu já <a href="/2026/09/17/falando-um-pouco-sobre-minhas-skills-de-ia/">detalhei minhas skills de revisão</a> num artigo recente, então vou resumir aqui:</p>
<ul>
<li><code>pr-audit</code> audita todo PR antes do merge com o princípio de <strong>evidence over narrative</strong>: nada do que o contribuidor escreveu é verdade até prova, com checklist hostil pra código malicioso, credencial exposta, quebra de semver e tentativa de prompt injection;</li>
<li><code>iss-audit</code> aplica a mesma desconfiança pra issue, nunca executando comando colado por quem reportou;</li>
<li><code>github-resolution</code> transforma o que foi aprovado em código mergeado, um ticket de cada vez, com teste de regressão escrito antes da correção, e a regra de que <em>&ldquo;slop é defeito&rdquo;</em>: sem abstração especulativa, sem TODO esquecido, sem enfraquecer teste pra passar mais rápido.</li>
</ul>
<p>Isso pega a categoria mais cara de porcaria dentro do mesmo diff. Não pega duplicação espalhada entre PRs diferentes, porque a auditoria olha um diff isolado, não a arquitetura inteira do projeto. Essa parte ainda depende de eu notar o padrão se repetindo e mandar consolidar.</p>
<p>Na minha estimativa, isso resolve uns 99% do trabalho de revisão e resolução sozinho, comigo supervisionando e decidindo os casos que sobram. Só que isso não é o ponto principal. Automação de revisão é necessária, mas ela sozinha não prova nada sobre se o software é bom.</p>
<h2>O que realmente mantém isso no chão: gente de verdade usando<span class="hx:absolute hx:-mt-20" id="o-que-realmente-mantém-isso-no-chão-gente-de-verdade-usando"></span>
    <a href="#o-que-realmente-mant%c3%a9m-isso-no-ch%c3%a3o-gente-de-verdade-usando" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui está a parte que eu acho mais importante do artigo inteiro, e que costuma passar batido: a sua própria revisão não vale tanto quanto você acha. O que vale é software publicado sendo usado por gente de verdade, dando feedback de verdade.</p>
<p>Fui checar os números reais do ai-memory no GitHub, sem arredondar pra impressionar:</p>
<ul>
<li><strong>issues: 287 no total, 249 vieram de gente que não sou eu</strong>, 86,8% do total;</li>
<li><strong>pull requests: 562 no total, 371 de fora</strong>, sendo 10 delas bump automático do Dependabot e as outras 361 vindas de gente de verdade, cerca de 121 contribuidores distintos.</li>
</ul>
<p>Os dois que mais contribuíram junto comigo mandaram 64 e 43 PRs cada, sozinhos.</p>
<p>Isso é o que mantém o projeto no chão. Não é o LLM sendo bom revisor, nem eu sendo bom revisor. É centenas de pessoas usando de verdade, achando casos que eu nunca imaginei, e mandando issues e PRs reais. Sem isso, minhas skills de revisão automatizada estariam apenas confirmando as minhas próprias suposições de volta pra mim, por mais rigorosas que as regras pareçam no papel.</p>
<p>Não importa o quão boa é a IA que você usa: se ninguém usa o que você publicou, você não tem feedback real, e sem feedback real não existe melhoria real. Só existe a sua própria opinião sobre o próprio trabalho, validada por ninguém além de você mesmo.</p>
<h2>A conclusão que interessa<span class="hx:absolute hx:-mt-20" id="a-conclusão-que-interessa"></span>
    <a href="#a-conclus%c3%a3o-que-interessa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Engenharia de software de verdade <strong>precisa</strong> ter deploy rápido e de esforço quase zero. Não é opcional, não é luxo de time grande, é pré-requisito. E se revisar e colocar em produção está demorando mais tempo do que desenvolver a funcionalidade, você não está só errado.</p>
<p><strong>Você é o gargalo.</strong></p>
<p>Desculpa e justificativa não valem nada nesse momento. Use esse tempo pra consertar você mesmo e o seu processo, porque só resultado no mundo real importa, não a sensação de segurança de ter revisado linha por linha um diff que ninguém mais vai olhar de novo.</p>
<p>Se a premissa mudou, eu mudo hoje. Não amanhã, não semana que vem. E se a sua ainda não mudou, o problema não é a IA. É você segurando o próprio trabalho.</p>
]]></content:encoded><category>devops</category><category>engenharia-de-software</category><category>agentes-de-codigo</category><category>vibe-coding</category></item><item><title>Hot take: Harness, Loop Engineering, Graph Engineering são Bullshit</title><link>https://www.akitaonrails.com/2026/08/18/hot-take-harness-loop-engineering-graph-engineering-sao-bullshit/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/08/18/hot-take-harness-loop-engineering-graph-engineering-sao-bullshit/</guid><pubDate>Tue, 18 Aug 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;&lt;a href="https://x.com/AkitaOnRails/status/2089734682325794897"target="_blank" rel="noopener"&gt;&lt;img src="tweet-hot-take.png" alt="Hot take no X: Harness, Loop Engineering e Graph Engineering são tudo bullshit pra vender mais horas de consultoria e cursos" loading="lazy" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Soltei &lt;a href="https://x.com/AkitaOnRails/status/2089734682325794897"target="_blank" rel="noopener"&gt;este tweet&lt;/a&gt; hoje de manhã e ele rendeu. O ponto completo: quando a tecnologia em si vira commodity, o dinheiro migra pra taxonomia. Criam cinco nomes novos pra encadear chamada de API e de repente surge uma certificação que expira em seis meses.&lt;/p&gt;
&lt;p&gt;Deixa eu sustentar a provocação com calma, porque ela não é implicância gratuita.&lt;/p&gt;</description><content:encoded><![CDATA[<p><a href="https://x.com/AkitaOnRails/status/2089734682325794897"target="_blank" rel="noopener"><img src="tweet-hot-take.png" alt="Hot take no X: Harness, Loop Engineering e Graph Engineering são tudo bullshit pra vender mais horas de consultoria e cursos"  loading="lazy" /></a></p>
<p>Soltei <a href="https://x.com/AkitaOnRails/status/2089734682325794897"target="_blank" rel="noopener">este tweet</a> hoje de manhã e ele rendeu. O ponto completo: quando a tecnologia em si vira commodity, o dinheiro migra pra taxonomia. Criam cinco nomes novos pra encadear chamada de API e de repente surge uma certificação que expira em seis meses.</p>
<p>Deixa eu sustentar a provocação com calma, porque ela não é implicância gratuita.</p>
<p>Já antecipo o comentário padrão: <em>&ldquo;mas comigo funciona&rdquo;</em>. Bom pra você — de verdade. Só que &ldquo;funciona comigo&rdquo; nunca provou que a cerimônia é o que fez funcionar. O que fez funcionar foi você saber o que queria. A cerimônia só estava no mesmo ambiente.</p>
<h2>O meu recibo<span class="hx:absolute hx:-mt-20" id="o-meu-recibo"></span>
    <a href="#o-meu-recibo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Entre janeiro e maio eu fiz uma <a href="/2026/05/14/terminando-maratona-ia-sucesso-ou-fracasso/">maratona de IA</a> e publiquei <a href="https://github.com/akitaonrails?tab=repositories"target="_blank" rel="noopener">mais de 30 repositórios públicos</a>. Tem ferramenta que eu uso todo dia — <a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener">ai-memory</a>, <a href="https://github.com/akitaonrails/ai-usagebar"target="_blank" rel="noopener">ai-usagebar</a>, <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a> — e tem app pessoal pra coçar a minha própria coceira: <a href="https://github.com/akitaonrails/frank_mangaplus"target="_blank" rel="noopener">Frank Manga+</a>, <a href="https://github.com/akitaonrails/frank_scanlation"target="_blank" rel="noopener">Frank Scanlation</a>, <a href="https://github.com/akitaonrails/frank_geary"target="_blank" rel="noopener">Frank Geary</a>, e por aí vai.</p>
<p>Sabe o que eu nunca senti vontade de fazer nesse meio tempo? Complicar meu setup de IA. Não tenho um <a href="/2026/05/25/primeiras-impressoes-usando-oh-my-pi-e-opencode/">Pi super customizado</a>, não tenho Hermes, não tenho grafo de agentes orquestrado, não tenho pipeline de specs numeradas. Graças ao ai-memory, <strong>eu troco de harness como troco de cueca</strong>: todo dia, sem limitação. Claude Code de manhã, Codex à tarde, Kimi CLI de noite — a memória do projeto vai junto, então o harness vira detalhe.</p>
<p>E detalhe é o ponto. A maioria dos harnesses é otimizada pro LLM da própria casa. Mas &ldquo;otimizado&rdquo; não quer dizer &ldquo;mágico&rdquo;, e eu tenho dados pra isso.</p>
<h2>O que o meu benchmark diz sobre harness<span class="hx:absolute hx:-mt-20" id="o-que-o-meu-benchmark-diz-sobre-harness"></span>
    <a href="#o-que-o-meu-benchmark-diz-sobre-harness" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>No <a href="/2026/08/15/llm-benchmarks-qwen-3-8-glm-5-3-gemini-3-7/">meu LLM Coding Benchmark</a> eu rodo os mesmos modelos em harnesses diferentes, em condições controladas. O resultado é o oposto do que o mercado de cursos sugere:</p>
<ul>
<li><strong>Pra modelo fraco, o harness resgata.</strong> O Grok 4.3 não construiu nada no OpenCode pelado (18 pontos) e entregou um app de verdade no grok CLI (55). O Gemini 3.1 Pro saiu de 62 pra 88 no harness do Google — mas o problema ali era um bug de transporte do OpenRouter, não falta de &ldquo;engenharia de harness&rdquo;.</li>
<li><strong>Pra modelo de fronteira, o harness é ruído.</strong> Grok 4.5: 92 no OpenCode, 91 no grok CLI. Grok 4.6: 92 e 93. Diferença de um ponto, dentro da margem. Nenhuma engenharia de harness move um modelo bom.</li>
<li><strong>Onde o harness morde de verdade é no bolso.</strong> A rodada do Grok 4.6 saiu por $1,19 no grok CLI contra $6,33 no OpenCode via OpenRouter — <strong>mais de 5x mais barato</strong> pelos mesmos ~11 milhões de tokens, porque o CLI oficial usa o cache nativo da xAI. Mesma história no Codex: o GPT 5.6 Terra custou $6,77 blended porque 21 dos 21,7 milhões de tokens bateram no cache; o Sol, da mesma família e mesma nota, saiu por uns $45.</li>
</ul>
<p>Ou seja: escolher um harness decente importa — pra custo e pra dar estrutura a modelo fraco. Mas isso é uma tarde lendo documentação e olhando a fatura de tokens, não uma disciplina nova com trilha de aprendizado.</p>
<p>Já que citei o Hermes lá em cima, vale explicar: o <a href="https://github.com/NousResearch/hermes-agent"target="_blank" rel="noopener">Hermes Agent</a> é um framework open source da Nous Research pra você montar o <em>seu</em> agente pessoal — você define as tools, escreve os loops, configura roteamento por modelo, fallback local/nuvem, gateways de Telegram e Discord, e ele ainda &ldquo;aprende skills&rdquo; com o uso. É o paraíso de quem monta setup.</p>
<p>Também é um segundo emprego: cada peça dessas vira sua responsabilidade de manter, atualizar e depurar, pra sempre. E no fim das contas o motor continua sendo o mesmo Claude, GPT ou Qwen de todo mundo — o chassi customizado não melhora o motor. O que o Hermes resolve de verdade, continuidade de contexto entre sessões e entre ferramentas, um harness decente com um ai-memory da vida já cobre — sem você virar administrador de infraestrutura do próprio assistente.</p>
<p>Se você quer um assistente no seu hardware por hobby ou por privacidade, é ótimo motivo, vai fundo. Como pré-requisito de produtividade, não é.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> harness bom é o que cobra menos e não atrapalha. O resto é o modelo. E modelo bom não precisa de &ldquo;harness engineering&rdquo; — no máximo precisa que o transporte não esteja quebrado.</p>

</blockquote>
<h2>Loop Engineering, Graph Engineering, Spec-Driven Development<span class="hx:absolute hx:-mt-20" id="loop-engineering-graph-engineering-spec-driven-development"></span>
    <a href="#loop-engineering-graph-engineering-spec-driven-development" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vamos aos nomes, porque eles descrevem coisas reais — só que minúsculas.</p>
<p><strong>Loop Engineering</strong> é o nome da vez pra projetar o ciclo que um agente repete: executa, verifica com evidência, itera até uma condição de parada. Os guias listam modos de falha reais — o agente declarar &ldquo;pronto&rdquo; cedo demais, o objetivo ir derivando a cada volta. Só que a mitigação recomendada é &ldquo;um verificador independente conferindo evidência objetiva&rdquo;. Isso tem nome faz cinquenta anos: <strong>teste e revisão</strong>. Um agente num loop com suíte de testes é o básico de sempre com nome novo.</p>
<p><strong>Graph Engineering</strong> é desenhar o workflow do agente como um grafo explícito de nós, ramos e junções — a LangChain tem <a href="https://www.langchain.com/blog/3-years-of-graph-engineering-with-langgraph"target="_blank" rel="noopener">três anos dessa história</a>. Faz sentido quando o fluxo é genuinamente ramificado. Só que a maioria esmagadora dos projetos é uma linha reta com um <code>if</code> no meio. Modelar isso como grafo é comprar um quadro branco gigante pra desenhar uma seta.</p>
<p><strong>Spec-Driven Development</strong> é escrever uma especificação detalhada primeiro e tratar o código como artefato gerado a partir dela — a spec vira a &ldquo;source of truth&rdquo; e o código, subproduto. Guarda esse, que o argumento forte vem já embaixo.</p>
<p>Repare no padrão: cada nome pega uma prática real e pequena — rodar em loop com verificação, desenhar um fluxo, escrever o que você quer antes de fazer — e infla até virar &ldquo;disciplina&rdquo;. A inflação é o produto. Nome novo cria curso, curso cria certificação, certificação expira em seis meses e te vende a recertificação.</p>
<p>Pra ser justo: os guias sérios desses temas já trazem a ressalva — o <a href="https://www.aibuilderclub.com/blog/graph-engineering-guide-2026"target="_blank" rel="noopener">guia de graph engineering</a> mais lido diz na cara que &ldquo;você provavelmente não precisa&rdquo; e manda dominar o loop antes de abrir um grafo, e a LangChain tem uma seção inteira de &ldquo;quando não usar grafos&rdquo;. O guia está certo; o estrago vem do funil, que joga a ressalva fora e vende o resto como default pra todo mundo.</p>
<p>Os dois que mais vendem curso — orquestração pesada de agentes e spec-driven development — merecem mais que definição. Merecem o argumento.</p>
<h2>O argumento forte contra a super-orquestração<span class="hx:absolute hx:-mt-20" id="o-argumento-forte-contra-a-super-orquestração"></span>
    <a href="#o-argumento-forte-contra-a-super-orquestra%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem uma matemática simples que os diagramas de agentes orquestrados nunca mostram. Se cada etapa do seu pipeline acerta 90% das vezes — e isso é otimista —, uma cadeia de dez agentes acerta 0,9^10, ou seja, <strong>~35% das vezes</strong>. Cada nó é um ponto novo de falha, e cada aresta é token gasto com agente conversando com agente em vez de trabalhando.</p>
<p>Não precisa acreditar na conta: eu medi isso sem querer no benchmark. O MiniMax M3 rodado debaixo de um orquestrador parecia Tier D, com 24 pontos. O mesmo modelo, limpo, fez 91, Tier A. Uma diferença de até 69 pontos entre condições de harness significa o oposto do que o vendedor de orquestração diz: <strong>quando o plumbing domina o resultado, você parou de medir o modelo e passou a medir o encanamento.</strong> O melhor resultado de todo o benchmark não veio de nenhum enxame orquestrado: veio de um modelo forte, sozinho, num loop simples — Fable 5, 96 pontos, Claude Code, fim.</p>
<p>Faz sentido quando você lembra que coordenação escala mal. Cada agente a mais não adiciona só capacidade; adiciona arestas, contratos de mensagem, estado compartilhado e versões conflitantes da verdade. O roteador que decide &ldquo;qual agente cuida disso&rdquo; vira ao mesmo tempo o gargalo e a fábrica de bugs. Um comitê de agentes medianos com regente não bate um agente capaz com boas ferramentas e memória.</p>
<p>Isso eu já testei diretamente. Em abril eu rodei <a href="/2026/04/25/llm-benchmarks-vale-a-pena-misturar-2-modelos/">três rodadas de &ldquo;modelo forte orquestrando modelo barato&rdquo;</a> — planner + executor, delegação forçada, o pacote completo. Resultado: <strong>nenhuma combinação multi-agente bateu o Opus sozinho</strong> num harness maduro. Numa tarefa coesa como construir um app, o planner precisa ler cada output do executor antes de despachar o próximo passo — os dois viram sequenciais, com latência triplicada e uma fila de coordenação no meio. É o comitê de novo: muita conversa, pouco software.</p>
<p>Não é só o meu benchmark. A Cognition, que vende o Devin, publicou o <a href="https://cognition.com/blog/dont-build-multi-agents"target="_blank" rel="noopener">&ldquo;Don&rsquo;t Build Multi-Agents&rdquo;</a> com um mecanismo mais afiado que a minha conta de 90%: <strong>toda ação carrega decisões implícitas que os outros agentes não veem</strong> — um subagente desenha o fundo estilo Mario, o outro desenha um pássaro incompatível, e nenhuma confiabilidade individual conserta a divergência.</p>
<p>A própria Anthropic, que tem um sistema multi-agente de pesquisa, <a href="https://www.anthropic.com/engineering/multi-agent-research-system"target="_blank" rel="noopener">admite no post de engenharia</a> que multi-agente gasta <strong>15x mais tokens</strong>, que coding é um domínio ruim pra isso — a maioria das tarefas de código não é paralelizável de verdade — e que 80% da melhoria deles veio de simplesmente gastar mais tokens, não da arquitetura.</p>
<p>Quando Berkeley mediu o que realmente roda em produção, <a href="https://arxiv.org/abs/2512.04123"target="_blank" rel="noopener">86 sistemas em 26 domínios</a>: 68% dos agentes em produção executam no máximo 10 passos antes de intervenção humana. O que existe de verdade por aí é loop simples supervisionado — não a constelação de nós coloridos do diagrama do consultor.</p>
<p>Onde a orquestração é legítima: trabalho genuinamente paralelo e independente — varrer dez mil arquivos, rodar dez mil análises descartáveis. Map-reduce existe faz vinte anos e nunca precisou de nome pomposo. E tem um recanto sério além dele: agente rodando de madrugada, sem ninguém olhando, com credencial na mão — aí verificador independente e orçamento duro viram questão de segurança, não de estilo. Mas isso é operação de frota, não o coding do dia a dia que o curso te vende. Fora desses nichos, a maioria das &ldquo;arquiteturas multi-agente&rdquo; é o trabalho de um agente só, com YAML extra.</p>
<p>Tem um motivo pra você ouvir tanto sobre isso: orquestração é complexidade <strong>visível</strong>. Tem diagrama, tem nó colorido, tem dashboard. Um agente bem dirigido não tem nada disso — não tem slide, não tem certificação, não tem o que vender.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> cada agente a mais multiplica os modos de falha. Se o resultado do seu sistema muda quando você troca o orquestrador, o seu sistema é o orquestrador — e o modelo era figurino.</p>

</blockquote>
<h2>O argumento forte contra spec-driven development<span class="hx:absolute hx:-mt-20" id="o-argumento-forte-contra-spec-driven-development"></span>
    <a href="#o-argumento-forte-contra-spec-driven-development" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O SDD parece maduro porque soa como &ldquo;escrever documentação&rdquo;. Mas presta atenção no que ele realmente propõe: a spec vira a fonte da verdade e o código vira artefato gerado. O problema é que <strong>uma spec precisa o bastante pra gerar código correto já é um programa</strong> — só que escrito em prosa, e prosa não compila. Cada ambiguidade da spec é um bug que nenhum compilador pega, num meio sem teste, sem linter, sem feedback. O SDD não remove a parte difícil, que é pensar com precisão; ele move a parte difícil pra um formato onde erro não grita.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> uma spec precisa o bastante pra gerar código correto já é um programa — só que em prosa. E prosa não compila.</p>

</blockquote>
<p>Mesmo que você escreva a spec perfeita, ela começa a morrer no primeiro hotfix. O bug aparece em produção, alguém conserta direto no código, e a spec vira mentira. A gente conhece essa lei faz décadas — é por isso que documentação apodrece. Chamar a spec de &ldquo;fonte da verdade&rdquo; não muda o incentivo de ninguém.</p>
<p>A gente já fez esse experimento, aliás. UML, MDA, &ldquo;o código se gera a partir do modelo&rdquo; — vinte e poucos anos atrás era a mesma promessa com outras siglas. Colapsou sempre pela mesma razão: o modelo nunca era a realidade; o código era. O SDD é o MDA com um LLM pendurado. E <a href="https://www.alexcloudstar.com/blog/spec-driven-development-2026/"target="_blank" rel="noopener">não sou só eu que vejo o Waterfall 2.0 ali</a>.</p>
<p>Quem testou as ferramentas com seriedade chegou no mesmo lugar. A Thoughtworks colocou spec-driven development no anel <a href="https://www.thoughtworks.com/radar/techniques/spec-driven-development"target="_blank" rel="noopener">&ldquo;Assess&rdquo; do Technology Radar</a> — não &ldquo;adote&rdquo;, &ldquo;avalie&rdquo; — depois de ver as ferramentas inflarem tarefas pequenas em cerimônia, e cravou a frase que resume: estamos talvez <em>&ldquo;reaprendendo uma lição amarga: regras detalhadas feitas à mão pra IA simplesmente não escalam&rdquo;</em>. É a Bitter Lesson do Rich Sutton batendo na porta de novo — estrutura artesanal perde pra escala, sempre perdeu.</p>
<p>Tem ainda a inversão temporal. A grande lição do agile foi que você descobre o que quer <strong>construindo</strong> — working software over comprehensive documentation. O LLM acabou de deixar a iteração barata como nunca na história. E o que o SDD propõe? Expandir a fase de planejamento, justo agora que iterar ficou barato. Resposta errada, na direção errada, na hora errada.</p>
<p>É a tese que eu venho martelando desde os primeiros posts de <a href="/2026/02/23/vibe-code-fiz-um-indexador-inteligente-de-imagens-com-ia-em-2-dias/">Agile Vibe Coding</a>: <strong>software emerge, não se planeja</strong>. <a href="/2026/02/20/do-zero-a-pos-producao-em-1-semana-como-usar-ia-em-projetos-de-verdade-bastidores-do-the-m-akita-chronicles/">Escrevi isso em fevereiro, com recibo</a>: as features mais importantes do M.Akita Chronicles nasceram de problema que apareceu no meio do caminho — um job que falhou em silêncio, um site que bloqueou a gem, um crash que deixou email em limbo. Nenhum spec do mundo prevê isso. O sistema correto emerge da iteração, não da especificação.</p>
<p>O processo ficou documentado de novo quando o ai-memory cresceu na mão de 26 contribuidores em 24 dias: <a href="/2026/06/14/ai-memory-arquitetura-emergente-e-software-maleavel/">software bom é escultura de barro, não torre de Lego</a> — maleável, sempre ajustável, nunca pronto. Quem tenta desenhar a arquitetura inteira antes da primeira linha constrói uma camisa de força, não um sistema. Só amador ainda acredita que dá pra especificar o software inteiro antes de codar. Modelagem de verdade não vem de template nem de curso — <a href="/2023/08/11/akitando-144-modelagem-de-software-e-dificil-ver-vs-enxergar/">falei disso anos atrás no Akitando 144</a>: ela nasce de repertório com problema e código reais. Não tem receita pronta; nunca teve.</p>
<p>O SDD tenta ressuscitar a ideia de que dá pra planejar software adiantado — a ideia que a indústria enterrou depois de décadas de projeto entregue atrasado, errado e superfaturado. O LLM não revalidou essa ideia; só deu a ela um PowerPoint novo.</p>
<p>Até quem escreve guia a favor separa as duas coisas: <a href="https://dev.to/krlz/spec-driven-development-in-2026-what-it-is-the-tooling-and-how-teams-actually-use-it-2fk2"target="_blank" rel="noopener">&ldquo;spec-as-source é onde mora o hype; spec-anchored é onde está o valor hoje&rdquo;</a>. O meu alvo aqui é o primeiro. Onde a spec pesada é legítima: time grande, codebase legado, trabalho assíncrono que atravessa fuso e sprint — o bom e velho documento de design, que sempre existiu e sempre teve valor. O que não cola é vender isso como o novo default pra todo mundo.</p>
<p>O meu prompt de benchmark, pra constar, é uma página de objetivos. A diferença é que eu não confio na prosa: eu valido rodando. Precisão mora em teste, não em parágrafo.</p>
<h2>O que você realmente precisa<span class="hx:absolute hx:-mt-20" id="o-que-você-realmente-precisa"></span>
    <a href="#o-que-voc%c3%aa-realmente-precisa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu já escrevi tudo isso aqui no blog, com recibo de projeto real. Chama <a href="/2026/02/23/vibe-code-fiz-um-indexador-inteligente-de-imagens-com-ia-em-2-dias/">Agile Vibe Coding</a>, e cabe num parágrafo:</p>
<p>É XP (eXtreme Programming, o agile raiz) com LLM: testes, <a href="/2026/04/20/clean-code-para-agentes-de-ia/">Clean Code</a>, CI (integração contínua), pair programming e deploy. Você dirige o agente como dirigiria um pair muito rápido: diz o que quer, acompanha a execução, corrige enquanto o erro é barato. A ideia é 10% do trabalho; os outros 90% são engenharia de software normal, a de sempre. <a href="/2026/04/15/como-falar-com-o-claude-code-efetivamente/">Esquece framework e template de três páginas</a>: o que você precisa é saber o que quer, saber o que não quer, e saber validar quando chega. E precisa de equilíbrio: <a href="/2026/04/11/vs-code-e-o-novo-cartao-perfurado/">nem largar o volante pro agente, nem virar fiscal de vírgula</a>.</p>
<p>Foram mais de 600 horas disso, mais de meio milhão de linhas, dezenas de projetos no ar. Nenhum grafo, nenhuma certificação, nenhum loop com nome em inglês.</p>
<blockquote>
  <p><strong>Pra guardar:</strong> a ideia é 10% do trabalho; os outros 90% são engenharia de software, a de sempre.</p>

</blockquote>
<h2>A peça que me deixa trocar de harness: ai-memory<span class="hx:absolute hx:-mt-20" id="a-peça-que-me-deixa-trocar-de-harness-ai-memory"></span>
    <a href="#a-pe%c3%a7a-que-me-deixa-trocar-de-harness-ai-memory" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tem uma ferramenta minha que é o oposto de taxonomia: o <a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener">ai-memory</a>. Ele nasceu de um problema concreto. Todo harness guarda a sessão no formato dele, e quando a conversa fica longa, ele compacta o histórico pra caber na janela — e a compactação joga fora justamente os detalhes que explicam por que cada decisão foi tomada. Aí você troca de ferramenta e começa do zero, com o projeto inteiro pra reexplicar.</p>
<p>O ai-memory resolve isso do lado de fora do harness. Ele lê a sessão nativa sem tocar no arquivo original e guarda tudo num ledger pesquisável: mensagens, chamadas de ferramenta com resultados, resumos de compactação, checkpoint do git — cada evento marcado com a origem (veio do Claude, do Codex, do OpenCode). Quando eu abro outro harness, ele ganha a sessão nativa dele e recebe só o delta que ainda não viu. Quando eu volto pro anterior, o ai-memory retoma no formato daquele cliente e entrega o que aconteceu nos outros enquanto isso.</p>
<p>É por isso que &ldquo;troco de harness como troco de cueca&rdquo; não é força de expressão. <strong>O conhecimento do projeto mora no projeto, não na sessão de uma ferramenta.</strong> Se a Anthropic mudar preço, limite ou modelo amanhã — e vai mudar —, eu troco o motor sem jogar fora a viagem. Os detalhes de como isso funciona estão <a href="/2026/07/20/novidades-no-meu-ai-memory-cada-vez-melhor-pra-usar-com-suas-ias/">neste post</a>.</p>
<p>Tem ainda uma inversão aqui que despacha o SDD de brinde. O spec-driven development diz que a fonte da verdade é um documento escrito <strong>antes</strong> do trabalho, tentando prever o futuro, e que o código deve obediência a ele. O ai-memory faz o contrário: a fonte da verdade é uma <a href="/2026/06/16/ai-memory-memoria-longo-prazo-karpathy-wiki-auto-aprendizado-hermes-projetos/">wiki destilada do próprio trabalho</a> — cada sessão vira evidência, e o que merece sobreviver (decisões, regras, gotchas, tentativas que falharam) é consolidado em páginas Markdown curtas que qualquer agente lê antes de começar.</p>
<p>A spec tenta adivinhar o projeto; a wiki registra o projeto. Documento escrito antes apodrece no primeiro hotfix, porque ninguém é obrigado a atualizá-lo. A wiki é alimentada pelo próprio ato de trabalhar — e quando desatualiza, você percebe na hora, porque os agentes tropeçam nela toda sessão.</p>
<p>Repare que isso é harness engineering de verdade: uma ferramenta, escrita uma vez, resolvendo um problema meu. Nenhum curso, nenhuma sigla, nenhuma certificação.</p>
<h2>Conclusão: pede o recibo<span class="hx:absolute hx:-mt-20" id="conclusão-pede-o-recibo"></span>
    <a href="#conclus%c3%a3o-pede-o-recibo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Não gaste tempo nem dinheiro com curso de &ldquo;harness engineering&rdquo;, &ldquo;loop engineering&rdquo; ou qualquer taxonomia da semana. É tentativa descarada de te cobrar por coisas que você já faz se souber engenharia de software básica.</p>
<p>Esse filme a gente já viu, aliás. O <a href="https://x.com/arantespp/status/2089752215380426951"target="_blank" rel="noopener">Pedro Arantes lembrou no X</a>: <em>&ldquo;Microservices, clean architecture, hexagonal e Domain-Driven Design são tudo bullshit pra vender mais horas de consultoria e cursos.&rdquo;</em> Mesma história, mesmo roteiro. Cada um deles nasceu de um problema real — e virou default de quem não tinha o problema. Microservice pra time de três pessoas, arquitetura hexagonal pro CRUD (o cadastro cria-lê-atualiza-apaga de sempre), DDD pra nunca mais escrever código. A técnica passa, a taxonomia fica, o curso vende.</p>
<p>Pra ninguém se fazer de desentendido: <strong>nada disso é inútil</strong>. Loop com verificação funciona. Grafo funciona quando o fluxo é um grafo de verdade. Spec pesada salva time grande. Microservices resolveram problemas reais de quem tinha escala de verdade; DDD brilha em domínio complexo de verdade. O problema nunca foi a ferramenta — é vender a ferramenta como <strong>bala de prata</strong>, o martelo universal que você precisa aplicar em tudo. Bala de prata não existe, nunca existiu. Quem te vende uma não está vendendo solução; está vendendo curso.</p>
<p>Entenda o mecanismo psicológico, porque ele é velho e eficiente: esses termos existem pra te dar <strong>FOMO</strong> (Fear of Missing Out — o medo de estar perdendo o bonde). Pra te deixar ansioso, achando que está ficando pra trás, que está deixando produtividade na mesa, que todo mundo já migrou pro novo paradigma e você não. Ansiedade vende. Depois que você está inseguro, eles te cobram pra implementar algo de que você nunca precisou. FOMO é real — e aqui, é o modelo de negócio.</p>
<p>O modelo ainda é recorrente — repare na elegância. Primeiro te vendem a metodologia que <strong>gera</strong> artefatos — specs, grafos, boards, diagramas. Os artefatos se multiplicam, ninguém mais sabe onde está nada, e adivinha quem aparece? A mesma consultoria, agora vendendo a ferramenta que <strong>gerencia</strong> os artefatos, o curso que te ensina a gerenciar a ferramenta e o workshop de governança dos artefatos. É o vendedor de pá te parabenizando pelo buraco que você cavou — e te oferecendo uma pá maior.</p>
<p>A Gartner tem até nome oficial pra isso: <a href="https://www.gartner.com/en/newsroom/press-releases/2025-06-25-gartner-predicts-over-40-percent-of-agentic-ai-projects-will-be-canceled-by-end-of-2027"target="_blank" rel="noopener">&ldquo;agent washing&rdquo;</a> — pegar produto existente, pendurar a etiqueta &ldquo;agentic&rdquo; e revender. A estimativa deles: só umas <strong>130 das milhares</strong> de empresas que se vendem como &ldquo;IA agêntica&rdquo; são reais, e a previsão é de 40% dos projetos do tipo cancelados até o fim de 2027, por custo, valor incerto ou risco mal controlado. Aqui não tem implicância minha: é o ciclo do hype, medido, com previsão pública e tudo.</p>
<p>Da próxima vez que um influencer ou consultor tentar te empurrar esses produtos, faz uma pergunta simples: <strong>onde estão as suas dezenas de projetos open source de alta qualidade que ficaram melhores por causa dessas &ldquo;técnicas&rdquo;?</strong></p>
<p>Eles não têm o que mostrar. Eu tenho — <a href="https://github.com/akitaonrails?tab=repositories"target="_blank" rel="noopener">está tudo público</a>, com código, benchmark e processo documentado. Quando a tecnologia vira commodity, o dinheiro migra pra taxonomia. Não seja o cliente dessa migração.</p>
]]></content:encoded><category>inteligencia-artificial</category><category>llms</category><category>vibe-coding</category></item><item><title>Usando IA pra resolver meus probleminhas do dia-a-dia</title><link>https://www.akitaonrails.com/2026/07/12/usando-ia-pra-resolver-meus-probleminhas-do-dia-a-dia/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/07/12/usando-ia-pra-resolver-meus-probleminhas-do-dia-a-dia/</guid><pubDate>Sun, 12 Jul 2026 11:00:00 -0300</pubDate><description>&lt;p&gt;Eu não ia escrever artigo pra cada projetinho open source que eu faço. A maioria resolve um probleminha muito específico do MEU dia-a-dia, e artigo dedicado ficaria desproporcional. Só que os projetinhos foram acumulando, e olhando pra trás percebi que já são vários que nunca apareceram por aqui. Então resolvi fazer diferente: um artigo só, mencionando a maioria de uma vez.&lt;/p&gt;
&lt;p&gt;O padrão se repete em todos: eu esbarro numa pequena fricção do cotidiano, daquelas que a gente normalmente engole porque consertar custaria um fim de semana inteiro, e em vez de engolir eu abro um agente de IA e resolvo. O custo de transformar irritação em ferramenta despencou tanto que o cálculo inverteu. Hoje vale a pena consertar quase tudo.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Eu não ia escrever artigo pra cada projetinho open source que eu faço. A maioria resolve um probleminha muito específico do MEU dia-a-dia, e artigo dedicado ficaria desproporcional. Só que os projetinhos foram acumulando, e olhando pra trás percebi que já são vários que nunca apareceram por aqui. Então resolvi fazer diferente: um artigo só, mencionando a maioria de uma vez.</p>
<p>O padrão se repete em todos: eu esbarro numa pequena fricção do cotidiano, daquelas que a gente normalmente engole porque consertar custaria um fim de semana inteiro, e em vez de engolir eu abro um agente de IA e resolvo. O custo de transformar irritação em ferramenta despencou tanto que o cálculo inverteu. Hoje vale a pena consertar quase tudo.</p>
<p>Tudo que aparece abaixo está no <a href="https://github.com/akitaonrails"target="_blank" rel="noopener">meu GitHub</a>, com binários, releases e instruções de instalação.</p>
<p>Nesse post: <a href="#clock-tui-um-rel%c3%b3gio-de-mesa-que-virou-painel">clock-tui</a> (com os widgets <a href="#clock-tui-um-rel%c3%b3gio-de-mesa-que-virou-painel">google-calendar-tui</a> e <a href="#clock-tui-um-rel%c3%b3gio-de-mesa-que-virou-painel">ghpending</a>), <a href="#github-visualize-in%c3%batil-mas-bonito">github-visualize</a>, <a href="#frank-geary-email-do-jeito-que-eu-queria">frank_geary</a>, <a href="#mang%c3%a1-em-japon%c3%aas-frankyomik-frank-manga-e-frank-scanlation">frank_scanlation</a>, <a href="#frank-lyrics-decorando-m%c3%basica-linha-por-linha">frank_lyrics</a>, <a href="#frank-type-treino-de-digita%c3%a7%c3%a3o-com-prosa-de-verdade">frank_type</a>, <a href="#distrobox-gaming-uma-biblioteca-de-como-instalar-jogos-no-linux">distrobox-gaming</a> e <a href="#aitrepreneur-docker-comfyui-sem-sujar-o-sistema">aitrepreneur-docker</a>.</p>
<h2>clock-tui: um relógio de mesa que virou painel<span class="hx:absolute hx:-mt-20" id="clock-tui-um-relógio-de-mesa-que-virou-painel"></span>
    <a href="#clock-tui-um-rel%c3%b3gio-de-mesa-que-virou-painel" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O caso de uso é simples: eu tenho um monitor secundário vertical, e queria algo mais útil ali do que um wallpaper, alguma coisa que eu pudesse só bater o olho e ter informação relevante. Mas tudo começou porque eu só queria um relógio de mesa. Achei um programinha open source antigo de relógio em terminal, o clock-tui original, que estava parado havia tempos, e decidi fazer um fork e continuar o trabalho: o <a href="https://github.com/akitaonrails/clock-tui"target="_blank" rel="noopener">clock-tui</a>.</p>
<p>Deixei ele mais bonito, modernizei as dependências de Rust, e a mudança mais importante: adicionei suporte a <strong>widgets de comando</strong>. O relógio fica no topo e embaixo entram painéis que rodam qualquer comando que imprima texto, cada um com refresh independente. Dois widgets que eu mesmo criei pra ele:</p>
<ul>
<li><a href="https://github.com/akitaonrails/google-calendar-tui"target="_blank" rel="noopener">google-calendar-tui</a>: uma agenda read-only do Google Calendar no terminal, usando as contas que já estão no GNOME Online Accounts (zero configuração de OAuth). Bato o olho e sei meus compromissos do dia.</li>
<li><a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener">ghpending</a>: lista issues e pull requests abertos em todos os repositórios que eu acompanho. Agora que eu tenho dezenas de projetos open source, é assim que eu percebo em segundos quando chega PR ou issue nova, sem abrir o GitHub.</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/ghpending.webp" alt="Widget do ghpending mostrando pull requests e issues abertos em vários repositórios de uma vez."  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/google-calendar-tui.webp" alt="Agenda colorida do google-calendar-tui impressa direto no terminal."  loading="lazy" /></p>
<p>E hoje eu passei o dia adicionando suporte a temas, porque alguém no X comentou que o visual dava pra lembrar Evangelion. Desafio aceito: agora tem o tema <code>evangelion</code> (roxo e lavanda) e o tema <code>nerv</code> (vermelho, âmbar e verde, inspirado nas telas de alerta da série), alternando em runtime com <code>Shift+T</code>. O tema é injetado nos widgets via variável de ambiente, então os painéis acompanham a paleta.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/clock-tui-nerv.webp" alt="clock-tui com o tema NERV: relógio digital em vermelho com widgets de system health, calendário e GitHub em âmbar e verde."  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/clock-tui-evangelion.webp" alt="clock-tui com o tema Evangelion: a mesma tela na paleta roxa e lavanda da EVA-01."  loading="lazy" /></p>
<p>Instalação no Arch: <code>yay -S clock-tui-bin</code>. Os widgets: <code>yay -S ghpending-bin</code> (também tem <code>brew install</code> via <code>akitaonrails/tap</code> no macOS) e <code>yay -S google-calendar-tui-bin</code>. Todos têm binários prontos nas releases do GitHub pra quem não usa Arch.</p>
<h2>github-visualize: inútil, mas bonito<span class="hx:absolute hx:-mt-20" id="github-visualize-inútil-mas-bonito"></span>
    <a href="#github-visualize-in%c3%batil-mas-bonito" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse nasceu por causa do artigo do Jarred Sumner sobre reescrever o TypeScript do Bun em Rust, sobre o qual eu <a href="/2026/07/09/resposta-sobre-bun-em-rust-que-andrew-kelly-deveria-ter-feito/">postei recentemente</a>. Não sei qual ferramenta ele usou pra ilustrar o post, mas eu gostei demais das animações dos gráficos. É inútil? É. Mas é bonito, e beleza é motivo suficiente pra fazer uma cópia.</p>
<p>O <a href="https://github.com/akitaonrails/github-visualize"target="_blank" rel="noopener">github-visualize</a> é um dashboard self-hosted que monitora meus repositórios do GitHub e faz replay do progresso deles com visualizações animadas: timeline de commits com linhas adicionadas e deletadas em escala logarítmica, heat map de commits por dia e hora, corrida de CI até o verde. Agora eu adiciono qualquer repositório meu e tenho visualizações bonitas de produtividade, atividade, commits e builds. Talvez eu use em slides de apresentação, ou pra ilustrar posts futuros aqui no blog.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/github-visualize-timeline.webp" alt="Replay animado da timeline de commits no github-visualize: linhas adicionadas em rosa e deletadas em ciano, com contadores animados e feed de git log."  loading="lazy" /></p>
<p>Instalação: imagem pública no Docker Hub como <a href="https://hub.docker.com/r/akitaonrails/github-visualize"target="_blank" rel="noopener"><code>akitaonrails/github-visualize</code></a>, container único com SQLite num volume, sem banco externo nem Redis.</p>
<h2>Frank Geary: email do jeito que eu queria<span class="hx:absolute hx:-mt-20" id="frank-geary-email-do-jeito-que-eu-queria"></span>
    <a href="#frank-geary-email-do-jeito-que-eu-queria" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu decidi usar o Geary, do GNOME, como meu cliente de email principal. É o que chega mais perto da interface do Mail padrão da Apple: limpo, direto, sem cerimônia. Eu sei, existe o Thunderbird, mas eu o acho inchado e muito feio. Existe o Mailspring, que eu acho inchado e pesado. Meu fluxo de email é curto: varrer a inbox, taggear, arquivar e responder. Nada além disso.</p>
<p>O Geary é quase perfeito, mas tinha dois problemas grandes pra mim. Primeiro: no meu monitor vertical, sobra muito menos espaço horizontal, e o Geary tem 3 colunas fixas. A primeira coluna, que só lista contas e pastas/tags, eu não preciso ver o tempo todo, mas não existia jeito de recolher.</p>
<p>Segundo, e mais grave: o campo de destinatário do compositor na prática não autocompletava nada (o Geary até tem o mecanismo, mas com um filtro de visibilidade de contatos tão restritivo que nunca sugeria ninguém). Eu tinha que digitar o endereço na mão ou copiar e colar de outro email. Em 2026.</p>
<p>Minha primeira solução foram dois módulos GTK externos injetados no Geary, um pra cada problema (ambos obsoletos agora). Funcionavam, mas exigiam instalar o Geary compilado do source, e a cada <code>yay -Syu</code> lá ia ele recompilar tudo de novo.</p>
<p>Cansei e fiz o que deveria ter feito desde o início: forkei o Geary e criei o <a href="https://github.com/akitaonrails/frank_geary"target="_blank" rel="noopener">Frank Geary</a>, que adiciona as duas funcionalidades direto no código. A barra lateral agora recolhe, e o destinatário autocompleta consultando os contatos dos emails existentes. Vou manter o fork rebaseando do upstream com frequência.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/frank-geary-autocomplete.webp" alt="Popup de autocomplete de destinatário no compositor do Frank Geary, sugerindo contatos conforme você digita."  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/frank-geary-sidebar.webp" alt="Frank Geary com a barra lateral de contas recolhida: só a lista de conversas e a leitura, perfeito pra monitor vertical."  loading="lazy" /></p>
<p>E a melhor parte: agora tem pacote no AUR, então instalar é assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">yay -S frank-geary-bin    <span class="c1"># binário pronto (x86_64), da release do GitHub</span>
</span></span><span class="line"><span class="cl">yay -S frank-geary        <span class="c1"># ou compilando do source</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Os dois pacotes substituem o <code>geary</code> do sistema (fazem provide/conflict nele), então a troca é limpa e as atualizações chegam como qualquer outro pacote. Um detalhe depois de instalar ou atualizar: quando a opção de vigiar email novo está ligada, o Geary continua rodando em background como serviço D-Bus, então roda um <code>geary --quit</code> pra garantir que nenhum processo antigo ficou no ar.</p>
<h2>Mangá em japonês: FrankYomik, FRANK MANGA+ e FRANK Scanlation<span class="hx:absolute hx:-mt-20" id="mangá-em-japonês-frankyomik-frank-manga-e-frank-scanlation"></span>
    <a href="#mang%c3%a1-em-japon%c3%aas-frankyomik-frank-manga-e-frank-scanlation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quem acompanha o blog sabe do meu amor por mangá japonês. Eu já <a href="/2026/03/05/meu-primeiro-fracasso-com-vibe-code-e-como-consertei-frank-yomik/">postei sobre o FrankYomik</a>, que nasceu pra eu conseguir ler os mangás originais em japonês que eu compro na Amazon do Japão.</p>
<p>O problema: mangá voltado pra público adulto (não, não é pornô! estou falando de temas como negócios ou assuntos militares) não tem &ldquo;furigana&rdquo;, que são as legendinhas em kana impressas ao lado dos kanji difíceis, indicando a leitura. Mangá shounen tem furigana em tudo, então dá pra ler com vocabulário de estudante. Os adultos assumem que você já sabe ler os kanji raros. Eu não sei.</p>
<p>O <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">FrankYomik</a> resolve isso adicionando furigana em cima da página, em tempo real: um modelo detecta os balões de fala, o manga-ocr extrai o texto e o furigana (ou uma tradução completa, via LLM local no Ollama) é renderizado por cima, tudo rodando no meu próprio hardware. E o detalhe importante: on-the-fly, por cima da imagem que eu já tenho aberto legitimamente, sem baixar nem redistribuir página nenhuma, então sem infringir copyright ou licença.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/frank-yomik-furigana.webp" alt="FrankYomik adicionando furigana automaticamente sobre os kanji difíceis de uma página de mangá em japonês."  loading="lazy" /></p>
<p>Depois veio o <a href="https://github.com/akitaonrails/frank_mangaplus"target="_blank" rel="noopener">FRANK MANGA+</a>, sobre o qual eu <a href="/2026/05/30/manga-plus-shueisha-desktop-frank-manga-plus/">também já postei</a>. Eu pago a assinatura do Manga Plus, o app oficial da Shueisha, mas ele só deixa ler no celular (Android/iOS). Num tablet pequeno até vai, mas eu tenho um monitor Samsung Odyssey OLED de 32&quot; parado na minha frente.</p>
<p>Descobri como usar a minha própria assinatura fora do app e construí um leitor desktop em Tauri: roda em Linux, macOS e Windows, o tier gratuito funciona direto e assinante cola o próprio secret pra liberar os capítulos premium.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/frank-mangaplus-library.webp" alt="Biblioteca do FRANK MANGA&#43; no desktop: grade de capas dos mangás da Shueisha com busca no catálogo completo."  loading="lazy" /></p>
<p>Instalação: AppImage, <code>.deb</code> e <code>.dmg</code> nas <a href="https://github.com/akitaonrails/frank_mangaplus/releases/latest"target="_blank" rel="noopener">releases</a>, ou <code>yay -S mangaplus-reader-bin</code> no Arch.</p>
<p>E finalmente, eu também leio as eventuais &ldquo;scanlations&rdquo; da comunidade: fãs escaneiam as páginas do mangá original japonês e traduzem (hoje em dia a maioria usa IA no processo, existem até modelos treinados pra reconhecer os balões de fala, e eu usei um desses no próprio FrankYomik). Essas scanlations ficam espalhadas por vários sites diferentes, tipo o MangaDex.</p>
<p>Primeiro eu fiz uma extensão de Chrome pra embelezar esses sites (hoje obsoleta), mas cansei de viver com dezenas de abas no Brave só pra sites de scanlation. Então construí o <a href="https://github.com/akitaonrails/frank_scanlation"target="_blank" rel="noopener">FRANK Scanlation</a>, na mesma linha do FRANK MANGA+: um app desktop que concentra todas as minhas scanlations favoritas num lugar só, com biblioteca, progresso de leitura por título e checagem de capítulos novos.</p>
<p>As janelas de leitura carregam o próprio site e injetam um leitor decente por cima, com spreads de página dupla e navegação por teclado. Nada é hardcoded pra site nenhum: são heurísticas que detectam sequências de imagens e numeração de capítulos em qualquer URL. Virou meu jeito preferido de ler os mangás que não existem pra comprar na Amazon nem na Shueisha.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/frank-scanlation-library.webp" alt="Biblioteca do FRANK Scanlation: cards com capas, progresso de leitura e botão de continuar de onde parou."  loading="lazy" /></p>
<p>Instalação: AppImage, <code>.deb</code> e <code>.rpm</code> (e build de macOS) nas <a href="https://github.com/akitaonrails/frank_scanlation/releases"target="_blank" rel="noopener">releases do GitHub</a>.</p>
<h2>Frank Lyrics: decorando música linha por linha<span class="hx:absolute hx:-mt-20" id="frank-lyrics-decorando-música-linha-por-linha"></span>
    <a href="#frank-lyrics-decorando-m%c3%basica-linha-por-linha" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu já <a href="/2026/04/05/transformando-youtube-num-app-de-karaoke-frank-karaoke/">postei sobre o Frank Karaoke</a>, um appzinho pra adicionar pontuação em cima do YouTube e curtir karaokê com família e amigos. Mas tem um caso de uso irmão: eu gosto de aprender música nova (abertura de anime nova, por exemplo), e o processo de decorar letra é repetir a MESMA linha várias vezes. No player do YouTube isso é um inferno: fica caçando no dedo o ponto exato da timeline pra voltar, erra por dois segundos, volta demais, passa do ponto.</p>
<p>Pra resolver isso criei o <a href="https://github.com/akitaonrails/frank_lyrics"target="_blank" rel="noopener">Frank Lyrics</a>, uma extensão de Chrome simples que descobre o timing correto de cada linha da letra usando os marcadores públicos do <a href="https://lrclib.net/"target="_blank" rel="noopener">LRCLIB</a> e injeta controles na própria página do YouTube. O LRCLIB, pra quem não conhece, é uma biblioteca aberta e comunitária de letras sincronizadas: cada linha vem com timestamp (o formato LRC clássico dos players de música), com API pública, gratuita e sem chave.</p>
<p>A extensão casa esses timestamps com o vídeo e me dá botões de repetir a linha exata que eu estou decorando, quantas vezes eu precisar, sem caçar timeline. Se você curte karaokê, o combo Frank Karaoke + Frank Lyrics deve te atender bem.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/frank-lyrics.webp" alt="Painel do Frank Lyrics sobre um vídeo de letra no YouTube: cada linha da letra com seu timestamp e controles pra repetir a linha atual."  loading="lazy" /></p>
<p>Instalação: baixa o <code>.zip</code> das <a href="https://github.com/akitaonrails/frank_lyrics/releases"target="_blank" rel="noopener">releases</a>, descompacta, abre <code>chrome://extensions</code>, liga o modo desenvolvedor e faz &ldquo;Load unpacked&rdquo; apontando pra pasta.</p>
<h2>Frank Type: treino de digitação com prosa de verdade<span class="hx:absolute hx:-mt-20" id="frank-type-treino-de-digitação-com-prosa-de-verdade"></span>
    <a href="#frank-type-treino-de-digita%c3%a7%c3%a3o-com-prosa-de-verdade" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quem me acompanha há tempo sabe que eu amo meus teclados. Tem vídeos no canal Akitando sobre isso e <a href="/2024/10/24/meus-teclados-modernos-favoritos/">posts aqui no blog sobre meus teclados favoritos</a>. E de tempos em tempos eu gosto de treinar no MonkeyType. Mas eu não gosto de digitar palavras aleatórias. O TypeRacer resolve isso usando textos públicos de verdade, só que ele me obriga a esperar outros jogadores na sala (eu detesto jogos multiplayer). Ou seja: eu queria uma experiência single player, com prosa de verdade, mas com a estética do MonkeyType, que eu prefiro.</p>
<p>Então criei o <a href="https://github.com/akitaonrails/frank_type"target="_blank" rel="noopener">Frank Type</a>, um mini-clone dos dois: um treinador de digitação em Rails 8 que usa trechos normalizados de prosa em domínio público em vez de listas de palavras aleatórias. É local-first e sem cadastro: histórico de sessões, tempos e gráficos de perfil ficam no local storage do navegador. Tem até heat map de dígrafos pra mostrar quais combinações de teclas te atrasam.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/frank-type.webp" alt="Tela de prática do Frank Type: texto de domínio público pra digitar, com estatísticas e heat map de dígrafos, na estética do MonkeyType."  loading="lazy" /></p>
<p>Instalação: é um container Docker publicado como <code>akitaonrails/frank_type</code>. Um comando levanta em <code>http://localhost:3200</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -fsSL https://raw.githubusercontent.com/akitaonrails/frank_type/master/bin/docker-run.sh <span class="p">|</span> bash</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>distrobox-gaming: uma biblioteca de como instalar jogos no Linux<span class="hx:absolute hx:-mt-20" id="distrobox-gaming-uma-biblioteca-de-como-instalar-jogos-no-linux"></span>
    <a href="#distrobox-gaming-uma-biblioteca-de-como-instalar-jogos-no-linux" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu jogo no mesmo PC em que trabalho, mas me recuso a misturar pacote de emulador e gambiarra de Wine com meu ambiente de desenvolvimento. A solução foi isolar os jogos inteiros dentro de um <a href="https://github.com/89luca89/distrobox"target="_blank" rel="noopener">distrobox</a> (containers que se integram ao desktop como se fossem nativos), e o <a href="https://github.com/akitaonrails/distrobox-gaming"target="_blank" rel="noopener">distrobox-gaming</a> é o repositório de playbooks Ansible que constrói essa box Arch do zero: ES-DE de frontend, emuladores standalone, cores do RetroArch, configs por jogo. Meu Arch/Omarchy host fica limpo.</p>
<p>A regra que eu me impus: todo jogo novo que eu instalo tem que virar receita Ansible reproduzível. Confesso que ainda não testei a reconstrução completa do zero, mas a ideia é essa: qualquer truque ou hack necessário fica documentado no código em vez de perdido no histórico do shell.</p>
<p>E truque é o que não falta, porque eu ando instalando abandonware antigo: a série Colin McRae Rally (da versão de PS1 com patch de 60 fps até o DiRT de 2007), Sega Rally Revo, OutRun 2006 Coast 2 Coast, entre outros. Cada um com suas manhas de Wine, seus patches e suas configurações. Sem querer, o repositório está virando uma biblioteca de documentação sobre como instalar jogos, emuladores e retro games no Linux.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/07/12/probleminhas/distrobox-gaming-cmr04.webp" alt="Colin McRae Rally 04 rodando na box de gaming: a tela de Service Area com o Mitsubishi Lancer Evolution antes da próxima especial."  loading="lazy" /></p>
<p>Na mesma linha existe o <a href="https://github.com/akitaonrails/distrobox-llm"target="_blank" rel="noopener">distrobox-llm</a>, que faz o mesmo isolamento pro meu ambiente de LLMs locais (CUDA, Ollama, LM Studio e companhia), deixando só o driver NVIDIA no host.</p>
<p>Instalação: clona o repositório e roda os playbooks:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">cd</span> ansible
</span></span><span class="line"><span class="cl">ansible-galaxy collection install -r collections/requirements.yml
</span></span><span class="line"><span class="cl">cp host_vars/localhost.yml.example host_vars/localhost.yml
</span></span><span class="line"><span class="cl"><span class="nv">$EDITOR</span> host_vars/localhost.yml
</span></span><span class="line"><span class="cl">ansible-playbook site.yml</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>aitrepreneur-docker: ComfyUI sem sujar o sistema<span class="hx:absolute hx:-mt-20" id="aitrepreneur-docker-comfyui-sem-sujar-o-sistema"></span>
    <a href="#aitrepreneur-docker-comfyui-sem-sujar-o-sistema" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra geração de imagens eu gosto do ComfyUI, e <a href="/2025/04/20/entendendo-o-basico-de-comfyui-pra-gerar-imagens-com-i-a/">postei sobre ele ano passado</a>. Eu acompanho um youtuber chamado <a href="https://www.youtube.com/@Aitrepreneur"target="_blank" rel="noopener">Aitrepreneur</a> que mantém uma página no Patreon com receitas pra instalar e configurar o ComfyUI com os modelos e workflows mais recentes. O trabalho de curadoria dele é ótimo, mas as receitas são scripts <code>.sh</code> old school, pensados pra rodar one-shot numa instância do RunPod, por exemplo.</p>
<p>Agora que as LLMs evoluíram a ponto de escreverem Dockerfiles decentes, criei o <a href="https://github.com/akitaonrails/aitrepreneur-docker"target="_blank" rel="noopener">aitrepreneur-docker</a>, que converte esses scripts em imagens e containers Docker de verdade: versões pinadas, dados persistentes no host ou NAS, um comando pra subir ou atualizar cada app. Assim eu rodo tudo isolado na minha máquina, sem entulhar meu Linux com toneladas de pacotes Python e gambiarras que poderiam desestabilizar o sistema.</p>
<p>Dois avisos importantes. O crédito da seleção de stack, curadoria de modelos e workflows é todo do Aitrepreneur: o repositório só traduz o trabalho dele pra forma de Docker, e os scripts originais dele são conteúdo pago do Patreon que NÃO está incluído.</p>
<p>Se achar útil, <a href="https://www.patreon.com/c/aitrepreneur/home"target="_blank" rel="noopener">assine o Patreon dele</a> pra contribuir com a criação de conteúdo (é lá que você pega os arquivos originais). E o repositório não cobre tudo que ele já produziu, só as receitas mais recentes.</p>
<p>Instalação: <code>git clone</code>, <code>make setup</code> (cria o <code>.env</code> pra você colocar seu token da Hugging Face), <code>make build</code> e <code>make up</code>.</p>
<h2>E tem mais de onde vieram esses<span class="hx:absolute hx:-mt-20" id="e-tem-mais-de-onde-vieram-esses"></span>
    <a href="#e-tem-mais-de-onde-vieram-esses" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você é novo por aqui: isso é só a leva que ainda não tinha aparecido no blog. Tem muito mais projeto que eu já cobri em posts dedicados. O <a href="/2026/06/16/ai-memory-memoria-longo-prazo-karpathy-wiki-auto-aprendizado-hermes-projetos/">ai-memory</a> é o mais popular, mais usado e com mais contribuições de todos. O <a href="/2026/05/24/dicas-e-toolkit-de-ia-do-akita-ai-jail-ai-memory-ai-usagebar/">ai-jail</a>, que apareceu de novo no <a href="/2026/07/11/como-me-precaver-pros-meus-agentes-nao-apagarem-minhas-coisas/">post de ontem sobre agentes destrutivos</a>, foi o primeiro da série e é o segundo mais popular.</p>
<p>E tem os outros da série Frank (&ldquo;Frank&rdquo; em homenagem ao Frank Rosenblatt, criador do perceptron, a rede neural original; o nome marca os projetos experimentais que eu faço por diversão):</p>
<ul>
<li><a href="/2026/02/23/vibe-code-fiz-um-indexador-inteligente-de-imagens-com-ia-em-2-dias/">Frank Sherlock</a>: indexador inteligente de imagens</li>
<li><a href="/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/">Frank Mega</a>: clone do MEGA pro meu home server</li>
<li><a href="/2026/02/01/vibe-code-fiz-um-editor-de-markdown-do-zero-com-claude-code-frankmd-part-1/">FrankMD</a>: editor de markdown</li>
<li><a href="/2026/03/09/atacando-fraudes-via-email-frank-fbi/">Frank FBI</a>: analisador de fraudes por email</li>
<li><a href="/2026/03/27/ensinando-a-questionar-noticias-frank-investigator/">Frank Investigator</a>: ensina a questionar notícias</li>
</ul>
<p>Tudo é open source e está no <a href="https://github.com/akitaonrails"target="_blank" rel="noopener">meu GitHub</a>. Todo mundo é bem-vindo pra contribuir com issues, pull requests, ou simplesmente compartilhando com os amigos. Cada um desses projetos nasceu de um probleminha real meu. Vai que um deles resolve o seu também.</p>
]]></content:encoded><category>vibe-coding</category><category>open-source</category><category>inteligencia-artificial</category></item><item><title>Por que LLMs vão falhar na Sua Empresa?</title><link>https://www.akitaonrails.com/2026/06/24/por-que-llms-vao-falhar-na-sua-empresa/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/06/24/por-que-llms-vao-falhar-na-sua-empresa/</guid><pubDate>Wed, 24 Jun 2026 12:00:00 -0300</pubDate><description>&lt;p&gt;Este post é mais especulação do que tese fechada. Estou pensando alto. Pode estar incompleto, pode estar errado. Se você discordar, melhor ainda: responde nos comentários. Não estou tentando vender metodologia nova. Quero cutucar uma ferida que muita gente finge que não existe.&lt;/p&gt;
&lt;p&gt;Nos últimos meses começaram a aparecer histórias de empresas grandes gastando quantidades absurdas de tokens em ferramentas de IA sem conseguir apontar, com clareza, o resultado equivalente em produto. O caso que ficou mais famoso foi a &lt;a href="https://www.forbes.com/sites/janakirammsv/2026/05/17/uber-burns-its-2026-ai-budget-in-four-months-on-claude-code/"target="_blank" rel="noopener"&gt;Uber queimando o orçamento anual de IA em poucos meses&lt;/a&gt; com uso pesado de ferramentas como Claude Code, e executivos admitindo que ainda não conseguiam traçar uma linha reta entre consumo de tokens e valor entregue. Não é que ninguém tenha usado. Usaram demais. Esse é justamente o problema.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Este post é mais especulação do que tese fechada. Estou pensando alto. Pode estar incompleto, pode estar errado. Se você discordar, melhor ainda: responde nos comentários. Não estou tentando vender metodologia nova. Quero cutucar uma ferida que muita gente finge que não existe.</p>
<p>Nos últimos meses começaram a aparecer histórias de empresas grandes gastando quantidades absurdas de tokens em ferramentas de IA sem conseguir apontar, com clareza, o resultado equivalente em produto. O caso que ficou mais famoso foi a <a href="https://www.forbes.com/sites/janakirammsv/2026/05/17/uber-burns-its-2026-ai-budget-in-four-months-on-claude-code/"target="_blank" rel="noopener">Uber queimando o orçamento anual de IA em poucos meses</a> com uso pesado de ferramentas como Claude Code, e executivos admitindo que ainda não conseguiam traçar uma linha reta entre consumo de tokens e valor entregue. Não é que ninguém tenha usado. Usaram demais. Esse é justamente o problema.</p>
<p>Até aqui, acho que a maioria já entendeu que prompt de uma linha não resolve nada. Aquela fantasia de escrever &ldquo;faça um clone do Uber&rdquo; e voltar com um produto pronto morreu rápido. A resposta do mercado foi correr para outro lado: <strong>spec-driven development</strong>.</p>
<p>Em vez de pedir tudo em uma frase, você escreve uma especificação. Define escopo, casos de uso, critérios de aceite, arquitetura desejada, restrições, fluxos, talvez até um PRD inteiro. Depois entrega isso para o agente e espera que ele implemente com menos ambiguidade.</p>
<p>Isso é melhor do que one-shot? Claro. Mas ainda acho que está errado.</p>
<h2>O problema não é falta de especificação<span class="hx:absolute hx:-mt-20" id="o-problema-não-é-falta-de-especificação"></span>
    <a href="#o-problema-n%c3%a3o-%c3%a9-falta-de-especifica%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu venho martelando essa ideia há anos, antes de LLM virar moda. Dois vídeos meus são importantes pra entender o ponto: <a href="/2019/10/09/akitando-63-nao-terceirize-suas-decisoes-a-licao-mais-importante-da-sua-vida/">Não Terceirize Suas Decisões</a> e <a href="/2019/06/18/akitando-51-esqueca-metodologias-ageis-rated-r/">Esqueça Metodologias Ágeis</a>.</p>
<blockquote>
  <p>O resumo é simples: a maioria dos profissionais foi treinada a não decidir.</p>

</blockquote>
<p>Pensa no seu dia a dia. Muita gente não faz nada se não houver backlog pronto. Cruza os braços, culpa gestão, culpa colega, culpa cliente, e espera alguém dizer o que fazer. Quando a tarefa chega, executa. Quando dá errado, a culpa era do requisito. Quando o cliente reclama, a culpa era da prioridade. Quando o produto degrada, a culpa era do processo.</p>
<p>Quase ninguém decide de verdade, banca a decisão até o fim e sofre as consequências. E sem sofrer a consequência, você não aprende a decidir.</p>
<p>Você não aprende quais escolhas levam a quais resultados. Não aprende a priorizar. Não aprende a explicar por que algo precisa existir. Não aprende a escrever uma especificação porque entende o problema; aprende a preencher template para ninguém reclamar.</p>
<h2>O Agile virou burocracia estruturada<span class="hx:absolute hx:-mt-20" id="o-agile-virou-burocracia-estruturada"></span>
    <a href="#o-agile-virou-burocracia-estruturada" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Os princípios originais de 2001 foram triturados. O que muita empresa chama de Agile hoje é burocracia estruturada em ciclos menores.</p>
<p>O desenho clássico todo mundo já viu: backlog, sprint planning, sprint, daily, review, retrospective, próximo sprint. A <a href="https://scrumguides.org/scrum-guide.html"target="_blank" rel="noopener">Scrum Guide</a> descreve os eventos formais. Diagramas como o <a href="https://commons.wikimedia.org/wiki/File:Scrum_process.svg"target="_blank" rel="noopener">Scrum process da Wikimedia</a> mostram o círculo bonitinho. Na prática, muita equipe transformou isso em teatro.</p>
<p>Passa-se um dia inteiro debatendo o que entra no backlog. E o backlog final raramente é o que precisa ser feito. É o consenso de menor resistência. Compromisso em cima de compromisso. Pequenas tarefas, pequenos riscos, pequenas vitórias, pequenos fracassos. Tudo desenhado para não gerar conflito demais.</p>
<p>Isso não é apenas medíocre. É pior: é a escolha ativa por decisões fáceis, que não levantam muita crítica contra você.</p>
<p>Depois a equipe passa dias ou semanas implementando aquele backlog morno. O QA entra, encontra problemas, tenta encaixar correções na próxima rodada. Mais alguns dias, talvez mais duas semanas. Um mês depois, talvez dois, a decisão original finalmente começa a mostrar resultado em produção.</p>
<p>Só que o resultado está tão longe da decisão que quase ninguém faz a correlação. A equipe não sente o peso real do que decidiu. O erro não vira aprendizado. O processo não melhora. A parte mais importante de qualquer sistema de melhoria contínua, o <strong>kaizen</strong>, nunca acontece.</p>
<blockquote>
  <p>O que acontece é outra coisa: todo mundo aprende a ficar invisível.</p>

</blockquote>
<p>Você aprende a agradar gestão, não o produto. Aprende a reduzir atrito, não a resolver o problema. Aprende a sobreviver à reunião, não a entregar valor. E quando os resultados pioram, a resposta da alta gestão é previsível: mais controle, mais regra, mais gate, mais comitê, mais checklist. A engrenagem fica mais pesada. O time fica mais covarde.</p>
<h2>Então chega a IA<span class="hx:absolute hx:-mt-20" id="então-chega-a-ia"></span>
    <a href="#ent%c3%a3o-chega-a-ia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agora entra a LLM.</p>
<p>Até pouco tempo atrás, o maior gargalo de desenvolvimento era escrever e ajustar código. Codar levava dias ou semanas. Por isso tanto esforço era colocado na fase de planejamento. Se o time vai gastar duas semanas implementando, parece fazer sentido gastar um dia discutindo o que entra na sprint.</p>
<p>Mas ferramentas de coding com LLM mudam a posição do gargalo. Elas conseguem fazer uma parte grande do trabalho braçal: gerar estrutura, escrever testes básicos, fazer refactors, adaptar APIs, criar telas, preencher boilerplate, tentar correções. Não é perfeito, mas é rápido o bastante para mudar a economia do processo.</p>
<p>É aquela história da corrente e do elo mais fraco. Você reforça um elo, outro passa a limitar o sistema. Antes, o elo fraco era a velocidade de codificação. Agora, cada vez mais, é a capacidade humana de decidir, explicar, testar e revisar.</p>
<blockquote>
  <p>O erro das empresas é colocar LLM dentro do mesmo processo quebrado e esperar ganho multiplicativo.</p>

</blockquote>
<p>Você pega pessoas treinadas por anos a não decidir, entrega uma ferramenta que exige clareza de decisão, e se surpreende quando sai lixo mais rápido.</p>
<h2>A LLM reflete o usuário<span class="hx:absolute hx:-mt-20" id="a-llm-reflete-o-usuário"></span>
    <a href="#a-llm-reflete-o-usu%c3%a1rio" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Um agente de coding precisa que você explique o objetivo, o contexto, as restrições, as prioridades, o que não pode quebrar, quais caminhos são aceitáveis, como medir sucesso. Ele precisa de uma direção objetiva.</p>
<p>Agora vai entrevistar seu time. Pergunta para cada pessoa:</p>
<ul>
<li>qual é o objetivo real do que você está fazendo?</li>
<li>como isso melhora o produto?</li>
<li>qual métrica deveria mudar?</li>
<li>que trade-off você está aceitando?</li>
<li>o que não pode quebrar?</li>
<li>quais casos tristes precisam ser testados?</li>
</ul>
<p>Muita gente não sabe responder. Sabe qual ticket pegou. Sabe que precisa fechar algumas tarefas por semana. Sabe que tem daily amanhã. Mas não sabe por que aquilo importa.</p>
<p>E aí você entrega uma LLM para essa pessoa.</p>
<p>O resultado não é mágica. É espelho. Código ruim dez vezes mais rápido. Bug dez vezes mais rápido. Cliente irritado dez vezes mais rápido. Pull request gigante dez vezes mais rápido. O problema não é só a IA. É que a IA amplifica a falta de direção.</p>
<p>Spec-driven development tenta corrigir isso com documentação. Mas se a especificação nasce do mesmo processo covarde, ela só documenta a covardia. Um PRD escrito por quem não entende a decisão continua ruim. Critério de aceite escrito para evitar conflito continua desviando do problema real. O agente só vai executar melhor uma decisão ruim.</p>
<h2>O processo precisa mudar<span class="hx:absolute hx:-mt-20" id="o-processo-precisa-mudar"></span>
    <a href="#o-processo-precisa-mudar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Minha hipótese é que o processo inteiro precisa ser redesenhado ao redor da nova restrição. De novo: é especulação. Eu não tenho dados suficientes para provar isso. Mas, olhando para o tipo de falha que vejo em empresas e para o tipo de sucesso que vejo em uso individual intenso, minha teoria é essa:</p>
<blockquote>
  <p><strong>jogue fora o ritual de sprint como unidade central de trabalho.</strong></p>

</blockquote>
<p>Não faz sentido manter dias de planning, dias de review e semanas de codificação como se o gargalo ainda fosse o mesmo. Com LLM, o ciclo precisa ficar mais curto. Muito mais curto. Idealmente, dentro do mesmo dia.</p>
<p>No meu desenho ideal, cada programador com LLM deveria parear diretamente com um Product Owner por blocos de trabalho curtos. O papel do PO é fechar a lacuna que o programador normalmente tem: objetivo, prioridade, restrição, contexto de negócio, resultado esperado. Isso precisa entrar no prompt e na conversa com o agente.</p>
<p>Mas como codar deixou de ser o gargalo principal, o PO não precisa esperar duas semanas para ver resultado. Ele pode sentar com o programador durante uma manhã, implementar uma pequena fatia funcional, ajustar na hora, decidir na hora, cortar escopo na hora. Depois roda para outro programador.</p>
<p>Em seguida, entra QA. Não como uma fase distante, mas como pareamento no mesmo ciclo. O QA fecha outra lacuna comum do programador: testar só o caminho feliz, esquecer casos de erro, não automatizar o suficiente, não pensar em regressão de comportamento. A função do QA vira ajudar a transformar a implementação em algo verificável agora, enquanto o contexto ainda está quente.</p>
<p>Depois entra o Tech Lead. Também no mesmo dia, não duas semanas depois. Ele olha para o que mudou e fecha a terceira lacuna: complexidade desnecessária, design ruim, regressão escondida, documentação faltando, vulnerabilidade óbvia, performance piorando, acoplamento que nasceu sem necessidade.</p>
<p>Constrói, checa, ajusta. Tudo em tempo real.</p>
<figure>
  <a href="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/06/24/why-llms-fail/llm-pairing-rotation.svg">
    <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/06/24/why-llms-fail/llm-pairing-rotation.svg" alt="Diagrama comparando o fluxo comum de sprint, com decisão distante do resultado, contra um fluxo de pareamento rotativo com LLM envolvendo PO, programador, QA e Tech Lead no mesmo dia." />
  </a>
  <figcaption>O gargalo muda: menos tempo esperando uma sprint terminar, mais ciclos curtos de decisão, implementação, teste e revisão.</figcaption>
</figure>
<h2>O novo papel de cada pessoa<span class="hx:absolute hx:-mt-20" id="o-novo-papel-de-cada-pessoa"></span>
    <a href="#o-novo-papel-de-cada-pessoa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esse modelo cobra mais responsabilidade, não menos.</p>
<p>O programador deixa de ser a pessoa que só pega ticket. Ele precisa aprender a transformar objetivo em instrução executável. Precisa discutir trade-off. Precisa entender por que está codando. Precisa usar a LLM como ferramenta de execução e investigação, não como desculpa para desligar o cérebro.</p>
<p>O PO deixa de ser fábrica de backlog. Ele precisa decidir com o programador, olhando para o produto vivo. Não é escrever card e sumir. É ver a coisa nascer, ajustar, cortar, priorizar e aceitar consequência.</p>
<p>O QA deixa de ser a pessoa que recebe o pacote no fim e devolve defeito. Ele entra cedo para transformar comportamento esperado em verificação. Se a LLM acelera código, QA precisa acelerar aprendizado sobre risco.</p>
<p>O Tech Lead deixa de ser carimbo de PR atrasado. Ele entra enquanto ainda dá para mudar barato. Ajuda a manter simplicidade, remove complexidade, protege arquitetura, documentação, segurança e performance.</p>
<p>Nada disso funciona se todo mundo continuar usando IA como autocomplete caro dentro do mesmo Scrum burocrático.</p>
<h2>Por que empresas não vão gostar disso<span class="hx:absolute hx:-mt-20" id="por-que-empresas-não-vão-gostar-disso"></span>
    <a href="#por-que-empresas-n%c3%a3o-v%c3%a3o-gostar-disso" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Porque esse processo expõe decisões.</p>
<p>No processo atual, a decisão se dilui. Foi o backlog. Foi o PO. Foi o refinamento. Foi a sprint. Foi o time. Foi a dependência. Foi o cliente que mudou de ideia. Todo mundo consegue se esconder.</p>
<p>Num ciclo curto com LLM, a decisão aparece rápido. Você disse que aquilo era prioridade? Então implementa agora. Você disse que o critério de aceite era esse? Então testa agora. Você disse que esse design era bom? Então revisa agora. Se estiver errado, a consequência aparece no mesmo dia.</p>
<p>Isso é desconfortável. Mas é assim que se aprende.</p>
<p>Sem consequência próxima, decisão vira teatro. Com consequência próxima, decisão vira treino.</p>
<h2>Como implementar isso sem queimar dinheiro<span class="hx:absolute hx:-mt-20" id="como-implementar-isso-sem-queimar-dinheiro"></span>
    <a href="#como-implementar-isso-sem-queimar-dinheiro" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O primeiro passo é aceitar uma coisa que programador profissional costuma odiar: dá para jogar código fora.</p>
<blockquote>
  <p>Na verdade, você deveria jogar código fora com muito mais frequência.</p>

</blockquote>
<p>Código não é monumento. Código é material de trabalho. Deletar código é tão importante quanto escrever código novo. É por isso que usamos Git. A ideia sempre foi trabalhar em branches, experimentar caminhos, mudar de ideia, comparar alternativas, apagar o que não funcionou e manter só o que sobreviveu ao teste da realidade.</p>
<p>Só que, no processo tradicional, código fica caro demais emocionalmente. Se levou duas semanas para sair, ninguém quer admitir que foi desperdício. O gerente sente que apagar aquilo prova que a decisão anterior foi ruim. O programador defende a implementação porque sofreu para fazê-la. O time empurra o problema para frente porque deletar parece derrota. Não é. Às vezes é higiene.</p>
<p>Com LLM, esse custo muda. <strong>Fica barato errar</strong>. Fica barato prototipar. Fica barato fazer prova de conceito. Fica barato testar dois caminhos e jogar um fora. Fica barato pedir para o agente reescrever uma abordagem inteira numa branch temporária, comparar, documentar o aprendizado e dar <code>git reset</code> sem drama.</p>
<p>Então faça mais disso. Mais protótipos. Mais provas de conceito. Mais testes A/B internos. Mais branches descartáveis. Menos apego ao primeiro código que apareceu. LLM é muito boa nesse tipo de trabalho: explorar alternativas rápido, mostrar onde a especificação estava vaga e produzir material que você pode destruir sem culpa.</p>
<p>Mas não espere que todo mundo receba tokens de LLM numa segunda-feira e saiba fazer isso sozinho. Esse é o jeito mais rápido de queimar orçamento e depois dizer que IA não funciona.</p>
<p>Minha sugestão é o contrário: assuma uma ou duas semanas sem entrega planejada. Diga claramente que o objetivo não é produzir feature. É treinar o novo processo. Quebrar o ritual antigo. Deixar o time praticar em ambiente seguro. Se der errado, ótimo: reseta a branch, apaga o protótipo, documenta o que aprendeu e tenta de novo.</p>
<p>Nessa fase, o PO não deveria escrever especificação de vinte páginas. Deveria parear com o programador e construir prompts pequenos, um passo de cada vez, vendo o resultado em tempo real. A cada resposta da LLM, o PO aprende quanto detalhe o código realmente precisa. O programador aprende a perguntar melhor. QA aprende a transformar comportamento em teste cedo. Tech Lead aprende a cortar complexidade enquanto ela ainda é barata.</p>
<p>É assim que se adota LLM com alguma chance de dar certo: como mudança de processo, não como licença cara de autocomplete.</p>
<h2>A conclusão incômoda<span class="hx:absolute hx:-mt-20" id="a-conclusão-incômoda"></span>
    <a href="#a-conclus%c3%a3o-inc%c3%b4moda" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><blockquote>
  <p>LLMs falham tanto porque elas entram em processos feitos por pessoas que foram treinadas a não decidir.</p>

</blockquote>
<p>Não é só falta de prompt melhor. Não é só falta de spec maior. Não é só falta de governança de token. Tudo isso ajuda, mas não toca o centro do problema.</p>
<p>O centro é que muita gente não sabe explicar objetivo mensurável, não sabe priorizar, não sabe assumir trade-off, não sabe conectar decisão com resultado. E uma LLM precisa exatamente disso para ser útil.</p>
<p>Se você coloca IA no processo velho, você acelera o processo velho. Mais backlog medíocre. Mais decisão ruim. Mais bug. Mais custo. Mais rework. Mais relatório tentando provar produtividade que o cliente nunca sentiu.</p>
<p>Se você quer que LLM funcione, precisa redesenhar o processo ao redor dela. Ciclos menores. Pareamento real. Decisão perto da execução. Teste perto da decisão. Revisão perto do código. Resultado perto de quem decidiu.</p>
<p>Essa é a parte que programador precisa encarar: sim, você provavelmente nunca aprendeu a decidir direito. Nunca aprendeu a descrever objetivo de forma objetiva. Nunca aprendeu a medir consequência. O processo te treinou para isso. E agora a IA está revelando a falha.</p>
<p>O lado bom é que também dá para treinar o contrário.</p>
<p>Mas não vai acontecer com mais uma cerimônia, mais uma planilha de prompt, mais uma política de uso de IA, ou mais uma sprint cheia de tickets escritos para ninguém ser criticado.</p>
<p><strong>Vai acontecer quando você parar de terceirizar decisão.</strong></p>
]]></content:encoded><category>llms</category><category>vibe-coding</category><category>engenharia-de-software</category></item><item><title>Terminando Minha Maratona de IA: Sucesso ou Fracasso?</title><link>https://www.akitaonrails.com/2026/05/14/terminando-maratona-ia-sucesso-ou-fracasso/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/05/14/terminando-maratona-ia-sucesso-ou-fracasso/</guid><pubDate>Thu, 14 May 2026 14:00:00 -0300</pubDate><description>&lt;p&gt;Acho que finalmente saí do meu hiperfoco em IA. Quando comecei a acelerar em fevereiro e fui até quase maio, o objetivo era responder uma pergunta simples: dá ou não dá pra usar a nova geração de frontier LLMs da Anthropic e da OpenAI pra codar 100% do tempo? Resposta curta: dá.&lt;/p&gt;
&lt;p&gt;Em vez de fazer um demo de 1 dia e arriscar um &amp;ldquo;eu acho que sim&amp;rdquo;, resolvi ir all-in: mais de 500 horas, quase meio milhão de linhas de código e documentação, 24 repositórios entre públicos e privados.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Acho que finalmente saí do meu hiperfoco em IA. Quando comecei a acelerar em fevereiro e fui até quase maio, o objetivo era responder uma pergunta simples: dá ou não dá pra usar a nova geração de frontier LLMs da Anthropic e da OpenAI pra codar 100% do tempo? Resposta curta: dá.</p>
<p>Em vez de fazer um demo de 1 dia e arriscar um &ldquo;eu acho que sim&rdquo;, resolvi ir all-in: mais de 500 horas, quase meio milhão de linhas de código e documentação, 24 repositórios entre públicos e privados.</p>
<p>Mas hoje me chamaram atenção pra um ponto que eu não imaginei que fosse gerar dúvida:</p>
<blockquote>
  <p>&ldquo;Todos os 24 projetos são perfeitos, modelos de excelência de código?&rdquo; — <strong>ABSOLUTAMENTE NÃO.</strong></p>

</blockquote>
<p>Achei que isso fosse óbvio. Pra começo de conversa, se eu achasse que qualquer um deles fosse tão bom assim, já estaria abrindo uma startup em cima. Não me falta recurso pra investir. Se não estou fazendo isso, obviamente é tudo em ritmo de hobby.</p>
<p>Segundo ponto óbvio: se eu pulo de um projeto pra outro em poucos dias, é porque perdi o interesse. Se gostasse muito, estaria ajustando até hoje. Sempre falei nos vídeos: software nunca &ldquo;acaba&rdquo;. Quando para, morre.</p>
<p>O objetivo era testar tipos diferentes de projetos pra ver como as LLMs se saíam em cada caso. Por isso tem projeto em Ruby on Rails, Rust, Go, Python, Crystal. Mais frameworks como Tauri e Flutter. Eu achava, por exemplo, que Flutter sendo bem popular, as LLMs não teriam muito trabalho. Tiveram. Conseguem avançar, só não tão suave quanto em Rails. Tauri foi &ldquo;ruim&rdquo; também. Crystal foi BEM ruim.</p>
<p>Terceiro ponto: todo projeto experimental virou &ldquo;Frank&rdquo;. Sarcasmo, claro. Não tenho paciência pra inventar nome bonito num projeto que sei de antemão que vai ser one-off.</p>
<p>Eu uso meu GitHub basicamente como repositório de exercícios e experimentos. Olhem todos os repos do passado: quase nenhum é &ldquo;projeto&rdquo; de verdade que eu queira manter. Faço o que eu prego nos vídeos: pra aprender, exercite-se em MUITOS projetos, mesmo que a maioria seja pra JOGAR FORA. Ninguém aprende tentando fazer um único projeto perfeito. Veja meus vídeos <a href="/2020/05/15/a-dor-de-aprender/">A Dor de Aprender</a> ou o <a href="/2020/05/16/guia-definitivo-de-aprendendo-a-aprender/">Guia Definitivo de Aprendendo a Aprender</a>.</p>
<p>No meio de vários projetos meia-boca, alguns emergem naturalmente como os que eu gosto e mantenho. Na prática joguei todos contra a parede pra ver quais grudam. Os melhores naturalmente atraíram mais contribuidores também. Não fico vendo Issues e PRs todos os dias. Vejo uma vez por semana. Há uns dois ou três dias dei uma passada e fechei tudo que estava pendente, por exemplo. Mesmo nos projetos que não gosto tanto, se chega PR, eu reviso e aceito. Não foi útil pra mim, mas pode ser útil pra outra pessoa.</p>
<h2>O ranking dos projetos<span class="hx:absolute hx:-mt-20" id="o-ranking-dos-projetos"></span>
    <a href="#o-ranking-dos-projetos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra ficar claro, segue um ranking dos projetos com comentário em cada um. As contagens de PR abaixo são não-dependabot (bump de gem não conta). A nota é minha estimativa de &ldquo;quão estável e usável isso está no estado atual&rdquo;.</p>
<h3>Tier de cima (7-8/10)<span class="hx:absolute hx:-mt-20" id="tier-de-cima-7-810"></span>
    <a href="#tier-de-cima-7-810" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong><a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a> — 8/10.</strong> O único que está bem próximo de uma release estável 1.0. Foi o que teve mais tração: 19 PRs fechadas, 23 Issues resolvidas. Recomendo usar, funciona muito bem.</p>
<p><strong>akitando-news (the M.Akita Chronicles) — 8/10.</strong> Foi o que fiz com mais cuidado, no modo &ldquo;Agile Vibe Coding&rdquo; que descrevi alguns posts atrás. E é o que eu mexo com mais frequência. Sempre tem coisinha pra ajustar, coisas que vou colocando, tirando, experimentando. Tanto que está funcionando toda semana. Por isso também: é fechado e não pretendo abrir.</p>
<p><strong><a href="https://github.com/akitaonrails/akitaonrails.github.io"target="_blank" rel="noopener">akitaonrails.github.io</a> — 7/10.</strong> É este blog. Mexo com frequência e já fechei umas 7 Issues e 30 PRs de contribuidores. Continuo ajustando aos poucos.</p>
<p><strong><a href="https://github.com/akitaonrails/FrankMD"target="_blank" rel="noopener">FrankMD</a> — 7/10.</strong> Foi um dos primeiros que fiz, sem critério nenhum: só queria um editor que tivesse upload automático de imagem pro meu S3. Hoje uso Claude Code pra editar os posts, então só quem contribui é quem usa. Esse teve bastante tração, fechei 17 PRs e 29 Issues. Não é perfeito, mas está bom pra usar, pelo menos foi testado o suficiente.</p>
<p><strong><a href="https://github.com/akitaonrails/easy-ffmpeg"target="_blank" rel="noopener">easy-ffmpeg</a> — 7/10.</strong> Projetinho pequeno, mas esse eu uso de fato nos vídeos. Meu OBS sempre grava em MKV e, quando quero compartilhar, converto pra MP4. Casos de uso pequenos assim. Está bom o suficiente pra usar.</p>
<h3>Tier do meio (5/10)<span class="hx:absolute hx:-mt-20" id="tier-do-meio-510"></span>
    <a href="#tier-do-meio-510" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong><a href="https://github.com/akitaonrails/distrobox-gaming"target="_blank" rel="noopener">distrobox-gaming</a> — 5/10.</strong> É o mais recente, serve como &ldquo;dump&rdquo; de documentação dos meus retrogames. Uso como bloco de anotações e tem funcionado bem pro meu setup. Não estou preocupado que funcione pra mais ninguém. Quem quiser, manda PR.</p>
<p><strong><a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">llm-coding-benchmark</a> — 5/10.</strong> Esse projeto não roda sozinho. Eu mando o Claude Code executar ele. O objetivo é tabular dados pra escrever os artigos sobre LLMs novas. O código deve estar BEM ruim.</p>
<p><strong><a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener">FrankMega</a> — 5/10.</strong> É o mais próximo de um exercício &ldquo;todo list&rdquo;. Eu só precisava compartilhar um arquivo grande com um amigo e pensei &ldquo;hum, aposto que dá pra fazer um mini-mega em 1 dia&rdquo;. Foi isso. O nome é brincadeira, de jeito nenhum chega perto do Mega de verdade, óbvio.</p>
<p><strong><a href="https://github.com/akitaonrails/distrobox-llm"target="_blank" rel="noopener">distrobox-llm</a> — 5/10.</strong> Mais organização pessoal do meu PC: queria separar as ferramentas de IA num distrobox pra não ficar atualizando pacote grande toda hora.</p>
<h3>Tier de baixo (3-4/10)<span class="hx:absolute hx:-mt-20" id="tier-de-baixo-3-410"></span>
    <a href="#tier-de-baixo-3-410" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong><a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">FrankYomik</a> — 4/10.</strong> Essa era uma ideia na minha cabeça há anos. Só queria ver se o conceito funcionava. Funciona. Mas rodar LLMs locais pra isso é lento e não tão prático quanto eu gostaria. É o único que ainda devo voltar pra mexer mais, porque realmente quero usar. No estado atual, está em &ldquo;funciona, mais ou menos&rdquo;.</p>
<p><strong><a href="https://github.com/akitaonrails/NW-Omarchy"target="_blank" rel="noopener">NW-Omarchy</a> — 4/10.</strong> Outro experimento de &ldquo;será que dá?&rdquo;, que durou 2 dias. O conceito funcionou, mas não pretendo fazer muito mais que isso. Fica pra quem tiver interesse de continuar. Na prática eu quis testar se em X11 ficaria MUITO mais ruim que em Wayland. O gap é menor do que eu esperava, mas realmente não me vejo usando no dia a dia. Como já falei, pro nicho de pessoas que não tem como usar Wayland, é uma boa alternativa.</p>
<p><strong><a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener">frank_investigator</a> — 3/10.</strong> Comecei, mas não tive saco pra continuar. Plantei as sementes da ideia. Se ninguém mandar PR, vai evoluir pouco. Precisa de mais pesquisa e muito mais teste.</p>
<p><strong><a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">frank_fbi</a> — 3/10.</strong> Mais um que quis testar o conceito, sem uso pessoal — nunca caí em phishing. Não sei pra onde levaria isso ainda.</p>
<p><strong><a href="https://github.com/akitaonrails/frank_karaoke"target="_blank" rel="noopener">frank_karaoke</a> — 3/10.</strong> Nasceu de uma conversa de fim de semana com meus irmãos. Mais uma tentativa de usar Flutter (e ver como tem limitação tanto no Flutter quanto nas LLMs usando Flutter). Serviu pra eu aprender sobre o ecossistema de karaokê e descobrir que pontuação de verdade só com metadado específico por música. Então não avancei muito além de brinquedo.</p>
<h3>Fundo do poço (1-2/10)<span class="hx:absolute hx:-mt-20" id="fundo-do-poço-1-210"></span>
    <a href="#fundo-do-po%c3%a7o-1-210" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong><a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener">FrankSherlock</a> — 2/10.</strong> Eu mesmo não uso. Por isso deve estar cheio de bug que precisa de correção. Foi teste pra ver se dava pra fazer um app Tauri, mas perdi o interesse rápido. Ainda dá pra melhorar, mas precisa de muito mais horas de uso, testes e correções. Foi a primeira vez que usei Tauri, então essa parte deve estar bem ruim também.</p>
<p><strong><a href="https://github.com/akitaonrails/FrankClaw"target="_blank" rel="noopener">FrankClaw</a> — 1/10.</strong> Eu mesmo não uso, e esse é o único que eu nem rodei uma única vez. O exercício foi só ver a dificuldade de traduzir o openclaw original de TypeScript pra Rust. Mas, como já postei várias vezes, não vejo utilidade no openclaw pra mim. Quando terminei a versão 0.1, parei. Ainda assim teve 6 PRs que fechei. Se alguém estiver interessado, pode contribuir.</p>
<p><strong><a href="https://github.com/akitaonrails/shadPS4"target="_blank" rel="noopener">shadPS4</a> — 1/10.</strong> Apesar de ter gerado muito código e muita documentação, nada foi pensado em virar contribuição. Foi exclusivamente aprendizado e pra resolver um problema muito específico. Deixei aberto pra mostrar como código puramente de material de estudo se parece.</p>
<p><strong><a href="https://github.com/akitaonrails/easy-subtitle"target="_blank" rel="noopener">easy-subtitle</a> — 1/10.</strong> Comecei, perdi o interesse, nunca parei pra testar de verdade. Só dei push e esqueci.</p>
<h3>Os privados<span class="hx:absolute hx:-mt-20" id="os-privados"></span>
    <a href="#os-privados" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Além do akitando-news, tenho no meu Gitea privado coisas como <code>my-omarchy-config</code>, <code>emulators-reorg-2026</code>, <code>thinkpad-omarchy-config</code>, <code>homelab-docs</code>, <code>akitando_english_translation</code> etc. Não vou descrever porque vocês nunca vão ver. Servem como &ldquo;scratchpads&rdquo; pro Claude Code administrar minhas máquinas.</p>
<h2>O que esse ranking diz<span class="hx:absolute hx:-mt-20" id="o-que-esse-ranking-diz"></span>
    <a href="#o-que-esse-ranking-diz" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Notem: não tem nenhum projeto que eu fiz que considero 10/10.</p>
<p>Pra um projeto sair de hobby e virar projeto de verdade, precisa de usuários e feedback. Se ninguém usa, sempre vai ter dezenas de bug funcional que nunca vai ser corrigido, porque ninguém esbarrou neles. Projeto sem usuário está morto.</p>
<p>Por isso eu repito nos vídeos: não existe planejar em detalhes, escrever um monte de código, e uma hora &ldquo;terminar&rdquo;. Não. Quando você acha que &ldquo;terminou&rdquo;, na verdade é a versão 0.1. Aí começam os meses de experimentação com usuário real reportando bug. Sair corrigindo. Dessas correções saem refatoramentos, mudanças de arquitetura porque as premissas iniciais estavam erradas comparado ao uso real. Software NUNCA acaba.</p>
<h2>O resumo do aprendizado dessa maratona<span class="hx:absolute hx:-mt-20" id="o-resumo-do-aprendizado-dessa-maratona"></span>
    <a href="#o-resumo-do-aprendizado-dessa-maratona" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>16 horas por dia, todos os dias, vibe coding, sempre vai sair com slop. Não tem jeito. Não adianta forçar quantidade. Tem que revisar manualmente, commit a commit, PR a PR. Por que eu não fiz isso em todos? Simples: não estou sendo pago. Fiz em uns 2 ou 3 projetos. O resto eu só queria testar o conceito e seguir pro próximo.</p>
<p>A forma correta é o que reportei vários posts atrás: &ldquo;Agile&rdquo; Vibe Coding. É meu jeito sarcástico de dizer &ldquo;use XP e técnicas normais de engenharia de software&rdquo;. Métricas, monitoramento, CI, etc. etc. Nada disso mudou. Sem isso, vai acabar como vários dos meus exercícios: one-off pra jogar fora.</p>
<p>Não tenha vergonha de subir projeto &ldquo;ruim&rdquo;. O que eu não pensei é que tem gente que assume que, só porque subi código, eu acho que é perfeito. Mas isso é o que essas pessoas fazem: acham que só vale a pena subir código bem feito, por isso nunca sobem nada. Eu já disse VÁRIAS vezes e demonstro: não ligo pro meu código, não tenho apreço pelo meu código, GitHub é diretório temporário de dump de qualquer coisa. Os que eu realmente gosto, normalmente estão privados.</p>
<p>Projeto realmente bom não basta &ldquo;dizer&rdquo; que é bom. Coloque seu dinheiro onde está sua boca: abra um negócio em cima, cobre dos usuários, entregue qualidade. Eu estou aposentado, não preciso cobrar nada, mas também não tenho vontade de trabalhar de graça. No momento que o código parou de me divertir, eu abandono e pulo pro próximo. Os que me interessaram mais (ai-jail, M.Akita Chronicles) continuo com mais frequência. Simples assim.</p>
<p>Como eu esperava, e como mostrei tanto nesses projetinhos quanto no benchmark, a escolha é simples: use GPT Codex ou Claude Code num dos planos subsidiados. Não pague por token. É onde está a produtividade 5x (máximo 10x, com slop junto). Sim, tem Kimi v2.6 ou DeepSeek V4 Pro. Mas adianto: os projetinhos abaixo de nota 5 virariam projetinhos abaixo de nota 2 com eles.</p>
<h2>O ponto que mais importa<span class="hx:absolute hx:-mt-20" id="o-ponto-que-mais-importa"></span>
    <a href="#o-ponto-que-mais-importa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>IA não substitui um bom programador. IA reflete e acelera quem você é. Se é um bom programador, a IA acelera bom código. Se é um mau programador, a IA acelera mau código.</p>
<p>Foi exatamente o que eu mostrei: a maior parte dos projetos que eu fiz eram one-off e teste de conceito. Mau código, sem interesse de continuar. A IA acelerou eu terminar esses testes de forma meia-boca. Pra mim está ótimo: sem a IA eu nem teria começado. Agora que vi como fica, posso riscar da minha lista e seguir em frente.</p>
<p>O que importa é qual é seu OBJETIVO. Se for fazer um negócio e cobrar de gente, tem que ter 10x mais cuidado do que eu tive. Tudo que eu faço é pra jogar fora. Não preciso ganhar dinheiro com nenhum desses projetos. Não assumam coisa que eu nunca falei.</p>
]]></content:encoded><category>vibe-coding</category><category>agentes-de-codigo</category><category>engenharia-de-software</category></item><item><title>Portando 10 Mil Linhas de Python pra Crystal com Claude: easy-subtitle</title><link>https://www.akitaonrails.com/2026/03/07/portando-10-mil-linhas-de-python-pra-crystal-com-claude-easy-subtitle/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/03/07/portando-10-mil-linhas-de-python-pra-crystal-com-claude-easy-subtitle/</guid><pubDate>Sat, 07 Mar 2026 22:00:00 -0300</pubDate><description>&lt;p&gt;No &lt;a href="https://www.akitaonrails.com/2026/03/07/crystal-e-um-wrapper-inteligente-pro-ffmpeg-feito-em-3-horas-easy-ffmpeg/"&gt;artigo anterior&lt;/a&gt; eu mostrei por que escolhi Crystal pra CLIs de linha de comando. Nesse artigo eu quero mostrar um caso mais ambicioso. Não é mais uma ferramenta do zero &amp;ndash; é um port feature-parity de um projeto open source de 10 mil linhas de Python.&lt;/p&gt;
&lt;h2&gt;O Problema: Legendas&lt;span class="hx:absolute hx:-mt-20" id="o-problema-legendas"&gt;&lt;/span&gt;
&lt;a href="#o-problema-legendas" class="subheading-anchor" aria-label="Permalink for this section"&gt;&lt;/a&gt;&lt;/h2&gt;&lt;p&gt;Quem mantém uma coleção de filmes e séries sabe a dor. Você baixa o arquivo MKV, mas a legenda embutida está dessincronizada. Ou pior: não tem legenda nenhuma no idioma que você quer. Aí você vai no OpenSubtitles, baixa uma legenda, e ela tá 3 segundos adiantada porque foi feita pra outro release. O fluxo manual é:&lt;/p&gt;</description><content:encoded><![CDATA[<p>No <a href="/2026/03/07/crystal-e-um-wrapper-inteligente-pro-ffmpeg-feito-em-3-horas-easy-ffmpeg/">artigo anterior</a> eu mostrei por que escolhi Crystal pra CLIs de linha de comando. Nesse artigo eu quero mostrar um caso mais ambicioso. Não é mais uma ferramenta do zero &ndash; é um port feature-parity de um projeto open source de 10 mil linhas de Python.</p>
<h2>O Problema: Legendas<span class="hx:absolute hx:-mt-20" id="o-problema-legendas"></span>
    <a href="#o-problema-legendas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Quem mantém uma coleção de filmes e séries sabe a dor. Você baixa o arquivo MKV, mas a legenda embutida está dessincronizada. Ou pior: não tem legenda nenhuma no idioma que você quer. Aí você vai no OpenSubtitles, baixa uma legenda, e ela tá 3 segundos adiantada porque foi feita pra outro release. O fluxo manual é:</p>
<ol>
<li>Extrair tracks de legenda do MKV com <code>mkvextract</code></li>
<li>Ir no OpenSubtitles, procurar legenda pro filme</li>
<li>Baixar, testar, ver que tá fora de sync</li>
<li>Rodar algum tool de sync (ffsubsync, alass)</li>
<li>Renomear o arquivo, mover pro lugar certo</li>
<li>Repetir pra cada idioma, pra cada filme</li>
</ol>
<p>Pra quem tem 10 filmes, é tolerável. Pra quem tem centenas, é insano. É exatamente o tipo de coisa que deveria ser automatizada.</p>
<h2>Subservient: A Solução Python<span class="hx:absolute hx:-mt-20" id="subservient-a-solução-python"></span>
    <a href="#subservient-a-solu%c3%a7%c3%a3o-python" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pesquisando o que já existia, encontrei o <a href="https://github.com/N3xigen/Subservient"target="_blank" rel="noopener">Subservient</a>. É um projeto Python que automatiza exatamente esse fluxo: extrai legendas de MKVs, baixa do OpenSubtitles via API REST, sincroniza com ffsubsync, e limpa propagandas dos arquivos SRT.</p>
<p>O projeto é completo. Tem modo filme e modo série, smart sync (testa todos os candidatos em paralelo e pega o melhor) e first-match (para no primeiro que funcionar). Usa o hash do OpenSubtitles pra matching exato e limpa watermarks e propagandas com mais de 30 regex patterns.</p>
<p>Mas tem os problemas típicos de distribuição Python:</p>
<ul>
<li><strong>7 dependências pip</strong>: colorama, requests, langdetect, ffsubsync, platformdirs, pycountry, tqdm</li>
<li><strong>ffsubsync como engine de sync</strong>: que por sua vez depende de numpy, auditok, e mais um monte de pacote Python</li>
<li><strong>Interface interativa com menus</strong>: boa pra uso manual, péssima pra scriptabilidade</li>
<li><strong>Config em formato INI</strong>: não é o fim do mundo, mas YAML é mais ergonômico</li>
<li><strong>10.220 linhas em 6 arquivos Python</strong>: arquivos de 2.700 linhas com funções de centenas de linhas cada</li>
</ul>
<p>O ponto não é que Python seja ruim pra isso. O Subservient funciona. Mas instalar e manter em produção é outra história. Se você quer rodar num server headless, precisa de Python 3.8+, pip, virtualenv (ou vai poluir o sistema), e torcer pra nenhuma dependência quebrar com a próxima atualização do OS.</p>
<h2>O Experimento: Port pra Crystal com Claude<span class="hx:absolute hx:-mt-20" id="o-experimento-port-pra-crystal-com-claude"></span>
    <a href="#o-experimento-port-pra-crystal-com-claude" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui é onde a coisa fica interessante. Eu queria testar uma hipótese: o Claude consegue pegar um projeto open source grande, entender a arquitetura, e fazer um port completo pra outra linguagem?</p>
<p>Não estou falando de traduzir arquivo por arquivo. Estou falando de entender o que o projeto faz, redesenhar a arquitetura onde faz sentido, e gerar código idiomático em Crystal.</p>
<p>O que eu fiz:</p>
<ol>
<li>Pedi pro Claude clonar e analisar o repositório do Subservient</li>
<li>Expliquei as decisões de design: usar <a href="https://github.com/kaegi/alass"target="_blank" rel="noopener">alass</a> (binário Rust, sem dependências Python) no lugar do ffsubsync, subcomandos CLI ao invés de menus interativos, YAML ao invés de INI</li>
<li>Pedi port feature-parity, com testes</li>
</ol>
<p>O alass é um detalhe importante. O ffsubsync funciona bem, mas é um pacote Python que puxa numpy e faz análise de áudio. O alass faz a mesma coisa (sincronização de legendas por análise de timing), mas é um binário Rust standalone. Trocar um pelo outro elimina a maior dependência Python do stack.</p>
<h2>O Resultado: easy-subtitle<span class="hx:absolute hx:-mt-20" id="o-resultado-easy-subtitle"></span>
    <a href="#o-resultado-easy-subtitle" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Cinco commits. Menos de 40 minutos do primeiro ao último.</p>
<table>
  <thead>
      <tr>
          <th>Commit</th>
          <th>Hora</th>
          <th>O que fez</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Initial implementation</td>
          <td>21:47</td>
          <td>Port completo: 42 arquivos src, 16 arquivos de teste, CI, install script</td>
      </tr>
      <tr>
          <td>Track shard.lock</td>
          <td>21:56</td>
          <td>Lock de dependências pra builds reproduzíveis</td>
      </tr>
      <tr>
          <td>Prefer ~/.local/bin</td>
          <td>22:03</td>
          <td>Fix no install script</td>
      </tr>
      <tr>
          <td>Add doctor command</td>
          <td>22:20</td>
          <td>Novo comando <code>doctor</code> pra validar setup + bump v0.2.0</td>
      </tr>
      <tr>
          <td>Homebrew formula</td>
          <td>22:24</td>
          <td>Suporte a <code>brew install</code> e workflow de auto-update</td>
      </tr>
  </tbody>
</table>
<p>O primeiro commit já entrega o projeto funcional: 8 comandos CLI, cliente OpenSubtitles com rate limiting, 76 testes passando, GitHub Actions com CI e release pra Linux e macOS.</p>
<h3>Números<span class="hx:absolute hx:-mt-20" id="números"></span>
    <a href="#n%c3%bameros" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><table>
  <thead>
      <tr>
          <th>Métrica</th>
          <th>Subservient (Python)</th>
          <th>easy-subtitle (Crystal)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Código fonte</td>
          <td>10.220 linhas (6 arquivos)</td>
          <td>2.516 linhas (42 arquivos)</td>
      </tr>
      <tr>
          <td>Testes</td>
          <td>0</td>
          <td>800 linhas (76 specs)</td>
      </tr>
      <tr>
          <td>Dependências runtime</td>
          <td>7 pacotes pip + ffsubsync</td>
          <td>0 (só webmock pra testes)</td>
      </tr>
      <tr>
          <td>Binário</td>
          <td>n/a (precisa Python + deps)</td>
          <td>~6MB estático</td>
      </tr>
      <tr>
          <td>Config</td>
          <td>INI</td>
          <td>YAML</td>
      </tr>
      <tr>
          <td>Sync engine</td>
          <td>ffsubsync (Python)</td>
          <td>alass (Rust)</td>
      </tr>
      <tr>
          <td>UI</td>
          <td>Menu interativo</td>
          <td>Subcomandos CLI</td>
      </tr>
      <tr>
          <td>Concorrência</td>
          <td>ThreadPoolExecutor</td>
          <td>Crystal fibers + channels</td>
      </tr>
  </tbody>
</table>
<p>A diferença de LOC é gritante: 10.220 vs 2.516. Mas isso não é mérito só do Crystal. O Python original tem arquivos monolíticos de milhares de linhas, com muita duplicação e lógica de UI misturada com lógica de negócio. O port separa as responsabilidades em módulos pequenos e focados.</p>
<h3>Arquitetura do Port<span class="hx:absolute hx:-mt-20" id="arquitetura-do-port"></span>
    <a href="#arquitetura-do-port" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>easy-subtitle/
  src/easy_subtitle/
    cli/           # Router &#43; 9 comandos (init, extract, download, sync, run, clean, scan, hash, doctor)
    core/          # Mapa de idiomas, parser/writer/cleaner de SRT, scanner de vídeo
    acquisition/   # Cliente API OpenSubtitles, auth, busca, download, movie hash
    extraction/    # Parsing de tracks MKV, extração, remuxing
    synchronization/  # Runner do alass, cálculo de offset, estratégias smart/first-match
    models/        # VideoFile, SubtitleCandidate, CoverageEntry</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada módulo tem uma responsabilidade clara. O maior arquivo tem 144 linhas (config). No Python original, o <code>acquisition.py</code> sozinho tem 2.726 linhas.</p>
<h3>O Que Cada Comando Faz<span class="hx:absolute hx:-mt-20" id="o-que-cada-comando-faz"></span>
    <a href="#o-que-cada-comando-faz" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Gerar config padrão</span>
</span></span><span class="line"><span class="cl">easy-subtitle init
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Extrair legendas de dentro de MKVs</span>
</span></span><span class="line"><span class="cl">easy-subtitle extract /path/to/movies
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Baixar legendas do OpenSubtitles</span>
</span></span><span class="line"><span class="cl">easy-subtitle download -l en,pt /path/to/movies
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Sincronizar legendas baixadas com o vídeo</span>
</span></span><span class="line"><span class="cl">easy-subtitle sync /path/to/movies
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pipeline completo: extract → download → sync</span>
</span></span><span class="line"><span class="cl">easy-subtitle run /path/to/movies
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Limpar propagandas/watermarks de SRTs</span>
</span></span><span class="line"><span class="cl">easy-subtitle clean /path/to/subtitles
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Ver cobertura de legendas por idioma</span>
</span></span><span class="line"><span class="cl">easy-subtitle scan --json /path/to/movies
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Computar hash OpenSubtitles (debug)</span>
</span></span><span class="line"><span class="cl">easy-subtitle <span class="nb">hash</span> /path/to/movie.mkv
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Validar setup: config, credenciais, dependências</span>
</span></span><span class="line"><span class="cl">easy-subtitle doctor</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>doctor</code> é um comando que eu adicionei depois. Ele verifica se o config existe, se a API key está configurada, testa login na API, e checa se <code>mkvmerge</code>, <code>mkvextract</code> e <code>alass</code> estão no PATH. Mostra instruções de instalação específicas pro OS quando algo falta.</p>
<h3>Smart Sync com Fibers<span class="hx:absolute hx:-mt-20" id="smart-sync-com-fibers"></span>
    <a href="#smart-sync-com-fibers" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O smart sync é a parte que eu mais gostei de ver o port. No Python original, ele usa <code>ThreadPoolExecutor</code> pra rodar múltiplos candidatos em paralelo. Em Crystal, a mesma lógica fica mais natural com fibers e channels:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-crystal" data-lang="crystal"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="n">candidates</span> <span class="p">:</span> <span class="nb">Array</span><span class="p">(</span><span class="n">Path</span><span class="p">),</span> <span class="n">video</span> <span class="p">:</span> <span class="n">VideoFile</span><span class="p">)</span> <span class="p">:</span> <span class="n">SyncResult?</span>
</span></span><span class="line"><span class="cl">  <span class="n">channel</span> <span class="o">=</span> <span class="nb">Channel</span><span class="p">(</span><span class="n">SyncResult</span><span class="p">)</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">candidates</span><span class="o">.</span><span class="n">size</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">candidates</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">candidate</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="bp">spawn</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">result</span> <span class="o">=</span> <span class="n">sync_one</span><span class="p">(</span><span class="n">candidate</span><span class="p">,</span> <span class="n">video</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">channel</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">results</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">(</span><span class="n">SyncResult</span><span class="p">)</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">candidates</span><span class="o">.</span><span class="n">size</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">candidates</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">times</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">results</span> <span class="o">&lt;&lt;</span> <span class="n">channel</span><span class="o">.</span><span class="n">receive</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">accepted</span> <span class="o">=</span> <span class="n">results</span><span class="o">.</span><span class="n">select</span><span class="p">(</span><span class="o">&amp;.</span><span class="n">accepted?</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">accepted</span><span class="o">.</span><span class="n">min_by</span><span class="p">(</span><span class="o">&amp;.</span><span class="n">offset</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada candidato de legenda é sincronizado em um fiber separado (via <code>spawn</code>). Os resultados voltam pelo <code>Channel</code>. No final, pega o aceito com menor offset. Sem ThreadPoolExecutor, sem futures, sem callbacks.</p>
<h3>Rate Limiting da API<span class="hx:absolute hx:-mt-20" id="rate-limiting-da-api"></span>
    <a href="#rate-limiting-da-api" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O OpenSubtitles exige throttling de 500ms entre requests. O cliente Crystal implementa isso com Mutex:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-crystal" data-lang="crystal"><span class="line"><span class="cl"><span class="no">RATE_LIMIT_MS</span> <span class="o">=</span> <span class="mi">500</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">private</span> <span class="k">def</span> <span class="nf">throttle!</span> <span class="p">:</span> <span class="nb">Nil</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@mutex</span><span class="o">.</span><span class="n">synchronize</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">elapsed</span> <span class="o">=</span> <span class="nb">Time</span><span class="o">.</span><span class="n">utc</span> <span class="o">-</span> <span class="vi">@last_request_at</span>
</span></span><span class="line"><span class="cl">    <span class="n">remaining</span> <span class="o">=</span> <span class="no">RATE_LIMIT_MS</span> <span class="o">-</span> <span class="n">elapsed</span><span class="o">.</span><span class="n">total_milliseconds</span>
</span></span><span class="line"><span class="cl">    <span class="nb">sleep</span><span class="p">(</span><span class="n">remaining</span><span class="o">.</span><span class="n">milliseconds</span><span class="p">)</span> <span class="k">if</span> <span class="n">remaining</span> <span class="o">&gt;</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="vi">@last_request_at</span> <span class="o">=</span> <span class="nb">Time</span><span class="o">.</span><span class="n">utc</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Simples, thread-safe, sem biblioteca externa.</p>
<h3>Instalação<span class="hx:absolute hx:-mt-20" id="instalação"></span>
    <a href="#instala%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O binário estático sai do GitHub Actions e pode ser instalado de três formas:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Homebrew (macOS / Linux)</span>
</span></span><span class="line"><span class="cl">brew install akitaonrails/tap/easy-subtitle
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Script de instalação</span>
</span></span><span class="line"><span class="cl">curl -fsSL https://raw.githubusercontent.com/akitaonrails/easy-subtitle/master/install.sh <span class="p">|</span> bash
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Ou baixa o binário direto dos Releases</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Um binário. Sem Python, sem pip, sem nada.</p>
<h2>Sobre Portar Coisas &ldquo;Só Porque Sim&rdquo;<span class="hx:absolute hx:-mt-20" id="sobre-portar-coisas-só-porque-sim"></span>
    <a href="#sobre-portar-coisas-s%c3%b3-porque-sim" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu sempre defendi que portar software de uma linguagem pra outra só pelo fetiche da linguagem é perda de tempo. Quantos projetos foram reescritos em Rust &ldquo;porque sim&rdquo;? Quanto esforço gasto em reescritas que não entregaram valor novo?</p>
<p>Mas preciso admitir que esse experimento me fez reconsiderar.</p>
<p>Quando o custo de portar cai de semanas/meses pra menos de 40 minutos, a equação muda. O port do Subservient pra Crystal com Claude não foi um exercício de vaidade linguística. Eu queria um binário estático que eu pudesse jogar num server e esquecer. Sem gerenciar runtime Python, sem pip install quebrando na próxima atualização do sistema.</p>
<p>E o resultado não é um port mecânico. São 2.516 linhas em 42 arquivos, contra 10.220 em 6 monolíticos. O port veio com 76 testes que o original não tinha, CI com release automático pra Linux e macOS, Homebrew formula e install script com verificação de checksum.</p>
<p>O ponto não é que Python é ruim. É que a barreira de &ldquo;será que vale a pena portar?&rdquo; ficou ridiculamente baixa. Port feature-parity com testes em menos de uma hora. Difícil argumentar contra.</p>
<p>O <a href="https://github.com/akitaonrails/easy-subtitle"target="_blank" rel="noopener">repositório está aqui</a>. GPL-3.0, como o original.</p>
]]></content:encoded><category>crystal</category><category>linguagens-de-programacao</category><category>vibe-coding</category></item><item><title>Crystal e um wrapper inteligente pro FFmpeg feito em 3 horas | easy-ffmpeg</title><link>https://www.akitaonrails.com/2026/03/07/crystal-e-um-wrapper-inteligente-pro-ffmpeg-feito-em-3-horas-easy-ffmpeg/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/03/07/crystal-e-um-wrapper-inteligente-pro-ffmpeg-feito-em-3-horas-easy-ffmpeg/</guid><pubDate>Sat, 07 Mar 2026 18:00:00 -0300</pubDate><description>&lt;p&gt;Todo mundo que já precisou converter um vídeo no terminal conhece a dor do FFmpeg. Ele faz tudo. Absolutamente tudo. Mas pra fazer qualquer coisa, você precisa lembrar combinações de flags que parecem feitiço:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ffmpeg -i input.mkv -c:v libx264 -crf &lt;span class="m"&gt;23&lt;/span&gt; -preset medium &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -profile:v high -level 4.1 -c:a aac -b:a 128k &lt;span class="se"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; -movflags +faststart output.mp4&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;O que isso faz? Converte pra H.264 com qualidade razoável pra web, usando AAC no áudio e movendo o moov atom pro início do arquivo pra permitir streaming progressivo. Se você já sabia disso, parabéns. Se não sabia, bem-vindo ao clube de 99% das pessoas que usam FFmpeg copiando comandos do Stack Overflow.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Todo mundo que já precisou converter um vídeo no terminal conhece a dor do FFmpeg. Ele faz tudo. Absolutamente tudo. Mas pra fazer qualquer coisa, você precisa lembrar combinações de flags que parecem feitiço:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ffmpeg -i input.mkv -c:v libx264 -crf <span class="m">23</span> -preset medium <span class="se">\
</span></span></span><span class="line"><span class="cl">  -profile:v high -level 4.1 -c:a aac -b:a 128k <span class="se">\
</span></span></span><span class="line"><span class="cl">  -movflags +faststart output.mp4</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O que isso faz? Converte pra H.264 com qualidade razoável pra web, usando AAC no áudio e movendo o moov atom pro início do arquivo pra permitir streaming progressivo. Se você já sabia disso, parabéns. Se não sabia, bem-vindo ao clube de 99% das pessoas que usam FFmpeg copiando comandos do Stack Overflow.</p>
<p>E esse é o caso simples. Quer fazer um GIF animado a partir de uma sequência de PNGs? Precisa de um pipeline de duas passadas com geração de paleta:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ffmpeg -framerate <span class="m">10</span> -i frame_%04d.png <span class="se">\
</span></span></span><span class="line"><span class="cl">  -vf <span class="s2">&#34;split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse&#34;</span> output.gif</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Quer cortar um trecho, redimensionar pra 720p e forçar aspect ratio 16:9? Boa sorte montando o filter chain.</p>
<p>Eu cansei de ficar decorando flags. Queria uma CLI que eu falasse &ldquo;converte isso pra MP4 otimizado pra web&rdquo; e ela resolvesse. Então fiz o <a href="https://github.com/akitaonrails/easy-ffmpeg"target="_blank" rel="noopener">easy-ffmpeg</a>.</p>
<h2>O que o easy-ffmpeg faz<span class="hx:absolute hx:-mt-20" id="o-que-o-easy-ffmpeg-faz"></span>
    <a href="#o-que-o-easy-ffmpeg-faz" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>É um wrapper inteligente. Você dá o arquivo de entrada e o formato de saída, e ele analisa o vídeo com ffprobe, descobre quais streams de vídeo, áudio e legenda existem, verifica compatibilidade de codecs com o container de destino, e decide sozinho o que pode copiar direto (sem re-encoding, instantâneo) e o que precisa transcodificar.</p>
<p>O uso mais básico:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Converter MKV pra MP4 (copia streams compatíveis, sem re-encoding desnecessário)</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg input.mkv mp4
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Otimizado pra web (H.264 + AAC, faststart)</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg input.mkv mp4 --web
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Otimizado pra celular (720p, AAC stereo, arquivo menor)</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg input.mkv mp4 --mobile
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Compressão máxima (H.265, CRF 28)</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg input.mkv mp4 --compress
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Qualidade alta pra streaming (H.265, CRF 18)</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg input.mkv mp4 --streaming</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h2>Exemplos do que dá pra fazer<span class="hx:absolute hx:-mt-20" id="exemplos-do-que-dá-pra-fazer"></span>
    <a href="#exemplos-do-que-d%c3%a1-pra-fazer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><strong>Cortar um trecho do vídeo:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Do minuto 1:30 até 3:00</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg video.mp4 mp4 --start 1:30 --end 3:00
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Os primeiros 90 segundos</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg video.mp4 mp4 --duration <span class="m">90</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Aceita vários formatos de tempo: 90, 1:30, 01:30.5, 1:02:30</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Redimensionar:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Pra 720p</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg video.mp4 mp4 --scale hd
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pra 1080p</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg video.mp4 mp4 --scale fullhd
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Presets: 2k (1440p), fullhd (1080p), hd (720p), retro (480p), icon (240p)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Mudar aspect ratio:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Forçar 16:9 (adiciona barras pretas se necessário)</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg video.mp4 mp4 --aspect wide
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Formato TikTok/Stories (9:16 vertical)</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg video.mp4 mp4 --aspect tiktok
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Quadrado pro Instagram</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg video.mp4 mp4 --aspect square
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Croppar em vez de adicionar barras</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg video.mp4 mp4 --aspect wide --crop</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Sequência de imagens pra vídeo:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Detecta automaticamente o padrão de numeração (frame_0001.png, frame_0002.png...)</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg /pasta/com/frames/ mp4
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># GIF animado com paleta otimizada</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg /pasta/com/frames/ gif
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Sequência de imagens em 720p a 30fps</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg /pasta/com/frames/ mp4 --fps <span class="m">30</span> --scale hd</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Ver o que vai fazer sem executar:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">easy-ffmpeg video.mkv mp4 --web --dry-run
</span></span><span class="line"><span class="cl"><span class="c1"># Mostra o comando ffmpeg exato que seria executado</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>Combinar tudo:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Cortar, redimensionar e comprimir pra mandar por WhatsApp</span>
</span></span><span class="line"><span class="cl">easy-ffmpeg video.mkv mp4 --start 0:30 --end 2:00 --scale hd --compress</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E se você rodar <code>easy-ffmpeg</code> sem argumentos num terminal interativo, abre um modo TUI com seleção de arquivo por fuzzy search, escolha de preset por menu, e input de tempos com validação &ndash; tudo sem precisar lembrar de nenhuma flag.</p>
<h2>Por que Crystal<span class="hx:absolute hx:-mt-20" id="por-que-crystal"></span>
    <a href="#por-que-crystal" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Faz anos que eu não mexia com Crystal. A última vez foi pra brincar, antes da versão 1.0. E eu queria revisitar.</p>
<p>Crystal ocupa um nicho interessante. Go e Crystal competem pelo mesmo espaço: linguagens compiladas pra aplicações, que geram binários estáticos, com garbage collector e sem dependência de runtime. Mas a abordagem é bem diferente.</p>
<p>Go é famoso por ser deliberadamente simples. Sem generics por anos (só chegaram na 1.18), sem exceções (error returns), sem expressividade. O argumento é que isso facilita leitura e manutenção em equipes grandes. Na prática, resulta em código verboso e repetitivo, com <code>if err != nil</code> em todas as linhas.</p>
<p>Crystal tem tipagem estática com inferência, macros em tempo de compilação, blocos como closures (igual Ruby), exceções, generics desde sempre, e uma sintaxe que qualquer Rubista reconhece:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-crystal" data-lang="crystal"><span class="line"><span class="cl"><span class="c1"># Crystal: ler um JSON e extrair dados</span>
</span></span><span class="line"><span class="cl"><span class="n">streams</span> <span class="o">=</span> <span class="n">json</span><span class="o">[</span><span class="s2">&#34;streams&#34;</span><span class="o">].</span><span class="n">as_a</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">s</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">StreamInfo</span><span class="o">.</span><span class="n">new</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="ss">codec</span><span class="p">:</span> <span class="n">s</span><span class="o">[</span><span class="s2">&#34;codec_name&#34;</span><span class="o">]?.</span><span class="n">try</span><span class="p">(</span><span class="o">&amp;.</span><span class="n">as_s</span><span class="p">)</span> <span class="o">||</span> <span class="s2">&#34;unknown&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">width</span><span class="p">:</span>  <span class="n">s</span><span class="o">[</span><span class="s2">&#34;width&#34;</span><span class="o">]?.</span><span class="n">try</span><span class="p">(</span><span class="o">&amp;.</span><span class="n">as_i</span><span class="p">)</span> <span class="o">||</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">height</span><span class="p">:</span> <span class="n">s</span><span class="o">[</span><span class="s2">&#34;height&#34;</span><span class="o">]?.</span><span class="n">try</span><span class="p">(</span><span class="o">&amp;.</span><span class="n">as_i</span><span class="p">)</span> <span class="o">||</span> <span class="mi">0</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Go equivalente: seria 3x mais linhas com type assertions e error checks</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>A stdlib do Crystal tem HTTP server, JSON parsing, YAML, regex, fibers (green threads com scheduler cooperativo), channels (igual Go), e até IO::FileDescriptor com raw mode pra terminal &ndash; que eu usei pro modo interativo. Concorrência funciona com <code>spawn</code> (equivalente ao <code>go</code> do Go) e <code>Channel</code> (idêntico ao chan do Go). A diferença é que tudo vem com a ergonomia do Ruby.</p>
<p>Pra uma CLI que precisa compilar num binário estático sem dependências, rodar em Linux e macOS, e ser distribuída como download direto &ndash; Crystal é perfeito. O compilador gera binários nativos, e com o Docker Alpine dá pra fazer static linking com musl pra Linux. O binário final do easy-ffmpeg tem ~6MB.</p>
<p>Eu considero Rust melhor pra código de sistemas (kernels, drivers, databases, coisas onde ownership e lifetime importam). Mas pra uma CLI de conversão de vídeo? Rust seria overkill. Crystal dá o mesmo resultado final (binário estático e rápido) com um terço do código e sem brigar com o borrow checker.</p>
<h2>Os números<span class="hx:absolute hx:-mt-20" id="os-números"></span>
    <a href="#os-n%c3%bameros" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O projeto inteiro foi construído em uma tarde. 10 commits entre 18:32 e 21:33 do dia 7 de março de 2026. Três horas.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Linguagem    Arquivos    Linhas    Código
Crystal      13          2.823     2.342
Shell        1           109       91
Markdown     1           266       210
YAML         2           46        34
─────────────────────────────────────────
Total        20          3.326     2.419</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>2.342 linhas de Crystal fazem: CLI com 5 presets, análise de mídia via ffprobe, planejamento inteligente de conversão com matriz de compatibilidade de codecs, progress bar com ETA, modo interativo com fuzzy search, suporte a sequências de imagem e GIF, e trimming/scaling/aspect ratio com validação.</p>
<p>A ferramenta já está com CI/CD no GitHub Actions, compilando binários estáticos pra Linux (x86_64 e arm64) e macOS (arm64), com instalação via curl one-liner:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -fsSL https://raw.githubusercontent.com/akitaonrails/easy-ffmpeg/master/install.sh <span class="p">|</span> sh</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Três horas de uma tarde, da primeira linha de código ao release no GitHub com binários pra três plataformas. O FFmpeg continua fazendo o trabalho pesado &ndash; eu só coloquei uma interface decente na frente.</p>
<p>O <a href="https://github.com/akitaonrails/easy-ffmpeg"target="_blank" rel="noopener">repositório está aqui</a>. MIT license.</p>
]]></content:encoded><category>crystal</category><category>audio-e-video</category><category>vibe-coding</category></item><item><title>37 dias de Imersão em Vibe Coding: Conclusão quanto a Modelos de Negócio</title><link>https://www.akitaonrails.com/2026/03/05/37-dias-de-imers%C3%A3o-em-vibe-coding-conclus%C3%A3o-quanto-a-modelos-de-neg%C3%B3cio/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/03/05/37-dias-de-imers%C3%A3o-em-vibe-coding-conclus%C3%A3o-quanto-a-modelos-de-neg%C3%B3cio/</guid><pubDate>Thu, 05 Mar 2026 14:00:00 -0300</pubDate><description>&lt;p&gt;Nos últimos 37 dias eu me enfiei numa imersão em vibe coding. O objetivo era simples: entender de verdade o que a geração atual de LLMs e agentes de código consegue fazer. Não testando num toy project de fim de semana, mas construindo projetos reais, com deploy em produção, usuários, e as dores de manutenção que vêm junto.&lt;/p&gt;
&lt;p&gt;O resultado foram 653 commits, ~144K linhas de código em 8 projetos publicados no GitHub, e uma série de artigos documentando cada um deles. Se você acompanhou, já leu os post-mortems. Se não acompanhou, aqui vai o índice:&lt;/p&gt;</description><content:encoded><![CDATA[<p>Nos últimos 37 dias eu me enfiei numa imersão em vibe coding. O objetivo era simples: entender de verdade o que a geração atual de LLMs e agentes de código consegue fazer. Não testando num toy project de fim de semana, mas construindo projetos reais, com deploy em produção, usuários, e as dores de manutenção que vêm junto.</p>
<p>O resultado foram 653 commits, ~144K linhas de código em 8 projetos publicados no GitHub, e uma série de artigos documentando cada um deles. Se você acompanhou, já leu os post-mortems. Se não acompanhou, aqui vai o índice:</p>
<h2>Os artigos<span class="hx:absolute hx:-mt-20" id="os-artigos"></span>
    <a href="#os-artigos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><a href="/2026/01/28/vibe-code-eu-fiz-um-appzinho-100-com-glm-4-7-tv-clipboard/">Vibe Code: Eu fiz um appzinho 100% com GLM 4.7 (TV Clipboard)</a></li>
<li><a href="/2026/01/29/vibe-code-qual-llm-%C3%A9-a-melhor-vamos-falar-a-real/">Vibe Code: Qual LLM é a MELHOR?? Vamos falar a REAL</a></li>
<li><a href="/2026/02/01/vibe-code-fiz-um-editor-de-markdown-do-zero-com-claude-code-frankmd-part-1/">Vibe Code: Fiz um Editor de Markdown do zero com Claude Code (FrankMD) PARTE 1</a></li>
<li><a href="/2026/02/01/vibe-code-fiz-um-editor-de-markdown-do-zero-com-claude-code-frankmd-parte-2/">Vibe Code: Fiz um Editor de Markdown do zero com Claude Code (FrankMD) PARTE 2</a></li>
<li><a href="/2026/02/08/rant-ia-acabou-com-programadores/">RANT: IA acabou com os programadores?</a></li>
<li><a href="/2026/02/16/vibe-code-do-zero-a-producao-em-6-dias-the-m-akita-chronicles/">Vibe Code: Do Zero à Produção em 6 DIAS - The M.Akita Chronicles</a></li>
<li><a href="/2026/02/20/do-zero-a-pos-producao-em-1-semana-como-usar-ia-em-projetos-de-verdade-bastidores-do-the-m-akita-chronicles/">Do Zero à Pós-Produção em 1 Semana - Como usar IA em Projetos de Verdade</a></li>
<li><a href="/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/">Vibe Code: Fiz um clone do Mega em Rails em 1 dia pro meu Home Server</a></li>
<li><a href="/2026/02/23/vibe-code-fiz-um-indexador-inteligente-de-imagens-com-ia-em-2-dias/">Vibe Code: Fiz um Indexador Inteligente de Imagens com IA em 2 dias - Frank Sherlock</a></li>
<li><a href="/2026/02/24/rant-o-akita-abriu-as-pernas-pra-ia/">RANT: o Akita abriu as pernas pra IA??</a></li>
<li><a href="/2026/03/01/ai-jail-sandbox-para-agentes-de-ia-de-shell-script-a-ferramenta-real/">ai-jail: Sandbox para Agentes de IA</a></li>
<li><a href="/2026/03/01/software-nunca-esta-pronto-4-projetos-a-vida-pos-deploy-e-por-que-one-shot-prompt-e-mito/">Software Nunca Está &lsquo;Pronto&rsquo; - 4 Projetos, a Vida Pós-Deploy</a></li>
<li><a href="/2026/03/04/eu-fiz-um-sistema-de-data-mining-pra-minha-namorada-influencer-dicas-e-truques/">Eu Fiz um Sistema de Data Mining pra Minha Namorada Influencer</a></li>
<li><a href="/2026/03/05/meu-primeiro-fracasso-com-vibe-code-e-como-consertei-frank-yomik/">Meu primeiro fracasso com Vibe Code e como Consertei - Frank Yomik</a></li>
</ul>
<h2>Os projetos<span class="hx:absolute hx:-mt-20" id="os-projetos"></span>
    <a href="#os-projetos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><table>
  <thead>
      <tr>
          <th>Projeto</th>
          <th>Commits</th>
          <th>LOC</th>
          <th>Tempo</th>
          <th>O que faz</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="https://github.com/akitaonrails/FrankMD"target="_blank" rel="noopener">FrankMD</a></td>
          <td>234</td>
          <td>38K</td>
          <td>~5 dias</td>
          <td>Editor de Markdown em Rust/Tauri</td>
      </tr>
      <tr>
          <td><a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">FrankYomik</a></td>
          <td>131</td>
          <td>21K</td>
          <td>~9 dias</td>
          <td>Tradutor de mangá/webtoon (Go + Python + Flutter)</td>
      </tr>
      <tr>
          <td><a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener">FrankSherlock</a></td>
          <td>103</td>
          <td>37K</td>
          <td>~6 dias</td>
          <td>Indexador inteligente de imagens (Rails + Python)</td>
      </tr>
      <tr>
          <td>mila-bot (privado)</td>
          <td>60</td>
          <td>30K</td>
          <td>~3 dias</td>
          <td>Sistema de data mining (Rails + Discord)</td>
      </tr>
      <tr>
          <td><a href="https://github.com/akitaonrails/tvclipboard"target="_blank" rel="noopener">TVClipboard</a></td>
          <td>49</td>
          <td>5K</td>
          <td>~2 dias</td>
          <td>App de clipboard cross-device com GLM 4.7</td>
      </tr>
      <tr>
          <td><a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a></td>
          <td>46</td>
          <td>6K</td>
          <td>~4 dias</td>
          <td>Sandbox de segurança pra agentes de IA (Rust)</td>
      </tr>
      <tr>
          <td><a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener">FrankMega</a></td>
          <td>29</td>
          <td>7K</td>
          <td>~1 dia</td>
          <td>Clone do Mega para home server (Rails)</td>
      </tr>
      <tr>
          <td>The M.Akita Chronicles</td>
          <td>1+</td>
          <td>muitas</td>
          <td>~6 dias</td>
          <td>Blog/newsletter completo em produção</td>
      </tr>
  </tbody>
</table>
<p>Isso tudo aconteceu entre 27 de janeiro e 5 de março de 2026.</p>
<p>Não vou repetir os detalhes técnicos de cada projeto. Cada post-mortem acima já cobriu isso. O que eu quero discutir aqui é a consequência.</p>
<h2>O que esses 37 dias me mostraram<span class="hx:absolute hx:-mt-20" id="o-que-esses-37-dias-me-mostraram"></span>
    <a href="#o-que-esses-37-dias-me-mostraram" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Projetos que antes levariam semanas ou meses pra um desenvolvedor experiente agora levam dias. Um editor de Markdown completo em 5 dias. Um clone do Mega em 1 dia. Um sistema de data mining com 40+ ferramentas de bot em 3 dias. Um indexador de imagens com vision AI em 2 dias.</p>
<p>E não estou falando de protótipos jogados fora. Esses projetos estão em produção. Têm testes. Têm deploy automatizado. São software de verdade construído com práticas de engenharia de verdade, só que com velocidade que antes era impensável.</p>
<p>Essa velocidade não é exclusiva minha. A Cloudflare acabou de demonstrar algo parecido. <a href="https://blog.cloudflare.com/vinext/"target="_blank" rel="noopener">Num post recente</a>, eles descrevem como um engenheiro reimplementou o surface de API do Next.js sobre Vite em uma semana, usando Claude como ferramenta principal. US$ 1.100 em tokens de API, mais de 800 sessões, 1.700 testes unitários, 380 testes E2E, builds 4.4x mais rápidos que o Next.js original. Controvérsias à parte sobre se é uma reimplementação &ldquo;completa&rdquo; ou não, o ponto central é real: software que antes exigia equipes e meses agora pode ser construído por uma pessoa em dias.</p>
<p>E isso muda tudo pra quem quer empreender.</p>
<h2>A morte da startup fácil<span class="hx:absolute hx:-mt-20" id="a-morte-da-startup-fácil"></span>
    <a href="#a-morte-da-startup-f%c3%a1cil" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Durante anos, o modelo clássico de startup funcionava assim: alguém tem uma ideia, monta uma equipe pequena, desenvolve um MVP em 3-6 meses, levanta seed money, e tenta escalar. A barreira de entrada era o custo de desenvolvimento. Programadores são caros, desenvolvimento é lento, e o primeiro a chegar no mercado tem vantagem.</p>
<p>Esse modelo está quebrando.</p>
<p>Se eu consigo fazer um clone funcional do Mega em 1 dia, quanto vale a startup de &ldquo;cloud storage&rdquo;? Se a Cloudflare reimplementa o core do Next.js em 1 semana com um engenheiro e US$ 1.100 em tokens, qual é o moat real de uma plataforma como Vercel? O SaaS de &ldquo;social listening&rdquo; que cobra R$ 500/mês compete com algo que eu fiz em 3 dias como side project.</p>
<p>Todo aquele empreendedor que chegava descrevendo sua ideia como &ldquo;é tipo um Uber, mas pra&hellip;&rdquo; ou &ldquo;é como Airbnb, só que&hellip;&rdquo; ou &ldquo;mais uma rede social, mas com&hellip;&rdquo; &ndash; essas pessoas precisam parar e repensar. Quando qualquer desenvolvedor competente com acesso a Claude Code ou GPT Codex consegue replicar seu MVP numa semana, sua ideia não vale mais nada. A execução ficou barata demais.</p>
<p>Não estou exagerando. Nos meus 37 dias, eu fiz coisas que antigamente eu não faria nem em 6 meses. Pequenos CRMs, ecommerces, gerenciadores de conteúdo, ferramentas de produtividade, apps de data mining, pipelines de processamento &ndash; tudo isso virou commodity. O código em si não é mais diferencial.</p>
<h2>O que é diferencial, então?<span class="hx:absolute hx:-mt-20" id="o-que-é-diferencial-então"></span>
    <a href="#o-que-%c3%a9-diferencial-ent%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui entra a parte que a maioria dos entusiastas de vibe coding ignora. Pra ilustrar, vou usar meu próprio projeto como exemplo.</p>
<p>O <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a> traduz páginas de mangá de japonês pra inglês em tempo quase real. O pipeline inteiro (detecção de balões, OCR, tradução, renderização) funciona. Mas o componente mais importante do sistema não é nenhum código que eu escrevi. É o modelo <code>ogkalu/comic-text-and-bubble-detector</code>, um RT-DETR-v2 treinado em ~11.000 imagens rotuladas de quadrinhos.</p>
<p>Eu não treinei esse modelo. Eu não poderia treinar esse modelo facilmente. Coletar 11.000 imagens de quadrinhos diversas, rotular manualmente os balões de fala em cada uma (ou gerar labels com alguma pipeline semi-automatizada, o que também não é trivial), configurar o treinamento, e rodar no hardware necessário &ndash; isso é trabalho de natureza diferente. É trabalho que vibe coding não resolve.</p>
<p>E esse é o caso de um modelo relativamente pequeno. Um detector de objetos pode ser treinado com algumas centenas a milhares de imagens rotuladas numa única GPU em horas. Estudos mostram que resultados úteis são possíveis com 100-350 imagens pra domínios específicos, mas detectores robustos pro mundo real geralmente precisam de milhares. O custo é baixo, na casa de centenas de dólares.</p>
<p>Agora olha o que acontece quando subimos pra modelos maiores.</p>
<h2>Os números que importam<span class="hx:absolute hx:-mt-20" id="os-números-que-importam"></span>
    <a href="#os-n%c3%bameros-que-importam" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O GPT-4 custou mais de US$ 100 milhões pra treinar, segundo o próprio Sam Altman. A Stanford estimou o custo de compute do Gemini Ultra do Google em US$ 191 milhões. O Llama 3 da Meta consumiu 39,3 milhões de GPU-horas em H100s, e a Meta montou dois clusters de 24.576 GPUs cada pra viabilizar isso &ndash; e até o final de 2024, planejava ter 350.000 H100s na sua infraestrutura.</p>
<p>Esses custos estão acelerando. Segundo a Epoch AI, o custo de hardware e energia pra treinar modelos de fronteira cresce a 2,4x por ano desde 2016. Se essa tendência continuar, os maiores treinamentos vão custar mais de US$ 1 bilhão antes do fim de 2027. O Dario Amodei, CEO da Anthropic, já disse que desenvolvedores de fronteira provavelmente estão gastando perto de um bilhão por treinamento agora, com treinamentos de US$ 10 bilhões esperados em dois anos.</p>
<p>E o hardware? Uma GPU H100 custa US$ 25.000-40.000 por unidade. Um servidor com 8 GPUs sai entre US$ 200.000 e US$ 400.000. A memória HBM que essas GPUs usam está com capacidade esgotada &ndash; SK Hynix, Samsung e Micron já anunciaram aumentos de ~20% nos preços pra 2026. A NVIDIA consome mais de 60% da produção global de HBM. É um gargalo estrutural, não temporário.</p>
<p>Em termos de energia: data centers globais consumiram ~415 TWh de eletricidade em 2024, segundo o IEA, cerca de 1,5% da eletricidade mundial. A projeção é ~945 TWh até 2030. Novos data centers estão sendo construídos com capacidades de 100 MW a 1 GW cada.</p>
<p>E os investimentos das big techs refletem isso. Em 2025, os gastos de capital agregados de Amazon (<del>$125B), Google (</del>$91B), Microsoft (<del>$80B) e Meta (</del>$71B) passaram de US$ 400 bilhões, crescimento de 62% sobre 2024. Goldman Sachs projeta mais de US$ 500 bilhões em 2026.</p>
<p>Esses números não são pra assustar. São pra contextualizar de onde vem a barreira de entrada real.</p>
<h2>A nova barreira: dados exclusivos e capacidade de treinamento<span class="hx:absolute hx:-mt-20" id="a-nova-barreira-dados-exclusivos-e-capacidade-de-treinamento"></span>
    <a href="#a-nova-barreira-dados-exclusivos-e-capacidade-de-treinamento" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se software ficou barato de produzir, o diferencial competitivo migrou. A pergunta deixou de ser &ldquo;quem escreve o código mais rápido?&rdquo; e virou &ldquo;quem tem acesso a dados que ninguém mais tem, e sabe transformar esses dados em modelos úteis?&rdquo;</p>
<p>O DeepSeek-V3 anunciou que seu treinamento custou US$ 5,5 milhões em compute. A imprensa celebrou o &ldquo;modelo barato chinês&rdquo;. Mas o The Register reportou que o custo de aquisição dos 256 servidores GPU usados foi de mais de US$ 51 milhões &ndash; e isso exclui R&amp;D, aquisição de dados, limpeza de dados, e todos os treinamentos fracassados antes do run final que deu certo. O custo real de desenvolver a capacidade é uma ou duas ordens de grandeza acima do custo marginal do treinamento final bem-sucedido.</p>
<p>É por isso que só vemos grandes empresas produzindo modelos de fronteira. Meta, Alibaba, Google, Amazon, Microsoft, Anthropic &ndash; empresas que investem dezenas de bilhões em hardware e energia. Uma startup em garagem não consegue competir nessa dimensão.</p>
<p>Mas a questão vai além de modelos de fronteira. Mesmo modelos especializados menores exigem algo que não se compra: dados proprietários de qualidade.</p>
<p>O Llama 3 foi treinado em 15 trilhões de tokens. A Epoch AI já documentou que estamos nos aproximando dos limites de dados textuais gerados por humanos na internet. Dados públicos estão sendo exauridos. Quem tem dados exclusivos &ndash; dados médicos, financeiros, industriais, logísticos, de sensores, de comportamento de usuário &ndash; tem algo que não pode ser replicado por vibe coding.</p>
<p>E mesmo quem treina modelos especializados com dados proprietários enfrenta um problema: a vantagem é temporária. Outro competidor pode coletar dados similares e treinar um modelo concorrente em meses. O diferencial precisa ser continuamente alimentado: mais dados, melhor curadoria, pipelines de treinamento mais eficientes, acesso a hardware que está cada vez mais escasso e caro.</p>
<h2>O cenário<span class="hx:absolute hx:-mt-20" id="o-cenário"></span>
    <a href="#o-cen%c3%a1rio" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se você está pensando em montar uma startup, a pergunta que importa mudou.</p>
<p>Antes era: &ldquo;consigo construir esse software?&rdquo; Agora a resposta é quase sempre sim, rápido e barato.</p>
<p>A pergunta agora é: &ldquo;tenho dados exclusivos que ninguém mais tem, e sei como transformar esses dados em algo útil que seja difícil de replicar?&rdquo;</p>
<p>Se a resposta for não, qualquer concorrente com Claude Code replica seu produto em dias. E depois aparece outro. E outro. A corrida pro fundo em preço é imediata quando o custo de construir é próximo de zero.</p>
<p>Se a resposta for sim, você tem um moat real &ndash; mas temporário. Modelos concorrentes treinados em dados similares podem aparecer em meses. Seu diferencial precisa ser continuamente alimentado.</p>
<p>A era das startups fáceis acabou. Não porque ficou mais difícil construir software &ndash; ficou muito mais fácil. Mas justamente por isso: quando todo mundo consegue construir a mesma coisa em uma semana, a vantagem competitiva precisa vir de outro lugar. E esse &ldquo;outro lugar&rdquo; requer capital e infraestrutura que são ordens de grandeza mais caros do que escrever código.</p>
<p>A garagem ainda funciona. Mas o que sai dela não pode mais ser &ldquo;um app&rdquo;.</p>
]]></content:encoded><category>vibe-coding</category><category>startups</category><category>negocios</category><category>off-topic</category></item><item><title>RANT: o Akita abriu as pernas pra IA??</title><link>https://www.akitaonrails.com/2026/02/24/rant-o-akita-abriu-as-pernas-pra-ia/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/02/24/rant-o-akita-abriu-as-pernas-pra-ia/</guid><pubDate>Tue, 24 Feb 2026 11:54:26 +0000</pubDate><description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: Eu sempre &amp;ldquo;abri as pernas&amp;rdquo; pra IA. Mas se você só assistiu clipes dos podcasts, entendo porque está confuso. Vou explicar.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Alguém lembra deste meu video: &lt;a href="https://www.akitaonrails.com/2023/09/20/akitando-145-16-linguagens-em-16-dias-minha-saga-da-rinha-de-backend/"&gt;&amp;ldquo;16 Linguagens em 16 Dias&amp;rdquo;&lt;/a&gt;? É do final de 2023 (1 ano depois do lançamento do primeiro ChatGPT) e é sobre a primeira Rinha de Backend. Como vocês acham que eu fiz código em 16 linguagens em 16 dias??&lt;/p&gt;
&lt;p&gt;Eu uso IA pra fazer código desde o começo de 2023 (3 anos já). Não comecei agora: eu nunca parei. 🤷‍♂&lt;/p&gt;</description><content:encoded><![CDATA[<blockquote>
  <p><strong>TL;DR</strong>: Eu sempre &ldquo;abri as pernas&rdquo; pra IA. Mas se você só assistiu clipes dos podcasts, entendo porque está confuso. Vou explicar.</p>

</blockquote>
<p>Alguém lembra deste meu video: <a href="/2023/09/20/akitando-145-16-linguagens-em-16-dias-minha-saga-da-rinha-de-backend/">&ldquo;16 Linguagens em 16 Dias&rdquo;</a>? É do final de 2023 (1 ano depois do lançamento do primeiro ChatGPT) e é sobre a primeira Rinha de Backend. Como vocês acham que eu fiz código em 16 linguagens em 16 dias??</p>
<p>Eu uso IA pra fazer código desde o começo de 2023 (3 anos já). Não comecei agora: eu nunca parei. 🤷‍♂</p>
<p><em>&ldquo;Mas você disse que IA nunca ia ser boa, você odeia IA&rdquo;</em></p>
<p>Isso foi o que você entendeu porque é preguiçoso e só assiste a clipes fora de contexto ou tweets fora de contexto. Se lesse meu blog, eu tenho literalmente dezenas de posts (particularmente de 2 anos pra cá) detalhando cada aspecto da evolução e uso de IAs (seja pra código, seja pra imagens, seja pra 3D modelling).</p>
<h2>&ldquo;Empolgação?&rdquo;<span class="hx:absolute hx:-mt-20" id="empolgação"></span>
    <a href="#empolga%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>E quanto à minha famosa frase:</p>
<blockquote>
  <p><strong>&ldquo;Sua empolgação com IA é inversamente proporcional ao seu conhecimento sobre IA&rdquo;</strong> ?</p>

</blockquote>
<p>Continua correta. É que nem quem falava que Low Code ia substituir todo programador. Não vai. Mesma coisa agora: por melhor que a IA seja, ela não vai substituir todo programador. Só os ruins, como já expliquei <a href="https://akitaonrails.com/2026/02/08/rant-ia-acabou-com-programadores/"target="_blank" rel="noopener">neste outro rant</a>.</p>
<p>Programadores como eu não têm nenhum problema: nós somos os tomadores de decisão que conseguem decidir o que IAs não conseguem hoje e nunca vão conseguir (é uma impossibilidade na fundação da computação, já volto nisso).</p>
<p>Os &ldquo;empolgados&rdquo; que eu falei, é todo não-programador startupeiro que - com ou sem IA - me lança código porcaria em produção e daí acontecem coisas como o fracasso do app <strong>&ldquo;Tea&rdquo;</strong>, lembram?</p>
<p><a href="https://www.npr.org/2025/08/02/nx-s1-5483886/tea-app-breach-hacked-whisper-networks"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/tea.jpg" alt="tea leak"  loading="lazy" /></a></p>
<p>Não houve nenhuma tentativa de &ldquo;hacking&rdquo;; o banco de dados deles já estava aberto ao público em produção: qualquer um podia só baixar. Isso é um cara &ldquo;empolgado com IA&rdquo;.</p>
<p>Ou os idiotas que já demitiram todos os seus programadores porque acha que o código de IA vai ser muito melhor. Spoiler alert: não vai.</p>
<p>Aí você fica mais confuso. <em>&ldquo;Você não gosta de IA, então?&rdquo;</em> Não, não, eu gosto. <em>&ldquo;Mas se gosta é porque ele já é inteligente?&rdquo;</em> Não, não. Veja como sua linha de raciocínio não faz sentido.</p>
<h2>&ldquo;Papagaio Estocástico?&rdquo;<span class="hx:absolute hx:-mt-20" id="papagaio-estocástico"></span>
    <a href="#papagaio-estoc%c3%a1stico" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Me lembraram hoje que eu disse também:</p>
<blockquote>
  <p><strong>&ldquo;IA é só um gerador de textos glorificado&rdquo;</strong></p>

</blockquote>
<p>Confusos? <em>&ldquo;Como ela pode ser boa se é só um gerador de textos??&rdquo;</em></p>
<p>Eu realmente não entendo essa linha de raciocínio. Geradores de texto são objetivamente bons. Eu e você usamos todos os dias o auto-corretor de todo app de e-mail e mensagens. Sejam os do teclado de iPhone ou Android. Seja Grammarly. Nenhum de vocês vai dizer que ela tem &ldquo;personalidade&rdquo; ou mesmo &ldquo;inteligência&rdquo;, mas acho que todos concordamos que ela é muito útil. Mesmo errando, de vez em quando, na maior parte do tempo conserta nossos textos direitinho, não?</p>
<p>GPT ou Claude ou GLM ou Gemini ou DeepSeek, todos eles continuam sendo &ldquo;papagaios estocásticos&rdquo; ou &ldquo;geradores de texto glorificados&rdquo;.</p>
<ul>
<li>Papagaio estocástico: algo que repete coisas com método aleatório ou parcialmente aleatório (existe um elemento de entropia)</li>
<li>Geradores de texto: utilizam cálculo probabilístico (transformers no lado complicado, ou cadeias de Markov, no lado simples) pra tentar descobrir a palavra/token mais provável, dado do texto anterior.</li>
</ul>
<p>Toda &ldquo;IA&rdquo; (mais corretamente &ldquo;LLM&rdquo;) são exatamente assim desde que foram lançadas em 2022 até hoje: gigabytes de matrizes hiper-dimensionais de pesos e probabilidades onde seu texto/prompt é calculado contra essas matrizes pra definir o &ldquo;próximo token&rdquo;. Concatena esse novo token no texto original e recalcula tudo de novo nas mesmas matrizes (mais corretamente, tensors), <strong>sorteia</strong> o próximo token de um pequeno grupo de probabilidades, concatena, repete, e segue assim.</p>
<p>Isso é um &ldquo;gerador de textos&rdquo;. Mas sim, eles são tão bons que &ldquo;parecem&rdquo; até ter personalidade nas respostas. E seres humanos são muito facilmente enganados a &ldquo;antropomorfizar&rdquo; coisas que não humanos. Não precisa de muita coisa.</p>
<p>Em 2023, no video <a href="/2023/06/19/akitando-142-entendendo-como-chatgpt-funciona-rodando-sua-propria-ia/">&ldquo;Entendendo como ChatGPT Funciona&rdquo;</a> eu explico sobre o Teste de Turing, um dos primeiros aplicativos &ldquo;conversacionais&rdquo; que passam no teste, a <a href="https://en.wikipedia.org/wiki/ELIZA"target="_blank" rel="noopener">&ldquo;Eliza&rdquo;</a>, que já passava no teste, e sem precisar de IA.</p>
<p>Por isso eu disse que são &ldquo;glorificados&rdquo;, porque todos que tem pouco conhecimento de IA, pensam nela como o macaco do filme pensa no monolito: como uma divindade que precisa ser glorificada.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/2001-a-Space-Odyssey.jpg" alt="2001 a Space Odyssey"  loading="lazy" /></p>
<h2>&ldquo;Mas e o Flow?&rdquo;<span class="hx:absolute hx:-mt-20" id="mas-e-o-flow"></span>
    <a href="#mas-e-o-flow" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><em>&ldquo;Mas Akita, no Flow você era contra IAs.&rdquo;</em></p>
<p>Nope, de novo, você só assistiu clipes. Veja como eu abro a conversa: estou falando da histeria em torno do <a href="https://ai-2027.com/"target="_blank" rel="noopener">&ldquo;Report AI 2027&rdquo;</a>, estou falando das asneiras do Sam Altman de que eles estavam bem próximos de atingir &ldquo;AGI&rdquo; (Inteligência Artificial GERAL, uma inteligência que, igual um humano, raciocina e decide as coisas sozinho e é capaz de evoluir autonomamente). E todas as controvérsias sobre IAs que chegavam num &ldquo;plano&rdquo; pra matar o dono (o que eu chamo de &ldquo;fanfics&rdquo;).</p>
<p>Daí eu passo horas explicando em detalhes a história e exatamente como é feito o &ldquo;cálculo de próximo token&rdquo;, como também <a href="/2025/04/29/dissecando-um-modelfile-de-ollama-ajustando-qwen3-pra-codigo/">dei mais detalhes</a> aqui no blog. E sempre dizendo que precisa existir alguma &ldquo;nova descoberta&rdquo; (que ainda não aconteceu). Enquanto cada novo GPT ou Claude forem só uma evolução do anterior, existem limites. E eu expliquei os limites.</p>
<p>Não é uma &ldquo;questão de tempo&rdquo;. Isso não existe. Mesmo nunca tendo ido pra, digamos, Saturno, nós sabemos o que é necessário e o principal: quanto custa. <strong>&ldquo;COM A TECNOLOGIA ATUAL&rdquo;</strong> - que é sempre a premissa - não tem como/não faz sentido prático.</p>
<p>Amanhã pode surgir uma &ldquo;nova descoberta&rdquo; que mude tudo. Mas enquanto ela não aparece, não podemos &ldquo;contar com isso&rdquo;. E toda minha explicação é com base nessa premissa. E a conclusão é objetiva e matemática:</p>
<blockquote>
  <p>AGI não é alcançável. Isso não mudou.</p>

</blockquote>
<p>Não significa que as IAs atuais sejam <strong>INÚTEIS</strong>. Essa é a contra-conclusão que eu não consigo entender o raciocínio também. Em todos os podcasts, depois do primeiro, eu voltava dizendo: <em>&ldquo;Do jeito que eu falo, alguns parecem entender que eu não gosto de IAs, mas é o contrário: eu gosto.&rdquo;</em></p>
<p>Cansei de repetir isso nos podcasts, no blog, no X, mas essa é a parte que todo mundo faz de conta que não está lá e <strong>OMITE</strong>. Por isso sempre deixo tudo por escrito aqui no blog e, como falei, vocês podem ver que eu já usava desde 2023 - 3 anos atrás.</p>
<blockquote>
  <p>Em resumo: TUDO que eu disse nos últimos videos do meu canal e nos podcasts, <strong>CONTINUAM VALENDO</strong>. A explicação continua correta. O que está errado são SUAS CONCLUSÕES. Reveja e interprete literalmente e não subjetivamente, com a falta de conhecimento que vocês tem. Não ignorem os termos que eu falei que vocês não entendem. O argumento só faz sentido se for completo, e ele custou 4 horas pra eu explicar, em cada video.</p>

</blockquote>
<h2>Recado aos &ldquo;Empolgados&rdquo;<span class="hx:absolute hx:-mt-20" id="recado-aos-empolgados"></span>
    <a href="#recado-aos-empolgados" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Eu disse isso no X esses dias:</p>
<p><a href="https://x.com/AkitaOnRails/status/2025649633318555812"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-24_12-23-27.jpg" alt="agile vibe code"  loading="lazy" /></a></p>
<p>Não importa se quer chamar de &ldquo;Vibe Code&rdquo;, &ldquo;Agentic Engineering&rdquo;, &ldquo;IA Assisted Programming&rdquo;, é tudo <strong>bullshit</strong>.</p>
<p>TUDO agora é &ldquo;programar com IA&rdquo;.</p>
<p>E só tem dois jeitos de fazer isso: o certo e o errado. O jeito errado é como vocês e o dono do app Tea (provavelmente, supostamente) fazem: deixam a IA fazer o código, não sabem revisar nem criticar esse código, sobem pra produção e deixam usuários usarem, sem saber dos riscos.</p>
<p>O jeito certo: eu passei DOIS MESES escrevendo mais de VINTE POSTS neste blog explicando o jeito certo, que eu chamei de &ldquo;Agile Vibe Code&rdquo;, mas é basicamente <strong>&ldquo;Engenharia de Software aplicada à IA&rdquo;</strong> e adivinhe: precisa ter estudado e experiência pra saber.</p>
<p>Não é 2026 e não vai ser 2027 nem tão cedo quanto você pensa que o Claude ou Codex vão &ldquo;substituir TODOS os programadores&rdquo;. Não, vão SIM substituir todos os ruins e isso é excelente! Novamente, <a href="https://akitaonrails.com/2026/02/08/rant-ia-acabou-com-programadores/"target="_blank" rel="noopener">leia este rant</a>.</p>
<p>Você nunca vai conseguir fazer um &ldquo;novo Linux&rdquo; e &ldquo;substituir o Linus Torvalds&rdquo; com IA. Vai conseguir &ldquo;copiar partes&rdquo;, sim. Copiar não é suficiente. Programador ruim já copiava antes, e isso nunca os tornou bons.</p>
<h2>Recados aos &ldquo;Haters de IA&rdquo;<span class="hx:absolute hx:-mt-20" id="recados-aos-haters-de-ia"></span>
    <a href="#recados-aos-haters-de-ia" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Desistam, é um caminho sem volta. Assim como foi mobile, assim como foi a internet, assim como foram micro-computadores, etc. A caixa de pandora já foi aberta, não tem como fechar mais. IAs vieram pra ficar.</p>
<blockquote>
  <p><em>&ldquo;Mas você disse que IA é uma bolha que ia estourar!&rdquo;</em></p>

</blockquote>
<p>De novo, ou vocês se fazem de bobo ou são bobos.</p>
<p>A Bolha da Internet de 2001 estourou, mas a Internet não desapareceu. Pelo contrário, ela aumentou! Quem sumiu foram as empresas que acharam que iam ganhar dinheiro fácil aproveitando a onda da bolha, e todo mundo que acreditou cegamente. Porém, quem foi <strong>&ldquo;Hater de Internet&rdquo;</strong>, perdeu também.</p>
<p>A esta altura, todo argumento contra IA já soa como uma desculpa mal feita. Vamos ver algumas:</p>
<p><em>&ldquo;IA comete vários erros, vira e mexe eu vejo um código meia boca.&rdquo;</em></p>
<p>É verdade, e justamente por isso falei pra não se empolgar à toa. Ela comete erros, eu pego todos os dias. Até o ano passado, a taxa de erro vs acertos era alta o suficiente pra eu não conseguir recomendar que qualquer amador usasse no dia a dia. Só quem presta muita atenção e sabe corrigir - como eu.</p>
<p>Porém, em 2026, essa taxa melhorou significativamente. Ela de fato acerta mais do que erra agora, o suficiente pra eu conseguir confiar nela sem &ldquo;micro-gerenciar&rdquo; cada passo.</p>
<p>A verdade é que todo programador humano erra, e erra BASTANTE. Quem erra tem viés de confirmação e não acha que erra tanto. Só quem vê de fora - alguém como eu, que já gerencei centenas de programadores em dezenas de projetos - sei que eles erram muito mais do que estão preparados pra admitir.</p>
<p>Eu CANSEI de pedir pra sempre só subir PR com testes unitários: todo mundo ignora.</p>
<p>Eu CANSEI de pedir pra sempre pelo menos rodar uma vez na própria máquina pra ver se está funcionando, do que eu puxar o PR e descobrir que nem executa: todo mundo ignora.</p>
<p>Eu CANSEI de pedir pra não só dar copy e paste do stackoverflow, e pelo menos ajustar a adaptar pro nosso projeto. Todo mundo ignora.</p>
<p>Eu CANSEI de pedir pra não jogar tudo no mesmo arquivão e fazer refactoring de tempos em tempos pra não aumentar débito técnico: todo mundo ignora.</p>
<p>Eu CANSEI de pedir pra não subir mais commits com mensagens de &ldquo;bug fix&rdquo;, sem explicar o que tem lá exatamente: todo mundo ignora.</p>
<p>Eu CANSEI de pedir pra lembrar de atualizar a documentação quando mudar alguma funcionalidade, pra facilitar quem vai pegar pra testar: todo mundo ignora.</p>
<p>Eu CANSEI de pedir pra cobrir um bug fix com teste de regressão pra não vermos o mesmo erro de novo. O mesmo erro sempre se repetia: todo mundo ignora.</p>
<p>Eu CANSEI de pedir pra seguir as convenções que concordamos no projeto e não fazer código diferente, com outros padrões. Todo mundo ignora.</p>
<p>Eu CANSEI de pedir pra ajustar scripts de deploy, CI ou coisas assim quando caem pedaços que afetam a infra: todo mundo ignora.</p>
<p>Eu CANSEI de pedir pra não acumular um monte de código que não faz sentido junto e não dar <code>git add .</code> no final e só escrever &ldquo;nova feature&rdquo; e comitar tudo junto. Todo mundo ignora.</p>
<p>Esses são só alguns exemplos. Sabe qual a novidade: o Claude e o Codex não ignoram. Tudo isso que listei, que acontece em TODO projeto com humanos, não importa o tamanho do projeto, não acontece mais pra mim com Claude.</p>
<p>Entendam: isso tudo deveria ser o básico do básico, nível estagiário. Mas eu CANSEI de pedir pra sênior tomar mais cuidado, pra não dar mau exemplo pros júniors: TODO MUNDO IGNORA.</p>
<p>E agora, todos esses que ME CANSARAM, são justamente os que se tornaram &ldquo;Haters de IA&rdquo;. Mas é lógico, a IA faz tudo que eles NÃO FAZEM. Olhem no espelho e reflitam se seu código era tão bom assim (Spoiler: não era).</p>
<p>90% de todo código produzido, não é nem de perto alguma coisa como &ldquo;otimização na solução de memory management do Linux&rdquo; ou &ldquo;bug fix de regressão de performance no drivers do file system&rdquo; ou &ldquo;melhoria no algoritmo de segurança do firewall&rdquo; ou &ldquo;reescrever esta parte antiga de Assembly em C&rdquo;..</p>
<p>90% da maior parte do código produzido no dia a dia é <strong>MUNDANO</strong>, é consumir uma API, é fazer um front-end, é mais um relatório, mais um CRUD, mais uma validação de e-mail, mais um job de limpeza, mais um script de deploy. Absolutamente NADA realmente &ldquo;interessante&rdquo;.</p>
<p>O que eu gostei das LLMs? Ela REMOVE de mim todas as tarefas mundanas e me deixa focar nas partes que eu gosto: a pesquisa, a formação de hipóteses, os benchmarks, os testes a/b pra conseguir tomar decisões melhores, os testes de integração que garantem que as diversas partes do meu sistema funcionam mesmo. Tudo que antes não dava pra fazer, porque 90% do meu tempo era ocupado consertando a porcaria do CSS de alguém.</p>
<p>Eu ODEIO mexer em CSS. Já passou da hora de não precisar mais.</p>
<p>Eu ODEIO fazer CRUD. Já passou da hora de não precisar mais.</p>
<p>Eu ODEIO fazer o setup inicial de ambiente de dev pra cada projeto diferente. Já passou da hora de não precisar mais.</p>
<p>Eu ODEIO ter que fazer especificação de coisas idiotas como mapear campos numa API mal feita de fintech/banco. Já passou da hora de não precisar mais.</p>
<p>Eu ODEIO ter quinhentos frameworks mal feitos de front-end pra costurar tudo junto e rezar pra funcionar na tentativa e erro. Já passou da hora de não precisar mais.</p>
<p>Eu ODEIO ter que participar de reunião de sprint, onde o que é pedido vai sendo modificado ao longo do caminho porque ninguém prestou atenção. Já passou da hora de não precisar mais.</p>
<p>Eu ODEIO ter impedimento, ter que ficar esperando, porque outro dev ou outra equipe tá trabalhando em coisa que afeta o meu lado e no final vou gastar dias depois só ajustando conflito na mão. Já passou da hora de não precisar mais.</p>
<p>Quem são os &ldquo;Haters de IA&rdquo;: justamente todo mundo que me causava impedimento antes, responsáveis por esticar mais do que deveria, e entregar em qualidade ruim, todas as tarefas MUNDANAS, e achar que está fazendo grande coisa.</p>
<h2>O que a História me Ensinou<span class="hx:absolute hx:-mt-20" id="o-que-a-história-me-ensinou"></span>
    <a href="#o-que-a-hist%c3%b3ria-me-ensinou" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Computadores entendem instruções de máquina. Elas estão cagando e andando pras nossas &ldquo;linguagens favoritas&rdquo;. É o que eu gastei horas explicando nos podcasts e nos videos do meu canal.</p>
<p>Foda-se sua linguagem/framework favorito. Foda-se seu design pattern favorito. No final, a máquina só se interessa pelo binário que vai rodar.</p>
<p>Antigamente, era extremamente custoso informar essas instruções pra máquina. Tínhamos que, literalmente, entrar bit a bit com cada instrução, no endereço exato na memória. Seja com FIOS ou com SWICHES, UM BIT DE CADA VEZ:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/7972f02c9ad7fe9bc30d1493b1295188.jpg" alt="eniac"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/NV_0103_Driscoll_Large.jpg" alt="Altair 8800"  loading="lazy" /></p>
<p>Felizmente, as coisas evoluíram e melhoramos as formas de &ldquo;INPUTAR&rdquo; instruções e dados. Seja com papel perfurado ou teletypes (máquinas de escrever elétricas adaptadas como terminais burros):</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/s-l400.jpg" alt="cartão perfurado"  loading="lazy" /></p>
<p><a href="https://www.youtube.com/watch?v=zeL3mbq1mEg"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-24_12-55-27.jpg" alt="teletype"  loading="lazy" /></a></p>
<p>Era caro programar, porque era MUITO custoso &ldquo;consertar&rdquo; um erro. Não tinha &ldquo;backspace&rdquo; fácil. Na hora que ia digitar, você tinha que ter MUITA CERTEZA do que ia digitar, sem errar!</p>
<p>Mesmo no fim dos anos 70, começo dos anos 80, já tinha evoluído muito, mas ter armazenamento permanente era uma coisa OPCIONAL na maioria dos &ldquo;micro&rdquo; computadores. Tínhamos que digitar os programas do zero pra rodar e quando a máquina desligava, o que estava em RAM apagava e perdíamos tudo. Gravar era caro, uma das opções mais populares, era gravar em fita cassete:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/c64set.jpg" alt="c64set"  loading="lazy" /></p>
<p>Mas pra gravar/ler somente míseros 1 kilobyte de dados, poderia levar MAIS de 20 segundos. Um joguinho de 10 kilobytes (que é bem pouco), levaria quase 4 MINUTOS pra carregar.</p>
<p>Você nem tem ideia do que significam 10 kilobytes. Qualquer ícone PNG ou SVG ou qualquer arquivinho idiota de JS no seu website tem muito mais do que isso.</p>
<p>Tudo isso pra dizer o seguinte:</p>
<ul>
<li>computadores rodam instruções de máquina, até hoje</li>
<li>sempre foi caro informar essas instruções pro computador</li>
</ul>
<p>Hoje em dia temos SSD NVME que me permitem ler 1 gigabyte hoje (&lt;70ms) mais rápido do que eu lia 10 kilobytes nos anos 80.</p>
<p>Mas pra compensar, nossos programas foram ficando mais e mais complexos e &ldquo;gordos&rdquo;. Uma vez que memória, armazenamento, barramento de dados, tudo ficou de ordens de grandeza mais rápido, nós humanos, fomos jogando mais e mais dados neles. Um editor de textos hoje, faz a mesma coisa que um editor de textos nos anos 80: edita texto. Mas, lógico, tem muito mais funcionalidades: fontes bonitas, smooth scrolling, auto-correct, auto-format, etc.</p>
<p>Trocamos recursos por conforto, e isso é uma boa coisa.</p>
<p>Falei nos meus videos, nos posts de rant recentes, que foi uma coisa muito ruim a bolha de startups que estourou só em 2022 (e continua repercutindo até hoje). Trocamos eficiência de programação por programadores ruins baratos. Pra que tentar ser mais eficiente se era &ldquo;barato&rdquo; só colocar mais &ldquo;braços&rdquo; pra resolver o problema? A mesma mentalidade que permitiu o surgimento de fábricas de software baratas nos anos 2000, fazendo software ruim como se amanhã não houvesse.</p>
<p>Eu, pessoalmente, fico muito contente por finalmente termos dado mais um passo para conseguir informar instruções à máquina com mais eficiência. LLMs, nossos papagaios estocásticos, são, de fato, a forma mais eficiente de produzir 90% das instruções que precisamos fornecer pras máquinas computarem o que precisamos. Da mesma forma que eu não sinto falta de cartão perfurado, nem de teletipos, nem de fita cassete, se eu não precisar mais usar uma IDE no meu dia a dia, não vou sentir falta.</p>
<blockquote>
  <p>Eu não escolhi me tornar um programador pra virar operador de IDE. Quando eu comecei, nem existia o conceito de IDEs.</p>

</blockquote>
<p>Eu escolhi me tornar um programador porque eu gosto da ideia de instruir uma máquina a computar as coisas que eu quero. Seja uma planilha, seja um jogo. COMO essas instruções são inputadas, não é o principal pra mim. Nunca foi. IDEs foram só uma pequena fase dentro de décadas de carreira e não serão as últimas.</p>
<blockquote>
  <p>Eu não entendo o raciocínio nem dos empolgados demais nem dos haters demais. Por que você precisa glorificar um martelo? Por que você precisa odiar um martelo? Era só isso que eu precisava dizer 🤷‍♂</p>

</blockquote>
<h2>Algumas Desculpas Idiotas<span class="hx:absolute hx:-mt-20" id="algumas-desculpas-idiotas"></span>
    <a href="#algumas-desculpas-idiotas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><em>&ldquo;Mas a Anthropic (ou OpenAI) são do mal&rdquo;</em></p>
<p>Foda-se. Microsoft também era, passou até por julgamento anti-truste no ano 2000 que quase dividiu a empresa. Mesmo se eu prefiro Mac ou Linux hoje, Windows deixou de ser o sistema operacional mais usado do mundo? Não.</p>
<p><em>&ldquo;Mas IA usa muita energia, e o meio ambiente?&rdquo;</em></p>
<p>Foda-se. Isso nunca foi um consenso (e mais provavelmente nunca vai ser e nem é um problema). O planeta já passou por 5 ou 6 grandes de eventos de extinção antes, o último foi um meteoro que caiu e dizimou os dinossauros. Já passamos por múltiplas Eras do Gelo. O planeta vai continuar bem, não se preocupem.</p>
<p>O problema da última década foi a imbecilidade de não terem investido em mais usinas nucleares - e agora essa opção finalmente voltou pra mesa. Eu disse no Flow que a Alemanha fechando as delas foi uma das decisões mais idiotas de todos os tempos, e reafirmo: foi.</p>
<p>São as duas mais prevalentes de que me lembro hoje. Mas é isso: qualquer coisa é desculpa agora. A caixa de pandora foi aberta, não tem mais volta.</p>
]]></content:encoded><category>inteligencia-artificial</category><category>llms</category><category>vibe-coding</category></item><item><title>Vibe Code: Fiz um Indexador Inteligente de Imagens com IA em 2 dias | Frank Sherlock</title><link>https://www.akitaonrails.com/2026/02/23/vibe-code-fiz-um-indexador-inteligente-de-imagens-com-ia-em-2-dias/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/02/23/vibe-code-fiz-um-indexador-inteligente-de-imagens-com-ia-em-2-dias/</guid><pubDate>Mon, 23 Feb 2026 18:34:34 +0000</pubDate><description>&lt;p&gt;Nas últimas 48 horas, construí do zero um aplicativo desktop completo, com binários publicados para Linux, macOS e Windows. 50 commits, ~26 horas de trabalho efetivo, 8.359 linhas de Rust, 5.842 de TypeScript, 338 testes automatizados. Se você me dissesse isso 2 anos atrás, eu chamaria de mentira.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://github.com/akitaonrails/FrankSherlock/raw/master/docs/frank_sherlock.png" alt="frank sherlock" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;O nome é &lt;a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener"&gt;&lt;strong&gt;Frank Sherlock&lt;/strong&gt;&lt;/a&gt; — um sistema local de catalogação e busca de imagens usando IA. Você aponta pra uma pasta (pode ser um NAS com terabytes), ele escaneia tudo, classifica cada arquivo usando um LLM de visão rodando localmente na sua GPU, e te dá busca full-text sobre o conteúdo. Não é cloud, não manda nada pra fora, roda 100% na sua máquina.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Nas últimas 48 horas, construí do zero um aplicativo desktop completo, com binários publicados para Linux, macOS e Windows. 50 commits, ~26 horas de trabalho efetivo, 8.359 linhas de Rust, 5.842 de TypeScript, 338 testes automatizados. Se você me dissesse isso 2 anos atrás, eu chamaria de mentira.</p>
<p><img src="https://github.com/akitaonrails/FrankSherlock/raw/master/docs/frank_sherlock.png" alt="frank sherlock"  loading="lazy" /></p>
<p>O nome é <a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener"><strong>Frank Sherlock</strong></a> — um sistema local de catalogação e busca de imagens usando IA. Você aponta pra uma pasta (pode ser um NAS com terabytes), ele escaneia tudo, classifica cada arquivo usando um LLM de visão rodando localmente na sua GPU, e te dá busca full-text sobre o conteúdo. Não é cloud, não manda nada pra fora, roda 100% na sua máquina.</p>
<p>Eis alguns exemplos de textos que ele extraiu de algumas das minhas imagens:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-23_16-08-03.png" alt="game boy example"  loading="lazy" /></p>
<p>E olha os detalhes do Surya OCR: ele leu direitinho o texto na tela do Game Boy:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-23_16-08-14.png" alt="gameboy screen ocr"  loading="lazy" /></p>
<p>Mais do que isso, eu tenho diretórios de screenshots de comprovantes de pagamento. Nunca mais ia achar nada ali, mas agora:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-23_16-07-37.png" alt="santander comprovante"  loading="lazy" /></p>
<p>Embora ele faça alguns erros, claro, animes obscuros (ele fica achando que tudo que ele não reconhece é Evangelion 😂). Mas muitos ele surpreendentemente acerta também. Mas as descrições em si já ajudam muito.</p>
<p>Vai precisar de um mínimo de GPU (eu só testei na minha 5090, mas é pra baixar modelos menores pra GPUs menores, e teoricamente suporta AMD e Mac também, mas ainda não testei - aceito Issues e Pull Requests pra quem quiser fazer beta-testing em Mac e Windows). Basta ter o Ollama instalado e rodando; opcionalmente, Python (pra ter o Surya OCR, que é opcional, mas é o melhor).</p>
<p>Vou colocar como &ldquo;1.0&rdquo; quando tiver mais gente testado em Windows/macOS e eu tiver os certificados pra assinar os executáveis direitinho. Esta é uma versão &ldquo;beta&rdquo; ainda! Compilem e rodem vocês mesmos em suas máquinas, tá tudo explicado no <a href="https://github.com/akitaonrails/FrankSherlock/blob/master/README.md"target="_blank" rel="noopener">README</a>.</p>
<p>E eu fiz isso com meu <a href="2026/02/20/do-zero-a-pos-producao-em-1-semana-como-usar-ia-em-projetos-de-verdade-bastidores-do-the-m-akita-chronicles/">&ldquo;Agile Vibe Coding&rdquo;</a> — basicamente, programar em parceria com um LLM (no caso, Claude Code).</p>
<blockquote>
  <p><strong>Agile Vibe Coding funciona. E funciona muito bem. Mas a ideia é só 10% do trabalho. Os outros 90% são engenharia.</strong></p>

</blockquote>
<p>Engenharia requer experiência, julgamento, e saber fazer as perguntas certas. O LLM é um executor excelente. Mas quem decide o que executar, em que ordem, e por quê, esse continua sendo o trabalho do desenvolvedor.</p>
<p>Tem um discurso crescente de que qualquer pessoa com uma boa ideia pode construir software agora. Num certo sentido, sim, você pode colocar um protótipo no ar rápido. Mas a distância entre &ldquo;roda na minha máquina&rdquo; e &ldquo;software que funciona em 3 sistemas operacionais, sobrevive a cancelamentos no meio do processamento, não corrompe dados, e escala de 94 arquivos de teste pra 500 mil em produção&rdquo; continua sendo enorme. Esse gap é engenharia, e engenharia ainda exige alguém que saiba o que está fazendo.</p>
<p>Vou contar a história completa: da pesquisa inicial ao release, passando por benchmarks, prova de conceito, decisões de arquitetura, CI/CD multi-plataforma, e tudo que ficou entre &ldquo;eu tenho uma ideia&rdquo; e &ldquo;aqui está o AppImage, o DMG e o MSI&rdquo;.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-23_15-47-30.png" alt="4 image preview"  loading="lazy" /></p>
<h2>A Ideia Original<span class="hx:absolute hx:-mt-20" id="a-ideia-original"></span>
    <a href="#a-ideia-original" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Tudo começou com uma pergunta simples: será que LLMs open-source de visão conseguem classificar conteúdo de verdade? Não estou falando de &ldquo;mulher na praia&rdquo; — isso qualquer modelo faz. Estou falando de olhar pra uma imagem e dizer &ldquo;isso é a Ranma, do anime Ranma 1/2 da Rumiko Takahashi, numa cena do OVA The Battle for Miss Beachside&rdquo;. Será que chegamos nesse nível? (TL;DR não, mas o suficiente)</p>
<p>E se chegamos, dá pra fazer um catálogo inteligente de arquivos? Algo que eu aponte pro meu NAS com terabytes de mídia acumulada ao longo de décadas e consiga buscar por conteúdo, não por nome de arquivo? Quem tem um NAS em casa sabe: depois de alguns anos, os arquivos acumulam e a organização de diretórios simplesmente para de escalar. Você sabe que tem aquele comprovante de pagamento de 2019 em algum lugar, mas o arquivo se chama <code>IMG_20190315_142301.jpg</code> e está num diretório com mais 3 mil fotos.</p>
<p>Meu hardware: AMD 7850X3D, RTX 5090, Arch Linux. Restrição absoluta: nada de APIs remotas, nada de OpenAI, nada de cloud. Tudo open-source, tudo local. Se eu vou processar terabytes de arquivos pessoais, incluindo documentos financeiros e fotos privadas, não quero mandar nada pra servidor de terceiros. Fora que o custo de API cloud pra esse volume seria proibitivo.</p>
<p>Mas primeiro: pesquisa. Sem saber se a tecnologia entrega o que eu preciso, não faz sentido construir nada. Scripts pequenos, protótipos rápidos, modelos diferentes. Ver o que funciona antes de escrever a primeira linha de código do app de verdade. Esse é um padrão que eu sigo há anos: valide a suposição mais arriscada primeiro. Se o LLM de visão não conseguir classificar decentemente, todo o resto é desperdício.</p>
<h2>Pesquisa A/B: Benchmark Driven Development<span class="hx:absolute hx:-mt-20" id="pesquisa-ab-benchmark-driven-development"></span>
    <a href="#pesquisa-ab-benchmark-driven-development" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa é a parte que a maioria das pessoas pula, e é exatamente onde a experiência faz diferença. A tentação de pular direto pra implementação é enorme. <em>&ldquo;Vou usar o modelo X porque li num blog que é bom.&rdquo;</em> Não. Antes de escolher modelo, framework, ou arquitetura, eu montei um <a href="https://github.com/akitaonrails/FrankSherlock/tree/master/_research_ab_test"target="_blank" rel="noopener">benchmark formal</a>.</p>
<p>Montei um corpus de teste com 94 arquivos: 60 imagens (fotos, screenshots, anime, documentos, receipts), 9 áudios, 13 vídeos e 12 documentos. Pra cada arquivo, criei um ground truth em JSON com a classificação correta — tipo, descrição, série (quando aplicável). Esse ground truth é o que permite medir accuracy de verdade, não <em>&ldquo;olhei o resultado e pareceu OK&rdquo;</em>.</p>
<p>O benchmark tem 6 fases, cada uma respondendo a uma pergunta específica:</p>
<ol>
<li><strong>Metadata</strong>: quanto custa extrair metadata básica? (resposta: barato, 0.07s/arquivo)</li>
<li><strong>Imagens</strong>: qual modelo de visão é melhor? Qual OCR?</li>
<li><strong>Áudio</strong>: Whisper funciona? Qual tamanho de modelo?</li>
<li><strong>Vídeo</strong>: classificação por frames funciona?</li>
<li><strong>Catálogo unificado</strong>: o pipeline completo integrado funciona?</li>
<li><strong>Projeção de custo</strong>: quanto tempo e dinheiro pra processar um NAS real?</li>
</ol>
<h3>Fase 2: Visão — O Resultado Que Ninguém Esperava<span class="hx:absolute hx:-mt-20" id="fase-2-visão--o-resultado-que-ninguém-esperava"></span>
    <a href="#fase-2-vis%c3%a3o--o-resultado-que-ningu%c3%a9m-esperava" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Testei <code>qwen2.5vl:7b</code>, <code>llava:13b</code> e <code>minicpm-v:8b</code> em 30 imagens rotuladas. O resultado:</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th style="text-align: center">Type Accuracy</th>
          <th style="text-align: center">Series Accuracy</th>
          <th style="text-align: center">JSON Valid</th>
          <th style="text-align: center">Latência/img</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>qwen2.5vl:7b</td>
          <td style="text-align: center"><strong>0.80</strong></td>
          <td style="text-align: center"><strong>0.14</strong></td>
          <td style="text-align: center"><strong>0.87</strong></td>
          <td style="text-align: center">0.55s</td>
      </tr>
      <tr>
          <td>minicpm-v:8b</td>
          <td style="text-align: center">0.50</td>
          <td style="text-align: center">0.00</td>
          <td style="text-align: center">0.83</td>
          <td style="text-align: center">1.63s</td>
      </tr>
      <tr>
          <td>llava:13b</td>
          <td style="text-align: center">0.33</td>
          <td style="text-align: center">0.06</td>
          <td style="text-align: center">0.83</td>
          <td style="text-align: center">1.62s</td>
      </tr>
  </tbody>
</table>
<p>O modelo de 7B parâmetros ganhou de lavada dos maiores. Não é typo. O <code>qwen2.5vl:7b</code> bateu o <code>llava:13b</code> (quase o dobro do tamanho) em todas as métricas, e ainda foi 3x mais rápido. Isso contradiz a intuição de <em>&ldquo;modelo maior = modelo melhor&rdquo;</em>. Na prática, depende do task e do prompt.</p>
<p>Naturalmente, a próxima pergunta é: e o 32B? Mesmo modelo, versão gigante. A gente deve conseguir bem mais, certo? Errado:</p>
<table>
  <thead>
      <tr>
          <th>Modelo</th>
          <th style="text-align: center">Type Accuracy</th>
          <th style="text-align: center">Series Accuracy</th>
          <th style="text-align: center">Latência/img</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>qwen2.5vl:7b</td>
          <td style="text-align: center">0.87</td>
          <td style="text-align: center">0.19</td>
          <td style="text-align: center"><strong>0.77s</strong></td>
      </tr>
      <tr>
          <td>qwen2.5vl:32b</td>
          <td style="text-align: center">0.87</td>
          <td style="text-align: center">0.25</td>
          <td style="text-align: center">22.46s</td>
      </tr>
  </tbody>
</table>
<p>O 32B deu +0.06 em <em>series accuracy</em> (literalmente 1 acerto a mais em 16 itens rotulados) e custou <strong>29x mais tempo</strong>. Pra quem vai processar centenas de milhares de arquivos, essa troca não fecha. 29x mais lento significa que um job de 6 horas vira um job de uma semana.</p>
<p>Aqui faço um comentário sobre ferramenta: fiz a primeira rodada com Claude Code e tinha pedido pra ele escolher os modelos que achava melhores. Mas depois decidi ir pro GPT Codex e ele fez outras sugestões que achei interessante testar. Em resumo: tenho achado Codex muito melhor pra <strong>experimentação</strong> e <strong>código exploratório</strong>, pra pesquisa mesmo. Claude eu acho melhor quando já sabemos melhor exatamente o que queremos.</p>
<p>Com o Codex, testei os novos candidatos <code>qwen3-vl:8b</code> e <code>qwen3-vl:30b-a3b</code> com 3 repetições para fins de significância estatística. Resultado? Ambos <em>piores</em> que o <code>qwen2.5vl:7b</code> — <code>type_accuracy</code> de 0.55 contra 0.89 do incumbent, com intervalo de confiança de 95% que nem se aproxima. E ainda mais lentos: 2x e 2.2x respectivamente. Modelo mais novo nem sempre é modelo melhor pro seu caso de uso. O qwen3-vl frequentemente retornava JSON truncado ou mal-formado — uma regressão real em robustez.</p>
<h3>OCR: Surya vs Ollama Vision<span class="hx:absolute hx:-mt-20" id="ocr-surya-vs-ollama-vision"></span>
    <a href="#ocr-surya-vs-ollama-vision" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Para extração de texto (documentos escaneados, receipts, screenshots com texto), testei dois engines:</p>
<table>
  <thead>
      <tr>
          <th>Engine</th>
          <th style="text-align: center">Cobertura</th>
          <th style="text-align: center">Similaridade</th>
          <th style="text-align: center">Latência/img</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Surya</td>
          <td style="text-align: center">54/65 arquivos</td>
          <td style="text-align: center">0.9455</td>
          <td style="text-align: center">8.15s</td>
      </tr>
      <tr>
          <td>Ollama Vision</td>
          <td style="text-align: center">38/65 arquivos</td>
          <td style="text-align: center">0.9419</td>
          <td style="text-align: center">1.73s</td>
      </tr>
  </tbody>
</table>
<p>Surya encontra texto em muito mais arquivos (83% vs 58%), mas é 5x mais lento. Quando encontra texto, a qualidade é praticamente igual (similarity &gt; 0.94 nos dois). Solução óbvia: abordagem híbrida. Usa Surya quando precisa de cobertura máxima, Ollama Vision como fallback rápido. O design do pipeline ficou: tenta Surya → se falhar ou não encontrar texto → fallback pra Ollama Vision. Por isso falei no começo que Surya é opcional, se não quiser instalar.</p>
<h3>Projeção de Custo<span class="hx:absolute hx:-mt-20" id="projeção-de-custo"></span>
    <a href="#proje%c3%a7%c3%a3o-de-custo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A fase 6 fez algo que raramente vejo em projetos open-source: projeção de custo pra uso real. Com os timings medidos por tipo de arquivo, extrapolamos pra cenários de NAS:</p>
<table>
  <thead>
      <tr>
          <th>Cenário</th>
          <th style="text-align: center">Arquivos</th>
          <th style="text-align: center">Tempo</th>
          <th style="text-align: center">Custo Elétrico</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Corpus teste</td>
          <td style="text-align: center">94</td>
          <td style="text-align: center">24 min</td>
          <td style="text-align: center">$0.02</td>
      </tr>
      <tr>
          <td>NAS pequeno</td>
          <td style="text-align: center">~5K</td>
          <td style="text-align: center">6.6 h</td>
          <td style="text-align: center">$0.36</td>
      </tr>
      <tr>
          <td>NAS médio</td>
          <td style="text-align: center">~50K</td>
          <td style="text-align: center">2.6 dias</td>
          <td style="text-align: center">$3.37</td>
      </tr>
      <tr>
          <td>NAS grande</td>
          <td style="text-align: center">~500K</td>
          <td style="text-align: center">26 dias</td>
          <td style="text-align: center"><strong>$33.70</strong></td>
      </tr>
  </tbody>
</table>
<p>Leve esses números com algum ceticismo porque é conta em papel de pão. $34 de eletricidade pra classificar 500 mil arquivos com GPU local. Tenta fazer isso com API do GPT-4 Vision — a $0.01 por imagem (conservador), são $5.000. O preço do meu setup (GPU + eletricidade) se paga no primeiro uso grande.</p>
<p>A lição: <strong>faça benchmark, não adivinhe</strong>. Eu poderia ter assumido que o modelo maior seria melhor, ou que o mais novo superaria o antigo. Os dados mostraram o contrário. ~2 horas de benchmarks me pouparam de escolhas erradas que custariam dias de retrabalho (antigamente eu diria &ldquo;semanas&rdquo;).</p>
<h2>Prova de Conceito em Python<span class="hx:absolute hx:-mt-20" id="prova-de-conceito-em-python"></span>
    <a href="#prova-de-conceito-em-python" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Com os benchmarks em mãos, o próximo passo foi validar o pipeline completo antes de partir pro Rust. Não o pipeline de benchmark (que testa cada modelo isoladamente), mas o pipeline de classificação real — a sequência exata de chamadas que o app final vai fazer pra cada arquivo.</p>
<p>Criei um protótipo em Python — 754 linhas em um único arquivo (<a href="https://github.com/akitaonrails/FrankSherlock/blob/master/_classification/run_classification.py"target="_blank" rel="noopener"><code>classification/run_classification.py</code></a>) — que implementava a estratégia vencedora dos benchmarks.</p>
<p>O pipeline tem 4 estágios de enriquecimento:</p>
<ol>
<li><strong>Classificação primária</strong>: manda a imagem pro <code>qwen2.5vl:7b</code> com prompt estruturado, pede JSON com tipo, descrição, tags, confidence</li>
<li><strong>Enriquecimento anime</strong>: se o tipo primário for anime/manga/cartoon, faz uma segunda passada com prompt especializado pedindo série, personagem, cena, artista</li>
<li><strong>Documento/OCR</strong>: se for documento ou receipt, extrai texto com Surya e/ou Ollama Vision, depois pede dados estruturados (datas, valores, IDs de transação)</li>
<li><strong>Output</strong>: grava resultado em YAML espelhando a estrutura de diretórios de origem</li>
</ol>
<p>A parte mais crítica é o parsing de JSON do LLM. Quem já trabalhou com output de LLMs sabe que eles são&hellip; criativos com formatação. Às vezes vem com markdown fences (<code>json ... </code>). Às vezes tem texto antes e depois do JSON. Às vezes o JSON está quase certo mas falta fechar uma chave. O protótipo implementou uma cascata de 3 tentativas que depois virou a regra do projeto inteiro:</p>
<ol>
<li><strong>Parse direto</strong>: <code>json.loads(response)</code> — funciona em ~70% dos casos</li>
<li><strong>Extração por balanceamento de chaves</strong>: encontra o primeiro <code>{</code>, conta chaves abertas e fechadas, extrai o substring — pega mais ~20%</li>
<li><strong>Regex field salvage</strong>: se tudo mais falhar, usa regex pra extrair campos individuais (&ldquo;type&rdquo;: &ldquo;&hellip;&rdquo;, &ldquo;description&rdquo;: &ldquo;&hellip;&rdquo;) — salva os últimos ~10%</li>
</ol>
<p>Essa cascata ficou no código Rust praticamente idêntica. PoC bem feito encurta o caminho pra produção.</p>
<p>Resultado: 60 imagens processadas, zero erros, 6.29s/imagem de média. O pipeline funcionava de ponta a ponta. Hora de construir o app de verdade.</p>
<h2>Construindo o App Tauri<span class="hx:absolute hx:-mt-20" id="construindo-o-app-tauri"></span>
    <a href="#construindo-o-app-tauri" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Nesse ponto, tentei continuar com o Codex, mas aí ele se engasgou em tentar converter o Poc em Python pra Tauri. Mas como agora as escolhas já foram feita, voltei pro Claude Code, e ele não teve problemas em mapear do Python pra Rust.</p>
<p>Aqui é onde o vibe coding mostra a que veio. Vou contar a timeline real, commit por commit, pra você ter noção do ritmo. Os timestamps são do git log, então são precisos.</p>
<h3>Sábado 21/02 (19:29 → 21:08) — 6 commits, 1h39<span class="hx:absolute hx:-mt-20" id="sábado-2102-1929--2108--6-commits-1h39"></span>
    <a href="#s%c3%a1bado-2102-1929--2108--6-commits-1h39" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Tudo começou com <a href="https://github.com/akitaonrails/FrankSherlock/tree/master/_research_ab_test"target="_blank" rel="noopener">research</a>. Seis commits de setup, scripts de benchmark, análise de resultados:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>f57221c 19:29 Phase 0: Project setup with uv, environment verification
4977497 19:45 Implement all phases: metadata, image/audio/video classification
af6e3aa 19:49 Add research results report
41d6c2a 20:12 Add per-file timing, OCR phase, cost estimation
0cd1b10 21:02 Fix Surya OCR, re-run on 94-file corpus
25b3ace 21:08 Add conclusions, cost analysis, recommended pipeline</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Sábado à noite, só pesquisa pura. Nenhuma linha de código do app. Mas quando fui dormir, sabia qual modelo usar, qual abordagem de OCR, e quanto ia custar em tempo e eletricidade.</p>
<h3>Domingo 22/02 (13:05 → 23:30) — 14 commits, ~10h25<span class="hx:absolute hx:-mt-20" id="domingo-2202-1305--2330--14-commits-10h25"></span>
    <a href="#domingo-2202-1305--2330--14-commits-10h25" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O domingo foi o dia de construção pesada. Acordei, almocei, e sentei pra trabalhar.</p>
<p><strong>13:05</strong> — Primeiro commit do dia: o protótipo Python de classificação. 749 linhas validando o pipeline completo de classificação, a cascata de JSON parsing, e o enriquecimento condicional. Esse protótipo não era throwaway — ele era o design document executável do pipeline Rust que viria depois.</p>
<p>Dei uma pausa pra pra dar uma saída, passear, tomar um vinho, depois voltei e continuei:</p>
<p><strong>17:31</strong> — Benchmarks atualizados com os novos candidatos (qwen3-vl:8b e qwen3-vl:30b-a3b). Três repetições cada, intervalos de confiança. Confirmaram que o qwen2.5vl:7b era a escolha certa — não por pouco, mas por uma margem enorme.</p>
<p><strong>17:56</strong> — O &ldquo;big bang&rdquo;: scaffold do app Tauri. <strong>9.631 linhas inseridas</strong> num único commit. A estrutura inteira do app: Rust backend com SQLite + FTS5, React frontend, config system, scanner de arquivos, modelos de dados, query parser pra busca natural. Nesse ponto o app já buscava no banco. Não tinha classificação ainda, mas a fundação estava sólida — e é exatamente o ponto. O scaffold veio com testes, tipos TypeScript, e a arquitetura de diretórios que eu defini. O Claude gerou o código, mas a estrutura de módulos (config, db, scan, models, query_parser) veio de como eu queria organizar as responsabilidades.</p>
<p><strong>19:04</strong> — O commit mais pesado do projeto: <strong>4.186 linhas</strong>. Classification pipeline em Rust (1.069 linhas de <code>classify.rs</code> — praticamente a tradução do PoC Python), geração de thumbnails (Lanczos3, 300px, JPEG 80%), scanning incremental com fingerprint, o script Surya OCR em Python, runtime detection pra Ollama, e a expansão brutal do banco de dados com upsert, touch, delete e FTS indexing. Num commit. Com 47 testes. Eu tive que tomar as decisões de arquitetura (como o cache espelha o rel_path, como o scan é dividido em duas fases, como os erros propagam), mas o Claude escreveu o grosso do código e os testes que vieram junto.</p>
<p><strong>19:55</strong> — Redesign de UI, cancellation de scan, auto-cleanup de classificações órfãs, reorganização do repo inteiro (movi tudo de research pra <code>_research_ab_test/</code>). CI e release workflow já configurados nesse commit — eu sabia que ia precisar deles no dia seguinte.</p>
<p><strong>21:02</strong> — DB resilience (WAL mode, backup, health check), gerenciamento de roots (adicionar, remover, listar diretórios monitorados), zoom, redesign do sidebar com tree view, fix de thumbnails. Quatro features num commit. Num workflow manual, cada uma dessas seria um PR separado com review de dias.</p>
<p><strong>21:14 → 21:39</strong> — Read-only database mode pra filesystems sandbox, resume de scans interrompidos no startup, redesign de grid tiles com hover overlay, modelo de seleção, infinite scroll. Três commits em 25 minutos. O Claude estava num ritmo absurdo.</p>
<p><strong>22:41</strong> — Multi-select com Ctrl/Shift click, preview de collage com os itens selecionados, Ctrl+C copia pro clipboard do OS.</p>
<p><strong>23:30</strong> — O grande refactor: frontend monolítico (tudo no <code>App.tsx</code> - se não explicar, Claude sempre faz isso) desmembrado em 15 componentes + 10 hooks + 84 testes frontend. Esse é o tipo de coisa que normalmente leva um dia inteiro de trabalho tedioso. Com vibe coding, foi um commit de uma hora. O Claude extraiu cada componente, criou os hooks, montou os testes com mocks adequados, e manteve tudo funcionando. Eu só precisei dizer <em>&ldquo;refatora esse monolito em componentes e hooks, e escreve testes pra cada um&rdquo;</em>.</p>
<h3>Segunda-feira 23/02 (00:09 → 14:33) — 30 commits, ~14h<span class="hx:absolute hx:-mt-20" id="segunda-feira-2302-0009--1433--30-commits-14h"></span>
    <a href="#segunda-feira-2302-0009--1433--30-commits-14h" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A segunda foi de polimento, robustez e a maratona de cross-platform.</p>
<p><strong>00:09</strong> — Abstração multi-OS: todo código específico de plataforma isolado no módulo <code>platform/</code>. Essa decisão, tomada cedo, salvou horas de dor no CI. Quando o Windows precisou de tratamento especial pra paths UNC, a mudança ficou contida em <code>platform/process.rs</code> em vez de espalhada em 8 arquivos.</p>
<p><strong>00:12 → 01:18</strong> — Sequência rápida: cargo audit no CI, remoção de duplicações, licença GPL-3.0, help dialog (F1) com exemplos de query syntax, sort controls pra resultados, sistema de migração SQLite formal, context menu (copy, delete, rename). Seis commits em pouco mais de uma hora.</p>
<p><strong>09:38</strong> — Após dormir umas 8 horas, o primeiro commit da manhã: extração do módulo LLM. O monolito de chamadas ao Ollama que vivia dentro de <code>classify.rs</code> foi separado em <code>llm/client.rs</code> (chamadas HTTP, parsing JSON), <code>llm/management.rs</code> (download, listagem, cleanup de modelos) e <code>llm/model_selection.rs</code> (seleção hardware-aware por tier). Atomic file ops pra não corromper cache se o processo morrer no meio de uma escrita.</p>
<p><strong>10:54</strong> — Extração de localização EXIF (GPS coordinates → endereço legível), modal de edição de metadata, setup instructions por OS (cada OS tem dependências diferentes pro Ollama e Python).</p>
<p><strong>11:34</strong> — Suporte a PDF: scanning, indexing e preview usando PDFium. Não é trivial — PDFium precisa de binário nativo por plataforma, renderização de páginas em imagem, detecção de páginas em branco (pra não gerar thumbnail de uma capa vazia), montagem de thumbnail com as 2 primeiras páginas com conteúdo, e extração de texto nativo como alternativa mais rápida ao OCR.</p>
<p><strong>12:08</strong> — Albums e Smart Folders. Albums são coleções manuais (o usuário arrasta arquivos pra dentro), Smart Folders são queries salvas que aparecem no sidebar e atualizam automaticamente. Duas migrações de banco, novo componente de sidebar, drag-and-drop. Em 34 minutos.</p>
<p><strong>12:11 → 12:52</strong> — Ícones SVG macOS-inspired no sidebar, suporte a argumento CLI (<code>sherlock /path/to/folder</code>), copy description/OCR text no context menu, fix de path do PDFium em produção, ícones e screenshots no README, script <code>tauri</code> no npm, sidebar toggle, titlebar dinâmico, fix de compilação Windows (imports Unix-only). Dez commits em 41 minutos. A maioria desses foram issues que apareceram no CI ou durante teste manual.</p>
<p><strong>13:14 → 13:54</strong> — A maratona de CI fixes. Auto-provision do venv Surya OCR com barra de progresso no SetupModal, regeneração de ícones com alpha channel, <code>cargo fmt</code> + clippy + fix de UNC paths no Windows, testes pros exemplos do help dialog, rescan de pasta individual, fix de assertions Windows, fix de permissions do release workflow. Sete commits de hardening. Cada um resolvendo um bug real que aparecia no CI matrix ou em teste.</p>
<p><strong>14:26</strong> — Drag-and-drop pra reordenar roots no sidebar, cancelamento de scan antes de deletar um root (pra não deixar o scan rodando em background num diretório que o usuário removeu — edge case sutil que poderia causar crash).</p>
<p><strong>14:33</strong> — Último commit: fix de responsividade no cancelamento de scan. Check do cancel flag após cada classificação, poll imediato em vez de esperar o próximo tick. Detalhe pequeno, impacto grande na UX.</p>
<h2>Decisões de Arquitetura<span class="hx:absolute hx:-mt-20" id="decisões-de-arquitetura"></span>
    <a href="#decis%c3%b5es-de-arquitetura" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Aqui é o que separa <em>&ldquo;deixar o LLM escrever código&rdquo;</em> de &ldquo;construir software de verdade&rdquo;. Nenhuma dessas decisões veio de prompt. O Claude não sugeriu nenhuma espontaneamente. Eu tive que pedir cada uma.</p>
<h3>Princípio Read-Only<span class="hx:absolute hx:-mt-20" id="princípio-read-only"></span>
    <a href="#princ%c3%adpio-read-only" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O app <strong>nunca</strong> escreve nos diretórios escaneados. Tudo — thumbnails, cache de classificação, banco de dados — fica em <code>~/.local/share/frank_sherlock/</code>. Isso não é só boa prática, é respeito pelo dado do usuário. Se alguém aponta pro NAS da empresa, o app não pode sair criando <code>.sherlock/</code> em cada subdiretório. Se o diretório é montado como read-only via NFS, o app precisa funcionar normalmente. Parece óbvio, mas muitos apps de catalogação que você conhece criam caches e thumbnails <strong>dentro</strong> dos diretórios de origem. (cof Synology @eaDir cof)</p>
<h3>Scanning Incremental<span class="hx:absolute hx:-mt-20" id="scanning-incremental"></span>
    <a href="#scanning-incremental" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Escanear terabytes de dados toda vez que o app abre seria insano. O scan é incremental em dois sentidos:</p>
<ol>
<li><strong>Fase de descoberta</strong> (rápida): caminha o filesystem comparando mtime + tamanho de cada arquivo. Se nada mudou desde o último scan, nem lê o conteúdo — apenas atualiza o marcador de &ldquo;visto neste scan&rdquo;. Pra um NAS com 500K arquivos onde 99% não mudou, essa fase leva segundos, não horas.</li>
<li><strong>Fase de processamento</strong> (pesada): só pra arquivos novos ou modificados. Calcula fingerprint (SHA-256 dos primeiros 64KB), gera thumbnail, classifica com o LLM. E aqui entra a detecção de movidos: se um arquivo mudou de path mas o fingerprint é o mesmo, o app preserva toda a classificação já feita e apenas atualiza o path. Você reorganizou 10 mil fotos em pastas novas? O app detecta e não reclassifica nenhuma.</li>
</ol>
<p>O checkpoint é por arquivo. Se o scan for interrompido (o app crashou, o usuário fechou, acabou a luz), na próxima vez ele retoma do último arquivo processado, não do zero. Isso é implementado via scan job persistence no banco: o cursor do scan fica salvo em <code>scan_jobs</code>.</p>
<h3>Cancelamento Cooperativo<span class="hx:absolute hx:-mt-20" id="cancelamento-cooperativo"></span>
    <a href="#cancelamento-cooperativo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O scan roda numa thread separada via <code>tokio::spawn_blocking</code>. Para cancelar, uso <code>AtomicBool</code> compartilhado entre a thread de scan e o frontend. O flag é checado:</p>
<ul>
<li>Antes de cada arquivo na fase de descoberta</li>
<li>Antes de cada classificação na fase de processamento</li>
<li>Após cada classificação (pro caso de a chamada ao Ollama demorar)</li>
</ul>
<p>Isso garante que o cancelamento responde em no máximo o tempo de uma classificação (~1 segundo), não no tempo do scan inteiro. Sem esse design, cancelar um scan de 500K arquivos poderia levar minutos — ou simplesmente não funcionar.</p>
<h3>Resiliência do Banco<span class="hx:absolute hx:-mt-20" id="resiliência-do-banco"></span>
    <a href="#resili%c3%aancia-do-banco" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>SQLite com WAL mode (permite leituras concorrentes durante escrita), health check no startup, backup automático antes de migrações, sistema de migração formal via <code>rusqlite_migration</code>. Cinco migrações no total:</p>
<ol start="0">
<li>Schema inicial (tabelas de arquivos, roots, scan_jobs, e a virtual table FTS5 pra busca)</li>
<li>Coluna <code>location_text</code> pra endereço EXIF</li>
<li>Rebuild do índice FTS (necessário após mudar a tokenização)</li>
<li>Tabelas <code>albums</code> e <code>album_files</code> pra coleções manuais</li>
<li>Tabela <code>smart_folders</code> pra queries salvas</li>
</ol>
<p>As migrações são identificadas por posição e nunca podem ser editadas ou reordenadas depois de publicadas. Isso é o tipo de regra que você aprende depois de corromper o banco de produção de alguém uma vez. A regra está codificada no CLAUDE.md do projeto pra que futuras sessões de vibe coding não a violem.</p>
<h3>Seleção de Modelo Hardware-Aware<span class="hx:absolute hx:-mt-20" id="seleção-de-modelo-hardware-aware"></span>
    <a href="#sele%c3%a7%c3%a3o-de-modelo-hardware-aware" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Nem todo mundo tem uma RTX 5090. O app detecta a GPU no startup e escolhe o modelo adequado:</p>
<ul>
<li><strong>GPU fraca ou sem GPU</strong>: <code>qwen2.5vl:3b</code> (tier small — roda em qualquer coisa)</li>
<li><strong>GPU com &gt;= 6GB VRAM</strong>: <code>qwen2.5vl:7b</code> (tier medium, o default dos benchmarks)</li>
<li><strong>Apple Silicon com &gt;= 48GB unificados</strong>: <code>qwen2.5vl:32b</code> (tier large, só onde a memória unificada permite sem swap)</li>
</ul>
<p>A detecção usa <code>nvidia-smi</code> no Linux/Windows, <code>system_profiler</code> no macOS, e <code>sysinfo</code> como fallback pra RAM do sistema. O resultado é cacheado no <code>AppState</code> do Tauri pra não ficar executando subprocesso toda hora.</p>
<h2>Multi-OS e CI/CD<span class="hx:absolute hx:-mt-20" id="multi-os-e-cicd"></span>
    <a href="#multi-os-e-cicd" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Se tem uma parte do projeto que justifica ter experiência, é essa. Fazer software que compila é fácil. Fazer software que compila e passa todos os testes em Linux, macOS e Windows ao mesmo tempo te ensina humildade.</p>
<h3>Módulo Platform<span class="hx:absolute hx:-mt-20" id="módulo-platform"></span>
    <a href="#m%c3%b3dulo-platform" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Todo código específico de OS vive em <code>src-tauri/src/platform/</code>:</p>
<ul>
<li><code>gpu.rs</code>: detecção de GPU (NVIDIA via nvidia-smi, AMD via sysfs/rocm-smi, Apple via system_profiler)</li>
<li><code>clipboard.rs</code>: cópia de imagem pro clipboard (xclip no Linux, pbcopy no macOS, PowerShell no Windows)</li>
<li><code>python.rs</code>: localização de Python (python3 vs python no PATH), caminhos de venv por OS</li>
<li><code>process.rs</code>: abstração de execução de subprocesso com tratamento de output</li>
</ul>
<p>Isso significa que <code>classify.rs</code>, <code>scan.rs</code>, <code>thumbnail.rs</code> — nenhum deles sabe em qual OS está rodando. Eles pedem pra plataforma e a plataforma resolve. Quando o Windows precisou de tratamento especial pra paths UNC (aqueles que começam com <code>\\?\</code>), a mudança ficou contida em <code>platform/</code>. Quando o macOS precisou de um import condicional, idem. O resto do codebase não foi tocado.</p>
<h3>GitHub Actions Matrix<span class="hx:absolute hx:-mt-20" id="github-actions-matrix"></span>
    <a href="#github-actions-matrix" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Dois workflows:</p>
<ul>
<li><strong>CI</strong> (push + PR): build e teste em Linux, macOS e Windows. Cada push roda <code>cargo test</code> + <code>npm test</code> nos 3 OS. Inclui <code>cargo fmt --check</code>, <code>cargo clippy -- -D warnings</code> e <code>cargo audit</code>. Se qualquer plataforma falhar, o PR não passa.</li>
<li><strong>Release</strong> (tags v*): build via <code>tauri-action</code>, gera AppImage (Linux), DMG (macOS arm64), MSI (Windows), e cria um Draft Release no GitHub com os binários anexados.</li>
</ul>
<p>Os 10+ commits de CI fix na segunda de manhã, foram a parte menos glamorosa do projeto. Coisas como:</p>
<ul>
<li><code>#[cfg(not(target_os = &quot;windows&quot;))]</code> em imports que usam <code>std::os::unix::fs::PermissionsExt</code></li>
<li><code>dunce::canonicalize</code> no lugar de <code>std::fs::canonicalize</code> porque o Windows gera paths com prefixo <code>\\?\</code> que quebram comparações de string</li>
<li>Instalar <code>rustfmt</code> e <code>clippy</code> explicitamente no runner porque nem sempre vêm no toolchain default da GitHub Actions</li>
<li>Remover o target macOS Intel do release workflow (Apple Silicon only — não vale o custo de manter dois targets Mac)</li>
</ul>
<p>Ninguém posta essas coisas no X. Mas sem elas, seu app não builda em 2 dos 3 targets.</p>
<h2>Integrações Externas<span class="hx:absolute hx:-mt-20" id="integrações-externas"></span>
    <a href="#integra%c3%a7%c3%b5es-externas" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O app depende de 3 sistemas externos. Cada um trouxe seus próprios problemas.</p>
<h3>Ollama<span class="hx:absolute hx:-mt-20" id="ollama"></span>
    <a href="#ollama" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Ollama serve os modelos de visão via API REST local (porta 11434). O app faz:</p>
<ul>
<li><strong>Verificação de status</strong>: checa se o Ollama está rodando e lista modelos instalados/carregados</li>
<li><strong>Download de modelo</strong>: se o modelo recomendado não está instalado, oferece download com barra de progresso via streaming da API</li>
<li><strong>Geração</strong>: manda imagem em base64 + prompt, recebe JSON (com a cascata de parsing de 3 níveis)</li>
<li><strong>Cleanup</strong>: descarrega modelos da VRAM quando não está classificando, pra não ficar monopolizando a GPU pras outras aplicações do usuário</li>
</ul>
<p>O Ollama é o único hard requirement. Sem ele, a classificação não funciona. O SetupModal guia o usuário pela instalação e download do modelo.</p>
<h3>Surya OCR<span class="hx:absolute hx:-mt-20" id="surya-ocr"></span>
    <a href="#surya-ocr" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Surya é um engine de OCR em Python que roda localmente. O problema: o app é Rust e não pode depender de uma instalação Python do sistema. A solução:</p>
<ul>
<li>O app mantém um Python venv isolado em <code>~/.local/share/frank_sherlock/surya_venv/</code></li>
<li>O script <code>surya_ocr.py</code> é bundled como resource do Tauri (empacotado no binário)</li>
<li>No primeiro uso, o <code>SetupModal</code> oferece provisionar o venv automaticamente (encontra Python, cria venv, pip install surya-ocr + dependências)</li>
<li>A classificação chama o script via subprocess, passa a imagem como argumento, lê o texto extraído do stdout</li>
</ul>
<p>O Surya é um <strong>soft requirement</strong>: se não estiver instalado, o app funciona normalmente — só não vai ter OCR dedicado. O pipeline degrada gracefully pra usar Ollama Vision como fallback, que é pior em cobertura mas funciona. O usuário vê um aviso no setup, não um erro que bloqueia o uso.</p>
<h3>PDFium<span class="hx:absolute hx:-mt-20" id="pdfium"></span>
    <a href="#pdfium" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pra PDFs, eu precisava de extração de texto nativo e renderização de páginas pra thumbnail. PDFium é o engine do Chrome pra PDFs, e tem bindings Rust via <code>pdfium-render</code>.</p>
<p>O binário do PDFium é baixado por um script (<code>scripts/download-pdfium.sh</code>) e bundled via <code>lib/</code> nos resources do Tauri. Cada plataforma recebe o binário correto (.so, .dylib, .dll). O <code>lib/</code> é gitignored — os binários são baixados no build, não versionados.</p>
<p>O pipeline de PDF:</p>
<ol>
<li>Tenta extrair texto nativo (sem OCR) — muitos PDFs já têm text layer</li>
<li>Se tiver texto suficiente, usa direto pra indexação (mais rápido e mais preciso que OCR)</li>
<li>Se não, renderiza a página e manda pro pipeline de imagem (Ollama Vision)</li>
<li>Detecta páginas em branco, encontra a primeira página com conteúdo real</li>
<li>Gera thumbnail como montagem das 2 primeiras páginas com conteúdo (dá uma noção melhor do documento do que só a capa)</li>
</ol>
<h2>O que Agile Vibe Coding Realmente É<span class="hx:absolute hx:-mt-20" id="o-que-agile-vibe-coding-realmente-é"></span>
    <a href="#o-que-agile-vibe-coding-realmente-%c3%a9" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>OK, agora o ponto que realmente importa. O motivo de eu estar escrevendo isso.</p>
<h3>O Que o Claude Fez<span class="hx:absolute hx:-mt-20" id="o-que-o-claude-fez"></span>
    <a href="#o-que-o-claude-fez" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Escreveu a maior parte do código Rust e TypeScript</li>
<li>Gerou 166 testes Rust e 172 testes frontend</li>
<li>Implementou o parsing de JSON com 3 níveis de fallback</li>
<li>Configurou CI/CD com matrix de 3 OS</li>
<li>Fez refactors massivos (monolito → 15 componentes + 10 hooks)</li>
<li>Lidou com edge cases de encoding, paths Unicode, e formatos de arquivo exóticos</li>
<li>Escreveu queries SQL, migrações, índices FTS5</li>
<li>Implementou detecção de GPU, clipboard por OS, resolução de Python</li>
<li>Criou o setup flow com barra de progresso e download de modelo</li>
<li>Debugou e fixou dezenas de issues de compilação cross-platform</li>
</ul>
<p>A velocidade é difícil de descrever sem parecer exagero. O commit das 19:04 de sábado, aquele de 4.186 linhas com 47 testes, levou cerca de uma hora incluindo minha revisão. Um dev humano, mesmo bom, levaria um dia inteiro pra escrever isso com a mesma cobertura de testes.</p>
<h3>O Que Eu Fiz<span class="hx:absolute hx:-mt-20" id="o-que-eu-fiz"></span>
    <a href="#o-que-eu-fiz" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Decidi fazer benchmarks antes de escrever qualquer código de app</li>
<li>Escolhi Tauri em vez de Electron (menor footprint, Rust nativo, sem Node runtime em prod)</li>
<li>Defini o princípio read-only como regra inviolável</li>
<li>Projetei o scan incremental com detecção de move por fingerprint</li>
<li>Decidi por cooperative cancellation com AtomicBool</li>
<li>Exigi migração formal de schema (nada de ALTER TABLE ad-hoc em scripts soltos)</li>
<li>Insisti em abstração de plataforma desde o primeiro commit multi-OS</li>
</ul>
<p>E principalmente: fiz as perguntas chatas. <em>&ldquo;E se o scan for cancelado no meio?&rdquo;</em> virou o sistema de checkpoint. <em>&ldquo;E se o banco corromper?&rdquo;</em> virou WAL + backup + health check. <em>&ldquo;E se a pessoa não tiver GPU boa?&rdquo;</em> virou model selection por tier. <em>&ldquo;E se o Surya não estiver instalado?&rdquo;</em> virou soft requirement com fallback. <em>&ldquo;E se o usuário deletar um root que está no meio de um scan?&rdquo;</em> virou cancel-before-delete. <em>&ldquo;E se o arquivo mudar de lugar mas o conteúdo for o mesmo?&rdquo;</em> virou detecção de move.</p>
<p>Ah, e decidi a hora de parar de adicionar features e publicar.</p>
<p>Essa última é subestimada. A tentação de ficar adicionando &ldquo;só mais uma coisa&rdquo; é enorme quando o custo marginal de implementar é baixo. O Claude implementa qualquer feature que eu pedir em minutos. Mas software que nunca é publicado não serve pra ninguém. Saber a hora de parar é uma skill que nenhum LLM vai te dar.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-23_16-37-22.jpg" alt="github releases"  loading="lazy" /></p>
<h3>O Padrão Real<span class="hx:absolute hx:-mt-20" id="o-padrão-real"></span>
    <a href="#o-padr%c3%a3o-real" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Agile Vibe coding não é <em>&ldquo;pedir pro LLM fazer um app&rdquo;</em>. É pair programming com um parceiro que é rápido pra caramba, tem memória perfeita, e nunca reclama de refactor. Você diz o que quer, ele implementa, você revisa e ajusta a direção. Mas se você não sabe pra onde ir, velocidade não ajuda — você só chega mais rápido no lugar errado.</p>
<p>As perguntas que eu fiz — sobre resiliência, cancelamento, multi-OS, degradação graceful, detecção de edge cases — nenhuma delas veio do LLM. Vieram de anos construindo software que precisa funcionar em ambientes reais, com usuários reais, em hardware diverso. O Claude não vai te perguntar <em>&ldquo;e se o usuário puxar o cabo de rede no meio do download do modelo?&rdquo;</em>. Mas se você perguntar, ele implementa o tratamento em minutos.</p>
<p>É tentador olhar pra esse projeto e concluir que qualquer pessoa com uma boa ideia poderia ter feito o mesmo. Mas tente imaginar: sem a decisão de fazer benchmarks, eu teria escolhido o modelo errado. Sem o PoC em Python, teria descoberto os problemas de JSON parsing em produção. Sem a abstração de plataforma, estaria debugando issues de Windows espalhadas por 15 arquivos. Sem o checkpoint de scan, usuários perderiam horas de processamento a cada crash. Sem a migração formal de schema, a primeira atualização quebraria o banco de todo mundo.</p>
<p>Pense num arquiteto com uma equipe de construção absurdamente eficiente. O arquiteto não precisa levantar cada parede, mas precisa saber onde elas vão e o que acontece se você tirar uma. A equipe executa rápido, trabalha de noite, não reclama. Mas alguém precisa ter desenhado a planta. Sem planta, é só um monte de tijolos empilhados rápido.</p>
<h2>Números Finais<span class="hx:absolute hx:-mt-20" id="números-finais"></span>
    <a href="#n%c3%bameros-finais" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Pra quem gosta de números concretos:</p>
<table>
  <thead>
      <tr>
          <th>Métrica</th>
          <th>Valor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Commits</td>
          <td>50</td>
      </tr>
      <tr>
          <td>Horas de trabalho efetivo</td>
          <td>~26h</td>
      </tr>
      <tr>
          <td>Linhas de Rust</td>
          <td>8.359</td>
      </tr>
      <tr>
          <td>Linhas de TypeScript</td>
          <td>5.842</td>
      </tr>
      <tr>
          <td>Linhas de CSS</td>
          <td>1.649</td>
      </tr>
      <tr>
          <td>Testes Rust</td>
          <td>166</td>
      </tr>
      <tr>
          <td>Testes frontend</td>
          <td>172</td>
      </tr>
      <tr>
          <td>Testes total</td>
          <td>338</td>
      </tr>
      <tr>
          <td>Plataformas</td>
          <td>3 (Linux, macOS, Windows)</td>
      </tr>
      <tr>
          <td>Migrações de banco</td>
          <td>5</td>
      </tr>
      <tr>
          <td>Módulos Rust</td>
          <td>13+</td>
      </tr>
      <tr>
          <td>Componentes React</td>
          <td>15+</td>
      </tr>
      <tr>
          <td>Hooks React</td>
          <td>10+</td>
      </tr>
  </tbody>
</table>
<p>O primeiro commit foi sexta-feira 21/02 às 19:29. O último foi segunda-feira 23/02 às 14:33. Descontando sono (~8h na noite de sábado/domingo, ~8h na madrugada de domingo/segunda) e pausas, são ~26 horas de trabalho distribuídas em um fim de semana.</p>
<p>De zero — sem um único arquivo no repositório — a binários publicados para 3 sistemas operacionais, com testes automatizados rodando no CI a cada push. Incluindo a fase de pesquisa, que sozinha justificaria um sprint.</p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Agile Vibe coding funciona. Mas funciona como qualquer ferramenta poderosa: nas mãos de quem sabe o que está fazendo.</p>
<p>A ideia do Frank Sherlock caberia num tweet: <em>&ldquo;classificar imagens com LLM local&rdquo;</em>. Mas transformar isso em software real exigiu: pesquisa benchmarked com ground truth formal, prova de conceito validando o pipeline completo, arquitetura incremental, tratamento de erros em 3 níveis, cancelamento cooperativo, migração formal de schema, abstração de plataforma, CI/CD com matrix de 3 OS, integração com 3 sistemas externos com graceful degradation, e 338 testes pra garantir que nada disso quebra quando alguém fizer cargo update.</p>
<p>O LLM acelerou tudo isso absurdamente. Mas não substituiu a necessidade de saber o que fazer. Se eu tivesse pedido <em>&ldquo;faz um app que classifica imagens&rdquo;</em> sem as 2 horas de benchmark, sem a prova de conceito, sem as decisões de arquitetura, sem as perguntas chatas sobre edge cases, o resultado seria um protótipo que funciona na minha máquina e quebra em qualquer outro lugar. E provavelmente eu nem perceberia até alguém reclamar.</p>
<p>O vibe precisa de um condutor experiente. Por enquanto, esse condutor ainda é humano.</p>
<p>Código em <a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener">github.com/akitaonrails/FrankSherlock</a>. GPL-3.0, local-only, open source.</p>
]]></content:encoded><category>frank</category><category>llms</category><category>inteligencia-artificial</category><category>vibe-coding</category></item><item><title>Vibe Code: Fiz um clone do Mega em Rails em 1 dia pro meu Home Server</title><link>https://www.akitaonrails.com/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/02/21/vibe-code-fiz-um-clone-do-mega-em-rails-em-1-dia-pro-meu-home-server/</guid><pubDate>Sat, 21 Feb 2026 18:48:25 +0000</pubDate><description>&lt;p&gt;Faz tempo que eu queria ter um serviço de file sharing privado no meu home server. Sabe quando precisa mandar um arquivo grande pra alguém? &amp;ldquo;Ah, usa o Google Drive&amp;rdquo;. Não. &amp;ldquo;WeTransfer?&amp;rdquo; Também não. Eu quero controle total. Quero saber onde meus arquivos estão, por quanto tempo existem, quem baixou, e não quero depender de serviço de terceiro pra algo tão simples.&lt;/p&gt;
&lt;p&gt;Então fiz o que qualquer programador faria: sentei, escrevi um documento de especificação, e com ajuda do Claude Code (Claude Opus 4.6), construí o &lt;a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener"&gt;&lt;strong&gt;FrankMega&lt;/strong&gt;&lt;/a&gt;, um clone simplificado do Mega, self-hosted, em Rails 8, em um único dia de trabalho. 21 commits, 3 horas de desenvolvimento ativo, 210 testes, zero dependência externa além do SQLite.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Faz tempo que eu queria ter um serviço de file sharing privado no meu home server. Sabe quando precisa mandar um arquivo grande pra alguém? &ldquo;Ah, usa o Google Drive&rdquo;. Não. &ldquo;WeTransfer?&rdquo; Também não. Eu quero controle total. Quero saber onde meus arquivos estão, por quanto tempo existem, quem baixou, e não quero depender de serviço de terceiro pra algo tão simples.</p>
<p>Então fiz o que qualquer programador faria: sentei, escrevi um documento de especificação, e com ajuda do Claude Code (Claude Opus 4.6), construí o <a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener"><strong>FrankMega</strong></a>, um clone simplificado do Mega, self-hosted, em Rails 8, em um único dia de trabalho. 21 commits, 3 horas de desenvolvimento ativo, 210 testes, zero dependência externa além do SQLite.</p>
<p><img src="https://github.com/akitaonrails/FrankMega/raw/master/docs/upload_screen.png" alt="frankmega"  loading="lazy" /></p>
<p>E antes que apareça o esperto nos comentários: &ldquo;ah, mas Vibe Coding é só prompt engineering, qualquer um faz&rdquo; &ndash; calma. Esse post é justamente pra mostrar que <strong>não é</strong>.</p>
<h2>Da Ideia ao Código: o IDEA.md<span class="hx:absolute hx:-mt-20" id="da-ideia-ao-código-o-ideamd"></span>
    <a href="#da-ideia-ao-c%c3%b3digo-o-ideamd" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Todo projeto começa com um documento. O meu foi o <a href="https://github.com/akitaonrails/FrankMega/blob/master/docs/IDEA.md"target="_blank" rel="noopener"><code>docs/IDEA.md</code></a> &ndash; 56 linhas descrevendo o que eu queria:</p>
<ul>
<li>Upload de arquivo único, com link compartilhável</li>
<li>Contador de downloads (padrão 5, configurável)</li>
<li>Expiração automática em até 24 horas</li>
<li>Autenticação completa: senha + 2FA + passkeys</li>
<li>Sem página de registro público &ndash; convites only</li>
<li>Admin cria convites, gerencia usuários, bane abusadores</li>
<li>Rate limiting agressivo, IP banning automático</li>
<li>Deploy via Docker + Cloudflare Tunnel no meu home server</li>
<li>Stack: Rails 8, SQLite, Tailwind CSS, Hotwire/Stimulus</li>
</ul>
<p>Eu deliberadamente incluí no final: <em>&ldquo;Sugira features importantes que possam ser relevantes pra um serviço assim.&rdquo;</em> Porque eu sei que um documento de spec nunca cobre tudo. E é aí que começa a parte interessante.</p>
<h2>O Big Bang: Commit #1<span class="hx:absolute hx:-mt-20" id="o-big-bang-commit-1"></span>
    <a href="#o-big-bang-commit-1" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O primeiro commit (<code>e277226</code>) às 12:17 entregou <strong>207 arquivos, 6.901 linhas de código</strong>. Num único commit. Sim, eu sei que é controverso, mas quando se está construindo do zero com AI, o primeiro commit é necessariamente grande.</p>
<p>O que veio nesse commit inicial:</p>
<ul>
<li>Autenticação completa com <code>has_secure_password</code> (bcrypt), TOTP via <code>rotp</code>, passkeys via <code>webauthn</code></li>
<li>Sistema de convites com códigos únicos e expiração</li>
<li>Admin panel completo (Users, Invitations, Files, MIME Types)</li>
<li>Upload com Active Storage + drag-and-drop via Stimulus</li>
<li>Links de download com hashes de 24 bytes (<code>SecureRandom.urlsafe_base64(24)</code>)</li>
<li>Rate limiting via Rack::Attack em todos os endpoints públicos</li>
<li>IP banning automático com modelo <code>Ban</code> e job assíncrono</li>
<li>Tema dark/light com Tailwind CSS inspirado no Mega.nz</li>
<li>73 testes (Minitest + FactoryBot)</li>
<li>Docker configuration com Puma + Thruster</li>
<li>Solid Queue/Cache/Cable &ndash; zero Redis</li>
</ul>
<p>Entenderam? <strong>Tudo isso no commit #1</strong>. O IDEA.md virou código funcional numa tacada. Mas o ponto é esse: <strong>o primeiro commit não era production-ready</strong>. Nem de longe.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-32-29.png" alt="tema light"  loading="lazy" /></p>
<h2>A Realidade: 20 Commits de Iteração<span class="hx:absolute hx:-mt-20" id="a-realidade-20-commits-de-iteração"></span>
    <a href="#a-realidade-20-commits-de-itera%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Depois do Big Bang vieram 20 commits ao longo de 3 horas. E é aqui que mora a verdade sobre desenvolvimento de software que nenhum tutorial de &ldquo;Vibe Coding&rdquo; vai te contar.</p>
<h3>Fase 1: &ldquo;Funciona na minha máquina&rdquo; (12:40 - 12:46)<span class="hx:absolute hx:-mt-20" id="fase-1-funciona-na-minha-máquina-1240---1246"></span>
    <a href="#fase-1-funciona-na-minha-m%c3%a1quina-1240---1246" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quatro commits em 6 minutos. Sabe o que aconteceu? Fui buildar o Docker e <strong>quebrou</strong>. Faltava <code>libssl-dev</code>. Consertei. Daí o CSP (Content Security Policy) do <code>secure_headers</code> conflitava com os inline scripts do importmap do Rails. Consertei. Mudei a porta de 3000 pra 3100 pra não conflitar com outros serviços no meu server.</p>
<p>Olha o commit <code>389ebe8</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># CSP agora é feito pelo Rails built-in, não pelo secure_headers</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Porque secure_headers não suporta nonces pra importmap inline scripts</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">csp</span> <span class="o">=</span> <span class="no">SecureHeaders</span><span class="o">::</span><span class="no">OPT_OUT</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esse tipo de incompatibilidade entre gems você só descobre na hora. Nenhum LLM vai te avisar disso de antemão porque é uma combinação específica de versões.</p>
<h3>Fase 2: O Commit de Segurança &ndash; 22 Falhas Corrigidas (13:10)<span class="hx:absolute hx:-mt-20" id="fase-2-o-commit-de-segurança--22-falhas-corrigidas-1310"></span>
    <a href="#fase-2-o-commit-de-seguran%c3%a7a--22-falhas-corrigidas-1310" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esse é o commit mais importante do projeto inteiro: <code>4a854a6</code>. 35 arquivos, 612 linhas, 22 issues de segurança corrigidas. Vou detalhar porque é aqui que se separa código de brinquedo de código de produção.</p>
<p>O que o commit inicial fazia de errado:</p>
<p><strong>CRÍTICO &ndash; Chaves de criptografia com fallback hardcoded:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES (perigoso):</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">active_record</span><span class="o">.</span><span class="n">encryption</span><span class="o">.</span><span class="n">primary_key</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;KEY&#34;</span><span class="p">,</span> <span class="s2">&#34;test-primary-key&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># DEPOIS (fail-fast):</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="no">Rails</span><span class="o">.</span><span class="n">env</span><span class="o">.</span><span class="n">production?</span>
</span></span><span class="line"><span class="cl">  <span class="n">config</span><span class="o">.</span><span class="n">active_record</span><span class="o">.</span><span class="n">encryption</span><span class="o">.</span><span class="n">primary_key</span> <span class="o">=</span> <span class="no">ENV</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Se o ENV var não existisse em produção, a app rodava feliz com a chave hardcoded. Qualquer pessoa que lesse o código-fonte conseguiria decriptar os segredos OTP de todos os usuários. Agora em produção, se faltar a variável, a app <strong>crasheia no boot</strong>. É o comportamento correto.</p>
<p><strong>CRÍTICO &ndash; Replay Attack no OTP:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">verify_otp</span><span class="p">(</span><span class="n">code</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">totp</span> <span class="o">=</span> <span class="no">ROTP</span><span class="o">::</span><span class="no">TOTP</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">otp_secret</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">totp</span><span class="o">.</span><span class="n">verify</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="ss">drift_behind</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span> <span class="ss">drift_ahead</span><span class="p">:</span> <span class="mi">30</span><span class="p">)</span><span class="o">.</span><span class="n">present?</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># DEPOIS:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">verify_otp</span><span class="p">(</span><span class="n">code</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">totp</span> <span class="o">=</span> <span class="no">ROTP</span><span class="o">::</span><span class="no">TOTP</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">otp_secret</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">timestamp</span> <span class="o">=</span> <span class="n">totp</span><span class="o">.</span><span class="n">verify</span><span class="p">(</span><span class="n">code</span><span class="p">,</span> <span class="ss">drift_behind</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span> <span class="ss">drift_ahead</span><span class="p">:</span> <span class="mi">30</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kp">false</span> <span class="k">unless</span> <span class="n">timestamp</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Previne replay: rejeita se o timestamp desse código já foi usado</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">last_otp_at</span><span class="o">.</span><span class="n">present?</span> <span class="o">&amp;&amp;</span> <span class="no">Time</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="n">timestamp</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="n">last_otp_at</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="kp">false</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">update_column</span><span class="p">(</span><span class="ss">:last_otp_at</span><span class="p">,</span> <span class="no">Time</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="n">timestamp</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="kp">true</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Todo tutorial de TOTP que você encontra na internet mostra o <code>verify(code).present?</code> e para aí. O problema: um código TOTP é válido por 30 segundos. Se eu interceptar seu código (shoulder surfing, câmera, clipboard), posso usá-lo múltiplas vezes nessa janela. O <code>last_otp_at</code> garante one-time use real.</p>
<p><strong>ALTO &ndash; Race Condition no Download Counter:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES (race condition):</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">increment_download!</span>
</span></span><span class="line"><span class="cl">  <span class="n">increment!</span><span class="p">(</span><span class="ss">:download_count</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># DEPOIS (atômico):</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">increment_download!</span>
</span></span><span class="line"><span class="cl">  <span class="nb">self</span><span class="o">.</span><span class="n">class</span><span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="nb">id</span><span class="p">:</span> <span class="nb">id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">&#34;download_count &lt; max_downloads&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">where</span><span class="p">(</span><span class="s2">&#34;expires_at &gt; ?&#34;</span><span class="p">,</span> <span class="no">Time</span><span class="o">.</span><span class="n">current</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="o">.</span><span class="n">update_all</span><span class="p">(</span><span class="s2">&#34;download_count = download_count + 1&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>increment!</code> faz: (1) lê o valor, (2) soma 1 em Ruby, (3) escreve de volta. Duas requisições simultâneas podem ler <code>4</code>, ambas escreverem <code>5</code>, e o usuário ganha dois downloads quando só tinha um. O <code>UPDATE ... WHERE</code> atômico no SQL garante que só uma requisição vence. E o <code>== 1</code> retorna sucesso/falha numa única operação, sem lock.</p>
<p>Isso é o clássico bug TOCTOU (Time of Check, Time of Use). Você nunca vai ver isso no desenvolvimento local com um browser. Só aparece em produção com requisições concorrentes.</p>
<p><strong>ALTO &ndash; Open Redirect após login:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># DEPOIS:</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">safe_redirect_url?</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">uri</span><span class="o">.</span><span class="n">host</span><span class="o">.</span><span class="n">nil?</span> <span class="o">||</span> <span class="n">uri</span><span class="o">.</span><span class="n">host</span> <span class="o">==</span> <span class="n">request</span><span class="o">.</span><span class="n">host</span>
</span></span><span class="line"><span class="cl"><span class="k">rescue</span> <span class="no">URI</span><span class="o">::</span><span class="no">InvalidURIError</span>
</span></span><span class="line"><span class="cl">  <span class="kp">false</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Sem isso, um atacante podia enviar um link tipo <code>frankmega.com/session?return_to=https://evil.com</code> e após login o usuário seria redirecionado pro site malicioso.</p>
<p><strong>MÉDIO &ndash; Nonce do CSP previsível:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># ANTES:</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">content_security_policy_nonce_generator</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> <span class="p">{</span> <span class="n">request</span><span class="o">.</span><span class="n">session</span><span class="o">.</span><span class="n">id</span><span class="o">.</span><span class="n">to_s</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># DEPOIS:</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">content_security_policy_nonce_generator</span> <span class="o">=</span> <span class="o">-&gt;</span><span class="p">(</span><span class="n">_request</span><span class="p">)</span> <span class="p">{</span> <span class="no">SecureRandom</span><span class="o">.</span><span class="n">base64</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span> <span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Session ID não muda entre requests. Se o nonce é previsível, um atacante pode injetar scripts com o nonce correto. Tem que ser aleatório por request.</p>
<p>E mais: IPv6 do Cloudflare adicionado aos trusted proxies, mínimo de 12 caracteres pra senha, MIME detection server-side via Marcel, verificação de file size pelo tempfile (não pelo header do client que pode ser forjado), proteção contra deletar/banir/rebaixar o último admin.</p>
<p>O total de testes pulou de 73 pra 109 com esse único commit.</p>
<h2>Como Rails Facilita Segurança<span class="hx:absolute hx:-mt-20" id="como-rails-facilita-segurança"></span>
    <a href="#como-rails-facilita-seguran%c3%a7a" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Rails torna MUITO mais fácil implementar segurança do que fazer do zero. Olha a comparação:</p>
<table>
  <thead>
      <tr>
          <th>Feature</th>
          <th>Rails</th>
          <th>Do zero</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CSRF</td>
          <td>Automático no <code>ActionController::Base</code></td>
          <td>Implementar tokens por sessão em todos os forms</td>
      </tr>
      <tr>
          <td>Hash de senha</td>
          <td><code>has_secure_password</code> (1 linha)</td>
          <td>Escolher algoritmo, salt, implementar verificação</td>
      </tr>
      <tr>
          <td>Parâmetros filtrados</td>
          <td><code>config.filter_parameters</code></td>
          <td>Hook no sistema de logging</td>
      </tr>
      <tr>
          <td>Strong params</td>
          <td><code>params.require().permit()</code></td>
          <td>Middleware de whitelist manual</td>
      </tr>
      <tr>
          <td>Criptografia de campos</td>
          <td><code>encrypts :otp_secret</code> (1 linha)</td>
          <td>Envelope encryption, key rotation, decrypt transparente</td>
      </tr>
      <tr>
          <td>CSP com nonces</td>
          <td>Config de 1 bloco</td>
          <td>Gerar nonces, injetar nos tags HTML, setar headers</td>
      </tr>
      <tr>
          <td>Rate limiting (Rails 8)</td>
          <td><code>rate_limit to: 10, within: 3.minutes</code></td>
          <td>Storage de contadores, lógica de sliding window</td>
      </tr>
      <tr>
          <td>SQL injection</td>
          <td>Parameterized queries by default</td>
          <td>Parametrizar manualmente cada query</td>
      </tr>
  </tbody>
</table>
<p>O Rails 8 em particular trouxe o <code>rate_limit</code> built-in no controller:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">SessionsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
</span></span><span class="line"><span class="cl">  <span class="n">rate_limit</span> <span class="ss">to</span><span class="p">:</span> <span class="mi">10</span><span class="p">,</span> <span class="ss">within</span><span class="p">:</span> <span class="mi">3</span><span class="o">.</span><span class="n">minutes</span><span class="p">,</span> <span class="ss">only</span><span class="p">:</span> <span class="ss">:create</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">with</span><span class="p">:</span> <span class="o">-&gt;</span> <span class="p">{</span> <span class="n">redirect_to</span> <span class="n">new_session_path</span><span class="p">,</span> <span class="ss">alert</span><span class="p">:</span> <span class="n">t</span><span class="p">(</span><span class="s2">&#34;flash.sessions.create.rate_limit&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E a autenticação? Rails 8 agora vem com um <strong>authentication generator</strong> built-in. Não precisa mais do Devise pra coisas básicas. O <code>app/controllers/concerns/authentication.rb</code> do FrankMega é baseado nisso e estende com 2FA, passkeys e invitation-only registration.</p>
<h2>File Sharing Não É Só Download<span class="hx:absolute hx:-mt-20" id="file-sharing-não-é-só-download"></span>
    <a href="#file-sharing-n%c3%a3o-%c3%a9-s%c3%b3-download" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uma coisa que muita gente não percebe: um serviço de file sharing tem uma superfície de ataque enorme. Não é só &ldquo;salvar arquivo, gerar link&rdquo;. Olha tudo que precisa ser considerado:</p>
<h3>Sanitização de Filename<span class="hx:absolute hx:-mt-20" id="sanitização-de-filename"></span>
    <a href="#sanitiza%c3%a7%c3%a3o-de-filename" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">sanitize_filename</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">content_type</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">basename</span><span class="p">(</span><span class="nb">name</span><span class="o">.</span><span class="n">to_s</span><span class="p">)</span>                          <span class="c1"># Strip path traversal</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">&#34;UTF-8&#34;</span><span class="p">,</span> <span class="ss">invalid</span><span class="p">:</span> <span class="ss">:replace</span><span class="p">,</span>      <span class="c1"># Handle invalid UTF-8</span>
</span></span><span class="line"><span class="cl">                                <span class="k">undef</span><span class="p">:</span> <span class="ss">:replace</span><span class="p">,</span> <span class="ss">replace</span><span class="p">:</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">gsub</span><span class="p">(</span><span class="sr">/[\x00-\x1f\x7f\/\\:*?&#34;&lt;&gt;|]/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span> <span class="c1"># Control chars + unsafe chars</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">/\A\.+/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>                        <span class="c1"># Leading dots (hidden files)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">gsub</span><span class="p">(</span><span class="sr">/\s+/</span><span class="p">,</span> <span class="s2">&#34; &#34;</span><span class="p">)</span><span class="o">.</span><span class="n">strip</span>                  <span class="c1"># Collapse whitespace</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># Windows reserved device names</span>
</span></span><span class="line"><span class="cl">  <span class="n">base_without_ext</span> <span class="o">=</span> <span class="n">sanitized</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">/\.[^.]*\z/</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">base_without_ext</span><span class="o">.</span><span class="n">match?</span><span class="p">(</span><span class="sr">/\A(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])\z/i</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">sanitized</span> <span class="o">=</span> <span class="s2">&#34;_</span><span class="si">#{</span><span class="n">sanitized</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">strip_extension_junk</span><span class="p">(</span><span class="n">sanitized</span><span class="p">,</span> <span class="n">content_type</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span> <span class="o">=</span> <span class="n">truncate_filename</span><span class="p">(</span><span class="n">sanitized</span><span class="p">,</span> <span class="mi">255</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">sanitized</span><span class="o">.</span><span class="n">presence</span> <span class="o">||</span> <span class="s2">&#34;unnamed_file&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Cada linha existe por um motivo:</p>
<ul>
<li><code>File.basename</code> impede path traversal (<code>../../etc/passwd</code> vira <code>passwd</code>)</li>
<li>Strip de control chars impede null byte injection e terminal escape injection</li>
<li>Nomes reservados do Windows (CON, NUL, AUX, LPT1-9) causam problemas se o storage for acessado de um Windows</li>
<li>Truncamento pra 255 bytes (não caracteres!) preservando a extensão &ndash; um emoji de 4 bytes conta como 4 pro limite do ext4/NTFS</li>
<li><code>strip_extension_junk</code> trata nomes que vêm de URLs com parâmetros grudados: <code>photo.jpg_1280x720+quality=80</code> vira <code>photo.jpg</code></li>
</ul>
<p>E isso é server-side. Tem validação client-side também no Stimulus controller pra rejeitar antes do upload:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">isInvalidFilename</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="k">new</span> <span class="nx">Blob</span><span class="p">([</span><span class="nx">name</span><span class="p">]).</span><span class="nx">size</span> <span class="o">&gt;</span> <span class="mi">255</span><span class="p">)</span> <span class="k">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="p">(</span><span class="sr">/[\x00-\x1f\x7f/:*?&#34;&lt;&gt;|\\]/</span><span class="p">.</span><span class="nx">test</span><span class="p">(</span><span class="nx">name</span><span class="p">))</span> <span class="k">return</span> <span class="kc">true</span>
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="kc">false</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>MIME Type Detection Server-Side<span class="hx:absolute hx:-mt-20" id="mime-type-detection-server-side"></span>
    <a href="#mime-type-detection-server-side" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="vi">@shared_file</span><span class="o">.</span><span class="n">content_type</span> <span class="o">=</span> <span class="no">Marcel</span><span class="o">::</span><span class="no">MimeType</span><span class="o">.</span><span class="n">for</span><span class="p">(</span><span class="n">uploaded</span><span class="o">.</span><span class="n">tempfile</span><span class="p">,</span> <span class="nb">name</span><span class="p">:</span> <span class="n">uploaded</span><span class="o">.</span><span class="n">original_filename</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="vi">@shared_file</span><span class="o">.</span><span class="n">file_size</span> <span class="o">=</span> <span class="n">uploaded</span><span class="o">.</span><span class="n">tempfile</span><span class="o">.</span><span class="n">size</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Não confia no <code>Content-Type</code> que o browser envia. O Marcel inspeciona os magic bytes do arquivo pra determinar o tipo real. E o file size vem do tempfile no disco, não do header <code>Content-Length</code> que pode ser forjado.</p>
<h3>Quotas e Disk Usage<span class="hx:absolute hx:-mt-20" id="quotas-e-disk-usage"></span>
    <a href="#quotas-e-disk-usage" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_16-00-52.png" alt="quota"  loading="lazy" /></p>
<p>Cada usuário tem uma quota de disco (5 GB default, admin pode customizar):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">can_upload?</span><span class="p">(</span><span class="n">file_size</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">grace</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">x</span><span class="o">.</span><span class="n">security</span><span class="o">.</span><span class="n">disk_quota_grace_bytes</span>
</span></span><span class="line"><span class="cl">  <span class="p">(</span><span class="n">storage_used</span> <span class="o">+</span> <span class="n">file_size</span><span class="p">)</span> <span class="o">&lt;=</span> <span class="p">(</span><span class="n">disk_quota</span> <span class="o">+</span> <span class="n">grace</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Sem quota, um único usuário malicioso enche o disco do servidor. Com o grace buffer de 10%, um upload que passa um pouco do limite é aceito pra não frustrar o usuário no edge case.</p>
<h3>Cleanup Automático<span class="hx:absolute hx:-mt-20" id="cleanup-automático"></span>
    <a href="#cleanup-autom%c3%a1tico" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">CleanupExpiredFilesJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="n">queue_as</span> <span class="ss">:default</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span>
</span></span><span class="line"><span class="cl">    <span class="no">SharedFile</span><span class="o">.</span><span class="n">inactive</span><span class="o">.</span><span class="n">find_each</span> <span class="k">do</span> <span class="o">|</span><span class="n">shared_file</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">shared_file</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">purge</span> <span class="k">if</span> <span class="n">shared_file</span><span class="o">.</span><span class="n">file</span><span class="o">.</span><span class="n">attached?</span>
</span></span><span class="line"><span class="cl">      <span class="n">shared_file</span><span class="o">.</span><span class="n">destroy</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Roda a cada 15 minutos via Solid Queue. Se ninguém baixou em 24 horas, sumiu. Se bateu o limite de downloads, sumiu. Sem isso, disco do servidor enche e o serviço morre.</p>
<h3>IP Banning Automático<span class="hx:absolute hx:-mt-20" id="ip-banning-automático"></span>
    <a href="#ip-banning-autom%c3%a1tico" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">InvalidHashAccessJob</span> <span class="o">&lt;</span> <span class="no">ApplicationJob</span>
</span></span><span class="line"><span class="cl">  <span class="n">queue_as</span> <span class="ss">:default</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">ip_address</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">security</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">x</span><span class="o">.</span><span class="n">security</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">unless</span> <span class="n">security</span><span class="o">.</span><span class="n">enable_banning</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">cache_key</span> <span class="o">=</span> <span class="s2">&#34;invalid_hash:</span><span class="si">#{</span><span class="n">ip_address</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="n">count</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">increment</span><span class="p">(</span><span class="n">cache_key</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="ss">expires_in</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">hour</span><span class="p">)</span> <span class="o">||</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">count</span> <span class="o">&gt;=</span> <span class="n">security</span><span class="o">.</span><span class="n">max_invalid_hash_attempts</span>
</span></span><span class="line"><span class="cl">      <span class="no">Ban</span><span class="o">.</span><span class="n">ban!</span><span class="p">(</span><span class="n">ip_address</span><span class="p">,</span> <span class="ss">reason</span><span class="p">:</span> <span class="s2">&#34;Repeated invalid download hash access&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">               <span class="ss">duration</span><span class="p">:</span> <span class="n">security</span><span class="o">.</span><span class="n">ban_duration</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rails</span><span class="o">.</span><span class="n">cache</span><span class="o">.</span><span class="n">delete</span><span class="p">(</span><span class="n">cache_key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Tentou 3 hashes inválidos? Banido por 1 hora. Isso impede enumeração de hashes. Com 24 bytes de entropia (192 bits, 2^192 possibilidades), brute force é computacionalmente inviável, mas a proteção extra não custa nada.</p>
<h3>Rate Limiting em Camadas<span class="hx:absolute hx:-mt-20" id="rate-limiting-em-camadas"></span>
    <a href="#rate-limiting-em-camadas" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O Rack::Attack opera na camada de middleware (antes do Rails processar):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># Login: 5 tentativas por minuto por IP</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;logins/ip&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">minute</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span><span class="o">.</span><span class="n">ip</span> <span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span> <span class="o">==</span> <span class="s2">&#34;/session&#34;</span> <span class="o">&amp;&amp;</span> <span class="n">req</span><span class="o">.</span><span class="n">post?</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Login: 5 tentativas por minuto por email</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;logins/email&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">minute</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span> <span class="o">==</span> <span class="s2">&#34;/session&#34;</span> <span class="o">&amp;&amp;</span> <span class="n">req</span><span class="o">.</span><span class="n">post?</span>
</span></span><span class="line"><span class="cl">    <span class="n">req</span><span class="o">.</span><span class="n">params</span><span class="o">.</span><span class="n">dig</span><span class="p">(</span><span class="s2">&#34;email_address&#34;</span><span class="p">)</span><span class="o">&amp;.</span><span class="n">to_s</span><span class="o">&amp;.</span><span class="n">downcase</span><span class="o">&amp;.</span><span class="n">strip</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Downloads: 60 views e 30 downloads por minuto</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;downloads_get/ip&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">60</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">1</span><span class="o">.</span><span class="n">minute</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span><span class="o">.</span><span class="n">ip</span> <span class="k">if</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">start_with?</span><span class="p">(</span><span class="s2">&#34;/d/&#34;</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">req</span><span class="o">.</span><span class="n">get?</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Geral: 300 requests por 5 minutos (exceto assets)</span>
</span></span><span class="line"><span class="cl"><span class="no">Rack</span><span class="o">::</span><span class="no">Attack</span><span class="o">.</span><span class="n">throttle</span><span class="p">(</span><span class="s2">&#34;requests/ip&#34;</span><span class="p">,</span> <span class="ss">limit</span><span class="p">:</span> <span class="mi">300</span><span class="p">,</span> <span class="ss">period</span><span class="p">:</span> <span class="mi">5</span><span class="o">.</span><span class="n">minutes</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">req</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="n">req</span><span class="o">.</span><span class="n">ip</span> <span class="k">unless</span> <span class="n">req</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">start_with?</span><span class="p">(</span><span class="s2">&#34;/assets&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Sete throttles diferentes pra cenários específicos. Mais o <code>rate_limit</code> do Rails 8 nos controllers. IPs banidos nem chegam ao Rails &ndash; são rejeitados na blocklist do middleware com status 403.</p>
<p>E os limites são configuráveis: em produção é 1x, em desenvolvimento é 10x pra não se trancar durante testes. Tudo centralizado no <code>config/initializers/security.rb</code>.</p>
<h2>A Saga do Download: 5 Commits em 21 Minutos<span class="hx:absolute hx:-mt-20" id="a-saga-do-download-5-commits-em-21-minutos"></span>
    <a href="#a-saga-do-download-5-commits-em-21-minutos" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-31-25.png" alt="download"  loading="lazy" /></p>
<p>Essa parte é divertida porque mostra debugging real. Entre 14:44 e 15:05, eu fiz <strong>5 commits</strong> tentando fazer o download funcionar com Turbo Drive:</p>
<ol>
<li><code>cc3c23d</code> &ndash; Adicionei <code>data-turbo=false</code> no botão de download. <strong>Não funcionou.</strong></li>
<li><code>5224ed2</code> &ndash; Movi <code>data-turbo=false</code> pro <code>&lt;form&gt;</code>. <strong>Não funcionou.</strong></li>
<li><code>98b0d4f</code> &ndash; Abandonei o <code>redirect_to rails_blob_path</code> e usei <code>send_file</code> direto. <strong>Parcialmente funcionou.</strong></li>
<li><code>1fdf5f9</code> &ndash; Troquei de POST pra GET link. Funcionou mas&hellip; bots podem consumir downloads com GET.</li>
<li><code>4c74b27</code> &ndash; Voltei pra POST. Adicionei cleanup de URL artifacts nos filenames.</li>
</ol>
<p>O root cause: o Turbo Drive intercepta navegação agressivamente, e download de arquivo via <code>redirect_to</code> (que gera uma cadeia de 302 redirects do Active Storage) confunde o Turbo. A solução final: <code>send_file</code> direto do disco com <code>disposition: &quot;attachment&quot;</code> via POST com <code>data: { turbo: false }</code> no form.</p>
<p>Esse tipo de problema um LLM não resolve sozinho. Precisa testar no browser real, ver o que acontece, tentar, falhar, ajustar. É desenvolvimento iterativo puro.</p>
<h2>O Gap entre a Ideia e o Produto Final<span class="hx:absolute hx:-mt-20" id="o-gap-entre-a-ideia-e-o-produto-final"></span>
    <a href="#o-gap-entre-a-ideia-e-o-produto-final" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Olha o que o IDEA.md não mencionava e que emergiu durante o desenvolvimento:</p>
<ol>
<li>Per-user disk quotas &ndash; sem isso um usuário enche o disco do server</li>
<li>I18n completo (EN + PT-BR) &ndash; 66 arquivos, ~250 strings extraídas</li>
<li>Terms of Service com aceite obrigatório</li>
<li>User self-deletion &ndash; requisito de privacidade</li>
<li>Blocked downloads de usuários banidos &ndash; seus links retornam 410 Gone</li>
<li>Client-side upload validation &ndash; verifica size, quota e filename antes do upload</li>
<li>Inline file previews &ndash; imagens, vídeo e áudio no download page sem consumir downloads</li>
<li>Styled error pages &ndash; em vez de plain text &ldquo;Not Found&rdquo;, páginas branded com i18n</li>
</ol>
<p>O IDEA.md dizia &ldquo;use activeadmin ou administrate&rdquo;. Na prática construí um admin panel custom com Tailwind porque o <code>administrate</code> tinha problemas de compatibilidade com Rails 8.1. O IDEA.md falava em &ldquo;progress bar se possível&rdquo;. Na prática usei drag-and-drop com preview de arquivo via Stimulus, que é melhor que progress bar.</p>
<p>Ninguém consegue prever todas as features no dia 1. Você descobre requisitos conforme constrói. E cada feature nova traz edge cases que precisam ser tratados.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-32-50.png" alt="pt"  loading="lazy" /></p>
<h2>Os Números<span class="hx:absolute hx:-mt-20" id="os-números"></span>
    <a href="#os-n%c3%bameros" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Ao final do projeto:</p>
<ul>
<li>21 commits em uma sessão de ~3 horas</li>
<li>210 testes (Minitest + FactoryBot), 513 assertions, zero failures</li>
<li>~3.100 linhas de código de aplicação (models, controllers, views, JS, CSS, configs)</li>
<li>~1.965 linhas de testes (incluindo testes de segurança dedicados)</li>
<li>9 models, 20 controllers, 36 views, 9 Stimulus controllers</li>
<li>24 medidas de segurança distintas em 7 camadas</li>
<li>CI: SimpleCov, RuboCop (zero offenses), Brakeman (zero warnings), bundler-audit (zero vulnerabilities)</li>
<li>Zero dependências externas além do SQLite. Sem Redis, sem PostgreSQL, sem serviço de fila externo</li>
</ul>
<h2>O Invitation-Only Server<span class="hx:absolute hx:-mt-20" id="o-invitation-only-server"></span>
    <a href="#o-invitation-only-server" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-31-54.png" alt="invite"  loading="lazy" /></p>
<p>O FrankMega não tem cadastro público. O fluxo é:</p>
<ol>
<li>Na primeira execução, sem nenhum usuário, aparece a tela de setup pra criar o admin</li>
<li>A rota <code>/setup</code> só existe enquanto <code>User.count.zero?</code> &ndash; depois some completamente:</li>
</ol>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">constraints</span><span class="p">(</span><span class="o">-&gt;</span><span class="p">(</span><span class="n">request</span><span class="p">)</span> <span class="p">{</span> <span class="no">User</span><span class="o">.</span><span class="n">count</span><span class="o">.</span><span class="n">zero?</span> <span class="p">})</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">get</span> <span class="s2">&#34;setup&#34;</span><span class="p">,</span> <span class="ss">to</span><span class="p">:</span> <span class="s2">&#34;setup#new&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">post</span> <span class="s2">&#34;setup&#34;</span><span class="p">,</span> <span class="ss">to</span><span class="p">:</span> <span class="s2">&#34;setup#create&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<ol start="3">
<li>O admin cria convites no painel admin (com expiração)</li>
<li>Cada convite gera um código único de 16 bytes</li>
<li>Cada código só pode ser usado uma vez (<code>with_lock</code> + transaction pra impedir race condition)</li>
<li>Após cadastrar, o convite é marcado como &ldquo;used&rdquo;</li>
</ol>
<p>Isso é perfeito pra um serviço pessoal/familiar. Controle total de quem acessa.</p>
<h2>Deploy: Docker + Cloudflare Tunnel<span class="hx:absolute hx:-mt-20" id="deploy-docker--cloudflare-tunnel"></span>
    <a href="#deploy-docker--cloudflare-tunnel" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-21_15-33-40.png" alt="share"  loading="lazy" /></p>
<p>Pra fechar, o tutorial de deploy, caso queira rodar no seu próprio Home Server Docker. Não é complicado mas tem detalhes que importam.</p>
<h3>O Dockerfile<span class="hx:absolute hx:-mt-20" id="o-dockerfile"></span>
    <a href="#o-dockerfile" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Multi-stage build em 3 estágios:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="c"># Stage 1: Base (runtime)</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">ruby:3.4.8-slim</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">base</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get install -y curl libjemalloc2 libvips sqlite3<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># jemalloc pra melhor gerenciamento de memória</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Stage 2: Build (throwaway)</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get install -y build-essential git libssl-dev<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Bundle install + asset precompilation com dummy env vars</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Stage 3: Final</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Copia gems e app, roda como user não-root (rails:rails, UID 1000)</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">CMD</span> <span class="p">[</span><span class="s2">&#34;./bin/thrust&#34;</span><span class="p">,</span> <span class="s2">&#34;./bin/rails&#34;</span><span class="p">,</span> <span class="s2">&#34;server&#34;</span><span class="p">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O <code>bin/thrust</code> é o Thruster (do Basecamp) &ndash; um proxy HTTP na frente do Puma que faz: gzip/brotli compression, asset caching, X-Sendfile acceleration. Escuta na porta 80, proxeia pro Puma na 3000.</p>
<p>O entrypoint roda <code>db:prepare</code> e <code>db:seed</code> automaticamente no boot:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/bash -e
</span></span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="si">${</span><span class="p">@: -2:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;./bin/rails&#34;</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="si">${</span><span class="p">@: -1:</span><span class="nv">1</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;server&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  ./bin/rails db:prepare
</span></span><span class="line"><span class="cl">  ./bin/rails db:seed
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="nb">exec</span> <span class="s2">&#34;</span><span class="si">${</span><span class="p">@</span><span class="si">}</span><span class="s2">&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>docker-compose.yml<span class="hx:absolute hx:-mt-20" id="docker-composeyml"></span>
    <a href="#docker-composeyml" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Dois serviços:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">web</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">akitaonrails/frankmega:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;3100:80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/seuuser/frankmega/uploads:/rails/storage/uploads</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">/home/seuuser/frankmega/db:/rails/storage</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">SECRET_KEY_BASE</span><span class="p">:</span><span class="w"> </span><span class="l">${SECRET_KEY_BASE}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">HOST</span><span class="p">:</span><span class="w"> </span><span class="l">${HOST}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="c"># ... mais ~15 variáveis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">tunnel</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">cloudflare/cloudflared:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">tunnel run</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">environment</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">TUNNEL_TOKEN=${TUNNEL_TOKEN}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">depends_on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">web</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O sidecar <code>cloudflare/cloudflared</code> cria um túnel outbound pra edge do Cloudflare. Não precisa abrir portas no firewall do home server. O Cloudflare faz SSL termination, DDoS protection, e roteia o tráfego pro container.</p>
<h3>Passo a Passo<span class="hx:absolute hx:-mt-20" id="passo-a-passo"></span>
    <a href="#passo-a-passo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>1. Crie o Tunnel no Cloudflare:</strong></p>
<ul>
<li>Cloudflare Zero Trust Dashboard &gt; Networks &gt; Tunnels &gt; Create</li>
<li>Tipo: Cloudflared</li>
<li>Copie o <code>TUNNEL_TOKEN</code></li>
<li>Configure o hostname: <code>frankmega.seudominio.com</code> apontando pra <code>http://web:80</code></li>
</ul>
<p><strong>2. Gere os segredos:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># SECRET_KEY_BASE</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">64</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Chaves de criptografia (3 valores separados)</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">32</span>  <span class="c1"># PRIMARY_KEY</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">32</span>  <span class="c1"># DETERMINISTIC_KEY</span>
</span></span><span class="line"><span class="cl">openssl rand -hex <span class="m">32</span>  <span class="c1"># KEY_DERIVATION_SALT</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>3. Configure o <code>.env</code>:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-env" data-lang="env"><span class="line"><span class="cl"><span class="nv">SECRET_KEY_BASE</span><span class="o">=</span>&lt;gerado acima&gt;
</span></span><span class="line"><span class="cl"><span class="nv">RAILS_MASTER_KEY</span><span class="o">=</span>&lt;<span class="k">do</span> config/master.key&gt;
</span></span><span class="line"><span class="cl"><span class="nv">HOST</span><span class="o">=</span>frankmega.seudominio.com
</span></span><span class="line"><span class="cl"><span class="nv">WEBAUTHN_ORIGIN</span><span class="o">=</span>https://frankmega.seudominio.com
</span></span><span class="line"><span class="cl"><span class="nv">WEBAUTHN_RP_ID</span><span class="o">=</span>frankmega.seudominio.com
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY</span><span class="o">=</span>&lt;gerado&gt;
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY</span><span class="o">=</span>&lt;gerado&gt;
</span></span><span class="line"><span class="cl"><span class="nv">ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT</span><span class="o">=</span>&lt;gerado&gt;
</span></span><span class="line"><span class="cl"><span class="nv">TUNNEL_TOKEN</span><span class="o">=</span>&lt;<span class="k">do</span> Cloudflare&gt;
</span></span><span class="line"><span class="cl"><span class="nv">FORCE_SSL</span><span class="o">=</span><span class="nb">true</span>
</span></span><span class="line"><span class="cl"><span class="nv">APP_LOCALE</span><span class="o">=</span>pt-BR</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>4. Suba:</strong></p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose pull
</span></span><span class="line"><span class="cl">docker compose up -d</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><strong>5. Acesse</strong> <code>https://frankmega.seudominio.com</code>, crie a conta admin, comece a usar.</p>
<h3>Cuidados Importantes<span class="hx:absolute hx:-mt-20" id="cuidados-importantes"></span>
    <a href="#cuidados-importantes" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li><code>FORCE_SSL=false</code> pra testar localmente em <code>http://localhost:3100</code> sem Cloudflare na frente, senão entra em redirect loop</li>
<li>WebAuthn origin precisa bater exatamente, incluindo o <code>https://</code>. Se errar, passkeys silenciosamente não funcionam</li>
<li>IPs do Cloudflare são hardcoded no initializer (porque o gem <code>cloudflare-rails</code> não é compatível com Rails 8.1). Se o Cloudflare mudar os ranges, precisa rebuildar a imagem</li>
<li>Chaves de criptografia são permanentes. Se trocar depois que usuários configuraram 2FA, os segredos OTP ficam ilegíveis e eles perdem acesso</li>
<li>Cloudflare free tem limite de 100 MB de upload. Pro plano grátis, arquivos acima disso não passam pelo tunnel. A app permite até 1 GB mas o Cloudflare é o gargalo</li>
<li>O container roda como UID 1000. Se usar bind mounts ao invés de named volumes, o diretório precisa ser owned pelo UID 1000</li>
<li>São 4 bancos SQLite separados em produção (app, cache, queue, cable), todos no volume <code>db_data</code>. Backup é copiar o volume inteiro</li>
</ul>
<h2>O Verdadeiro Ponto Sobre &ldquo;Vibe Coding&rdquo;<span class="hx:absolute hx:-mt-20" id="o-verdadeiro-ponto-sobre-vibe-coding"></span>
    <a href="#o-verdadeiro-ponto-sobre-vibe-coding" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O Claude Code é absurdamente produtivo. Eu não teria feito tudo isso em 3 horas sem ele. Lembra de antigamente que você dava murro em ponta de faca por 1 semana só pra fazer um login funcionar? Pois é.</p>
<p>O commit de I18n sozinho (66 arquivos, ~250 strings extraídas pra dois idiomas) levaria um dia inteiro manualmente.</p>
<p>Mas o ponto que ninguém quer ouvir: <strong>o LLM não tomou as decisões de segurança</strong>. Eu pedi pra ele fazer um audit e ele encontrou os problemas, mas eu precisei saber pedir. E precisei saber quais perguntas fazer. <em>&ldquo;Revise a segurança&rdquo;</em> é uma instrução vaga. <em>&ldquo;Verifique se tem race condition no increment do download counter&rdquo;</em> é uma instrução que só quem sabe o que é TOCTOU consegue dar.</p>
<p>Aquelas 56 linhas do IDEA.md carregam décadas de experiência em desenvolvimento web. Eu sabia pedir rate limiting porque já vi serviços derrubados por bots. Sabia pedir IP banning porque já lidei com abuse. Sabia pedir download counter atômico porque entendo concorrência.</p>
<p>Se um iniciante escrevesse o mesmo IDEA.md, provavelmente não teria metade dessas preocupações. E o LLM não iria sugerir espontaneamente. O resultado seria um serviço funcional, bonito, e completamente inseguro.</p>
<p>O valor do programador experiente no Vibe Coding não é escrever código. É saber o que pedir e revisar o que foi gerado. A experiência é o filtro entre &ldquo;funciona&rdquo; e &ldquo;funciona em produção&rdquo;.</p>
<p>Os 210 testes e 24 medidas de segurança não vieram de prompt engineering. Vieram de saber o que testar e por quê.</p>
<p>O código é open source sob AGPL-3.0: <a href="https://github.com/akitaonrails/frankmega"target="_blank" rel="noopener">github.com/akitaonrails/frank_mega</a>. Deploy no seu server, fucem o código, aprendam com os commits. E se acharem mais falhas de segurança, me avisem.</p>
<p>Lembrem-se:</p>
<blockquote>
  <p>A IA é o espelho da sua própria competência.</p>

</blockquote>
]]></content:encoded><category>vibe-coding</category><category>rails</category><category>homelab</category><category>seguranca</category></item><item><title>Vibe Code: Do Zero à Produção em 6 DIAS | The M.Akita Chronicles</title><link>https://www.akitaonrails.com/2026/02/16/vibe-code-do-zero-a-producao-em-6-dias-the-m-akita-chronicles/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/02/16/vibe-code-do-zero-a-producao-em-6-dias-the-m-akita-chronicles/</guid><pubDate>Mon, 16 Feb 2026 14:19:56 +0000</pubDate><description>&lt;p&gt;&lt;strong&gt;ASSINE O NOVO NEWSLETTER:&lt;/strong&gt; &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;
&lt;strong&gt;LEIA A VERSÃO BLOG:&lt;/strong&gt; &lt;a href="https://blog.themakitachronicles.com/"target="_blank" rel="noopener"&gt;Blog&lt;/a&gt;
&lt;strong&gt;OUÇA NO SPOTIFY!!&lt;/strong&gt; &lt;a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener"&gt;Podcast&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-14_18-59-53.png" alt="a ideia" loading="lazy" /&gt;&lt;/p&gt;
&lt;p&gt;No dia 10 de fevereiro de 2026, eu postei o tweet acima, só jogando no ar uma coisa que estava na minha to-do list há anos, desde que eu publicava vídeos no YouTube. Nunca fiz porque eu odeio HTML/CSS e ter que fazer template de e-mail era suficiente pra não começar. Além disso, fazer mais uma newsletter, que é só uma lista de links chata, não me motivava.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>ASSINE O NOVO NEWSLETTER:</strong> <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>
<strong>LEIA A VERSÃO BLOG:</strong> <a href="https://blog.themakitachronicles.com/"target="_blank" rel="noopener">Blog</a>
<strong>OUÇA NO SPOTIFY!!</strong> <a href="https://open.spotify.com/show/7MzG2UB7IAkC3GAwEXEIVD"target="_blank" rel="noopener">Podcast</a></p>
<p>&ndash;</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-14_18-59-53.png" alt="a ideia"  loading="lazy" /></p>
<p>No dia 10 de fevereiro de 2026, eu postei o tweet acima, só jogando no ar uma coisa que estava na minha to-do list há anos, desde que eu publicava vídeos no YouTube. Nunca fiz porque eu odeio HTML/CSS e ter que fazer template de e-mail era suficiente pra não começar. Além disso, fazer mais uma newsletter, que é só uma lista de links chata, não me motivava.</p>
<p>Mas nesse dia eu já estava terminando meu segundo projeto com vibe coding, o <a href="https://github.com/akitaonrails/FrankMD"target="_blank" rel="noopener">FrankMD</a>, e acho que entendi os principais truques e a metodologia pra fazer um produto completo da forma certa. Então, começaram a surgir várias ideias. Dei dump e escrevi este <a href="https://gist.github.com/akitaonrails/d2a7983fc4c839b8071f5d0babaadf94"target="_blank" rel="noopener">pequeno documento</a>.</p>
<p><a href="https://gist.github.com/akitaonrails/d2a7983fc4c839b8071f5d0babaadf94"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-16_12-40-04.jpg" alt="conceito original"  loading="lazy" /></a></p>
<p>Pensei: e se eu fizesse não só o código do sistema de newsletter com vibe code, mas também tivesse parte do próprio conteúdo gerada por IA de forma inusitada, em forma de comentaristas: eu e um robô? E, claro, com Guia do Mochileiro na mente, tinha que ser o MARVIN, ou melhor, o &ldquo;M.Arvin&rdquo; (pra ninguém encher com copyright).</p>
<p>É claro, vai ter versão por e-mail semanal e um blog de arquivos. Mas já que vai ter tudo isso, por que não reescrever já toda a newsletter em script de podcast? E já que recentemente saiu o <a href="https://qwen.ai/blog?id=qwen3tts-0115"target="_blank" rel="noopener">Qwen3 TTS</a>, open source, por que não treinar um LoRa com minha voz (obviamente, tenho centenas de horas de minha voz no YouTube) e fazer um podcast totalmente automatizado?</p>
<p>Finalmente, um dos desmotivadores pra mim seria criar um Web Admin tradicional e ter que entrar manualmente lá pra cadastrar os links de notícias e meus comentários. Eu sei que ia me dar preguiça e, a uma hora, ia cansar. Só tinha uma solução: criar um bot mais inteligente com o qual eu pudesse conversar via <strong>DISCORD</strong>. Assim, eu posso rapidamente acessar, seja do meu PC, seja do meu smartphone, sem precisar logar num sistema separado nem nada. Eu já converso com pessoas no Discord, então a fricção seria zero.</p>
<p>Então:</p>
<ul>
<li>Marvin Bot pra gerenciar todo conteúdo (que eu mando e os automatizados, com comentários e tudo mais)</li>
<li>Monitor de X.com pra checar o que eu postei e já pegar pra usar na newsletter. Incluindo as replies que eu dou, que vão virar Q&amp;A!</li>
<li>Newsletter website pra assinar, cancelar, etc</li>
<li>Blog pra arquivos de newsletter e de transcripts dos podcasts (também com termos de serviço e de privacidade — sim, sou LGPD compliant!)</li>
<li>Gerador automático de vozes e montador de podcast que vai subir automaticamente pra deixar disponível pro Spotify</li>
<li>E ainda templates de blog e de e-mail, com versões de tema light e dark.</li>
</ul>
<p>Você, que é um programador experiente, sem IA, quanto tempo levaria pra fazer tudo isso?</p>
<p>Pois bem, eu comecei no dia 10 de fevereiro. Terminei tudo no domingo, dia 15 de fevereiro. E hoje, dia 16, rodaram os jobs de produção pela primeira vez, o que gerou newsletter, blog posts, podcast, upload pro Spotify, envio de email.</p>
<p>E não foi do jeito podre que todo vibe coder amador faz:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-16_11-37-20.jpg" alt="newsletter stats"  loading="lazy" /></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-16_11-37-27.jpg" alt="bot stats"  loading="lazy" /></p>
<p>Quem é de Rails deve ter ficado surpreso: projeto com taxa de 1 pra 1 de testes! 1 linha de testes pra cada 1 linha de código: 100% de cobertura. E isso, sem contar um projeto separado deles de integração completa, ponta a ponta (que usei pra garantir que a produção ficaria perfeita).</p>
<p>Todas as estratégias que eu aprendi ao longo dos anos colocando projeto em produção: segurança, escalabilidade, durabilidade, garantia de entrega sem spammar duplicatas de e-mails, etc. Depois vejo se faço mais posts específicos sobre alguns dos truques técnicos.</p>
<p>Este projeto não vou abrir como open source. Eu fiz 100% customizado pra mim, pro meu fluxo de trabalho e meus estilos. Não é difícil pra outro programador competente fazer a mesma coisa com vibe code: se eu consegui, qualquer um consegue. Então é só não ter preguiça de pensar e fazer. Aliás, não entendo como tech youtubers ainda não estão lançando 1 projeto completo em produção, por vídeo, pra todo mundo testar e ver.</p>
<p>No screenshot também não está o servidor de Qwen3-TTS, que produz áudio. Esse é bem mais simples, um servidor PyTorch. Aqui foi Python mesmo. O mais importante é o fine tuning do modelo pra gerar vozes exatamente do jeito que eu queria:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-16_11-45-20.jpg" alt="qwen3 server"  loading="lazy" /></p>
<p>O blog é todo com <strong>Hugo/Hextra</strong>, como este também. E meu bot publica lá automaticamente.</p>
<h2>M.Arvin Bot<span class="hx:absolute hx:-mt-20" id="marvin-bot"></span>
    <a href="#marvin-bot" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>O M.Arvin tem uma personalidade que eu detalhei como eu queria. E é um bot de Discord, então meu trabalho é assim:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-16_12-45-48.jpg" alt="bot url register"  loading="lazy" /></p>
<p>Só preciso mandar um link com meu comentário (igual eu faria no X.com), ou posso postar direto no X, que ele vai pegar igual. Daí ele busca o conteúdo, faz um resuminho, faz um pequeno fact-check e o M.Arvin gera seu comentário em cima do meu.</p>
<p>Eu programei ele pra responder a um monte de comandos pra facilitar meu dia a dia. Isso é só uma pequena parte:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-16_11-56-13.jpg" alt="bot help commands"  loading="lazy" /></p>
<p>Eis um exemplo: eu tenho controle sobre quanto de IA ele está usando (é bem barato):</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-16_11-57-07.jpg" alt="AI cost"  loading="lazy" /></p>
<h2>Deployment<span class="hx:absolute hx:-mt-20" id="deployment"></span>
    <a href="#deployment" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Neste post não vou detalhar muito, mas uma coisa em que demorei bastante, passando por várias discussões e versões com o Claude Code, foi na arquitetura de infraestrutura. E no final ele conseguiu me dar um Deployment Guide, que consegui seguir até o fim e sabendo que minimamente está como eu mesmo faria se tivesse feito tudo sozinho:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-16_11-58-45.jpg" alt="topology"  loading="lazy" /></p>
<p>Nenhum software tem garantia de ser 100% perfeito; este também não deve ser. Mesmo eu tendo revisado por dias, passando por cada etapa várias vezes, sempre vai ter algum bug que eu não previ. Eu &ldquo;acho&rdquo; que, pelo menos, tudo o que sei que costuma dar problema em produção, &ldquo;acho&rdquo; que consegui cobrir de forma satisfatória. Se fosse uma equipe humana, eu teria colocado em produção num estado pior do que o que atingi com o Claude.</p>
<p>Tudo o que dava pra proteger com Cloudflare, eu configurei. Todas as boas práticas pra e-mail/mailing, eu segui. Todas as precauções contra acidentes eu tomei (todo deploy automatizado com Kamal) e todos os e-mails dos assinantes foram encriptados com AES-256 no banco de dados (mesmo que vazem por acidente, não vazam os dados dos usuários). Não implementei profiling, cookies nem pixels de rastreamento, nada. Privacidade foi o foco.</p>
<p>Tudo o que dava pra reduzir custos, eu reduzi; afinal, vou pagar do meu bolso. Então, coisas como GPU na RunPod pra gerar os áudios são serverless. Eu subo só no momento de gerar; ele faz todo o processo e, depois, a máquina desliga. O servidor está todo organizado numa imagem Docker leve. Todos os modelos de IA estão num volume de rede separado.</p>
<h2>Spoiler do Processo<span class="hx:absolute hx:-mt-20" id="spoiler-do-processo"></span>
    <a href="#spoiler-do-processo" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Depois pretendo fazer um post maior falando da minha metodologia, mas é muito simples: trate o Claude Code/Codex como se fosse um programador humano esforçado — que vai cometer muitos erros, mas sabe obedecer ordens super explícitas, diretas, claras e sem ambiguidade. Meu papel é de gerente de produto, Tech Lead ou QA. Têm que ser os TRÊS papéis.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-16_12-06-47.jpg" alt="commits"  loading="lazy" /></p>
<p>No total, o Claude Code fez 201 commits. Mas não são commits normais; considero que, num projeto normal com humanos, cada um teria feito aproximadamente <strong>150 Pull Requests</strong>.</p>
<p>Pergunta a vocês que participam de sprints em projetos: quanto tempo leva pra ter 150 user stories/bug fixes aprovados e em produção no seu projeto real? Quantos user stories vocês conseguem fazer e aprovar (com QA e tudo) em UMA semana?</p>
<p>Eis um exemplo de um commit/&ldquo;user story&rdquo;:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-16_12-08-55.jpg" alt="commit example"  loading="lazy" /></p>
<p>Quando estava lá no commit, talvez 50, eu já pensava: &ldquo;Já funciona!&rdquo;* É o estágio que todo criador de conteúdo (que nunca colocou projetos de verdade em produção) já postaria, dizendo: <em>&ldquo;fiz vibe code em 2 dias&rdquo;</em>. De fato, em 2 dias eu já tinha o grosso das funcionalidades prontas.</p>
<p>Os outros 4 dias foram:</p>
<ul>
<li>1 dia pra deploy em produção (quando tem tudo planejado com antecedência, todas as contingências já pensadas, é fácil).</li>
<li>3 dias só testando, criando testes automatizados/integração, gerando e regerando, exercitando todas as etapas de todas as partes, múltiplas vezes, e fazendo ajustes, refatorações ou até mesmo jogando fora um pedaço inteiro pra mudar, porque os testes indicaram que valia mais a pena.</li>
</ul>
<p>Isso levou de 50 a 200 commits! E é a parte que todo vibe coder <strong>pula</strong>. Pra ser justo, é a parte que qualquer pessoa, com ou sem IA, pularia. E é por isso que sempre os projetos dão errado e vão pra produção cheios de erros e de buracos óbvios de segurança.</p>
<blockquote>
  <p>Mesmo depois de pedir ao Claude Code pra revisar boas práticas de segurança e de cobertura de testes, múltiplas vezes. Depois eu abri o Codex e pedi pra fazer a mesma coisa, e ele ainda achou mais buracos de segurança e mais testes faltando, múltiplas vezes. Mesmo depois dos dois fazerem essas checagens várias vezes, eu ainda encontrei sozinho mais buracos que mandei consertar. Em nenhum momento, nenhum dos dois conseguiu achar &ldquo;TUDO&rdquo;. Ninguém consegue, nem a IA.</p>

</blockquote>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Programar Vibe Coding da forma correta só tem um jeito: Extreme Programming!</p>
<p>Sim, todas as coisas &ldquo;chatas&rdquo; e &ldquo;burocráticas&rdquo; que todo desenvolvedor odeia fazer são o que garante que a IA vai devolver um resultado aceitável pra colocar em produção:</p>
<ul>
<li>Pair Programming: você precisa mandar a IA planejar primeiro e te dizer exatamente tudo o que vai fazer, ANTES de começar. Eu reviso, ajusto e, só quando estou satisfeito, deixo fazer. Fico olhando cada coisa que ele faz e, no final, mando ajustar o que acho que faltou.</li>
<li>Test-Driven Development: toda funcionalidade deve vir acompanhada de testes unitários. Toda correção de bug precisa de testes de regressão.</li>
<li>Continuous Integration: Toda vez que termina de implementar, precisa rodar script de CI pra ver se nada quebrou em outro lugar. Mais do que isso: no branch master, só há commits com testes que passam. Eu posso reverter em qualquer ponto e o projeto roda.</li>
<li>Small Releases: isso eu achei ótimo no Claude, mesmo se eu acabar esquecendo de mandar ele comitar uma tarefa e mandar fazer outra, no fim, ele não faz aquela tosquice de <code>git add .</code> — não: ele separa em 2 commits e descreve cada um da forma correta. Além disso, eu subi produção na sexta-feira, e continuei sábado e domingo testando integração local e em ambiente de produção, pra garantir que não tem isso de <em>&ldquo;só na minha máquina funciona&rdquo;</em></li>
<li>Coding Standard: eu iniciei com o mínimo de requerimentos técnicos, mas, durante o desenvolvimento, mandei refatorar do meu jeito várias e várias vezes.</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-16_12-21-22.jpg" alt="tech stack"  loading="lazy" /></p>
<p>Essas são as principais tarefas a serem feitas. Já adianto: &ldquo;Spec Driven Development&rdquo; não é totalmente inútil, mas a estrutura não tem importância: é o seu texto que conta.</p>
<p>É a mesma coisa que User Stories. Grande bosta que você escreveu &ldquo;Eu, como o usuário final, Quero que quando digitar X &hellip;, o sistema responda com Y&rdquo;. O formato NUNCA é importante. O que é importante é COMO você escreveu o que vai no meio. Se você não sabe escrever, a IA não vai saber responder a você como deveria. Vou fazer outro artigo só pra demonstrar como todos vocês são péssimos em escrever.</p>
<p>O recado é simples: Vibe Code funciona, em partes.</p>
<blockquote>
  <p>O que funciona mesmo é &ldquo;Senior Agile Vibe Coder&rdquo;</p>

</blockquote>
<p>Vou perguntar de novo: quanto tempo, sem IA, você levaria pra produzir 150 user stories/bug fixes, no escopo que resumi no começo? Até colocar em produção com tudo testado? Duvido que seja em 6 dias como eu fiz.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>vibe-coding</category><category>agile</category></item><item><title>Vibe Code: Qual LLM é a MELHOR?? Vamos falar a REAL</title><link>https://www.akitaonrails.com/2026/01/29/vibe-code-qual-llm-%C3%A9-a-melhor-vamos-falar-a-real/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/01/29/vibe-code-qual-llm-%C3%A9-a-melhor-vamos-falar-a-real/</guid><pubDate>Thu, 29 Jan 2026 10:43:16 -0300</pubDate><description>&lt;p&gt;Esta é a pergunta que não quer calar:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;ldquo;Eu, como um total zero à esquerda em programação, quero fazer um app perfeito, seguro, de qualidade, somente com prompts. Qual LLM vai me atender?&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A minha resposta de 2 anos atrás e até agora continua sendo:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NENHUMA!&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Ontem eu fiz &lt;a href="https://www.akitaonrails.com/2026/01/28/vibe-code-eu-fiz-um-appzinho-100-com-glm-4-7-tv-clipboard/"&gt;um LONGO artigo&lt;/a&gt; onde eu DEMONSTRO como é fazer um appzinho do zero somente com Vibe Coding. Um escopo menor que um exercício de &amp;ldquo;to-do list&amp;rdquo;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Esta é a pergunta que não quer calar:</p>
<blockquote>
  <p><em>&ldquo;Eu, como um total zero à esquerda em programação, quero fazer um app perfeito, seguro, de qualidade, somente com prompts. Qual LLM vai me atender?&rdquo;</em></p>

</blockquote>
<p>A minha resposta de 2 anos atrás e até agora continua sendo:</p>
<blockquote>
  <p><strong>NENHUMA!</strong></p>

</blockquote>
<p>Ontem eu fiz <a href="/2026/01/28/vibe-code-eu-fiz-um-appzinho-100-com-glm-4-7-tv-clipboard/">um LONGO artigo</a> onde eu DEMONSTRO como é fazer um appzinho do zero somente com Vibe Coding. Um escopo menor que um exercício de &ldquo;to-do list&rdquo;.</p>
<p>Pra &ldquo;fazer funcionar&rdquo; foi fácil: em 1 hora eu tinha o app. O tal &ldquo;zero à esquerda&rdquo; acima, pararia aqui.</p>
<p>Mas pra fazer um código de qualidade, seguro, otimizado, testado, etc, me custou as próximas 12 horas e mais de 250 prompts e intervenções manuais.</p>
<p>Quando postei, alguns ainda vieram <em>&ldquo;Ah, mas você usou GLM, se tivesse usado Claude Code ou Codex, teria sido mil vezes melhor.&rdquo;</em></p>
<p>Ok, engoli o bait.</p>
<p>Hoje eu passei mais umas boas 2 horas em cima do código final do GLM e fiz o seguinte:</p>
<ul>
<li>
<p>Mandei o Claude Code fazer uma análise EXTENSIVA de segurança, otimização, testes, etc e ele saiu com <a href="https://github.com/akitaonrails/tvclipboard/commit/d3e2b1b8dcbe1f0451995623df17b38ad236dbb7"target="_blank" rel="noopener">este commit</a></p>
</li>
<li>
<p>Em cima disso que o Opus corrigiu, pedi pro GPT 5.2 Codex fazer a mesma análise EXTENSIVA. E ele saiu com <a href="https://github.com/akitaonrails/tvclipboard/commit/78dcfb11e58d026e2bc34476d849561724879fa8"target="_blank" rel="noopener">este commit</a></p>
</li>
<li>
<p>Não satisfeito, e vendo que acabou de sair o Kimi 2.5, pedi a ele pra fazer a mesma análise EXTENSIVA. E ele saiu com <a href="https://github.com/akitaonrails/tvclipboard/commit/0b522ab0976ab64b8855ed668c0dc2df062776b8"target="_blank" rel="noopener">este commit</a></p>
</li>
<li>
<p>Mas pra garantir mesmo, a seguir pedi ao Gemini 3.0 Preview a mesma análise EXTENSIVA. E ele saiu <a href="https://github.com/akitaonrails/tvclipboard/commit/3dc2b26291b7b7d2518762994c71ecdb096449b4"target="_blank" rel="noopener">com este commit</a></p>
</li>
<li>
<p>Não resisti e quis terminar mandando o MiniMax v2.1 também participar fazendo a mesma análise EXTENSIVA. E ele saiu <a href="https://github.com/akitaonrails/tvclipboard/commit/9c4c320a0c946b6a6fb1c8b69270afae96b8bd43"target="_blank" rel="noopener">com este commit</a></p>
</li>
</ul>
<p><strong>Estão entendendo??? <em>LET THAT FUC&ndash;NG SINK IN!!</em></strong></p>
<p>NENHUM é capaz de fazer um <strong>appzinho menor que um to-do list</strong> da <strong>forma perfeita</strong>. Isso não existe.</p>
<blockquote>
  <p>A única coisa que existe são seus requerimentos. A qualidade do app é diretamente proporcional à sua senioridade!</p>

</blockquote>
<p>Quanto mais júnior você for, pior vai ser seu app, com ou sem IA. Quanto mais sênior você for, melhor vai ser seu app, com ou sem IA. A IA é correlacionada diretamente com seu conhecimento E sua experiência.</p>
<p>IAs não são gênios com vontade própria. Elas não vão adivinhar o que você QUER e nem o que você REALMENTE PRECISA.</p>
<blockquote>
  <p>O que você QUER e o que você PRECISA costumam ser BEM diferentes.</p>

</blockquote>
<p>Não adianta colocar segurança da NASA num sistema que não precisa. Não adianta deixar senha de admin aberta e colocar em produção. Você precisa saber quais são os limites certos pro seu caso de uso particular.</p>
<p>IAs são máquinas de probabilidade, cuspidores de texto glorificados, sem nenhuma inteligência. Elas nunca vão alcançar 100% porque isso é impossível.</p>
<p>E eu digo &ldquo;matematicamente&rdquo; impossível. Isso porque a quantidade de esforço/recursos necessários pra subir cada por cento nessa escala, aumenta exponencialmente. Como se diria em Cálculo &ldquo;no limite&rdquo;, o 100% vai pra infinito.</p>
<p>Então sempre necessariamente vai ser &ldquo;abaixo de 100%&rdquo;.</p>
<p>Os 99% do esforço servem pra fazer um app to-do list mínimo. O resto do caminho: escalabilidade, otimização, segurança, mantenabilidade, etc dependem unicamente de quem está pedindo: <strong>você</strong>.</p>
<h2>Russian Doll de LLMs<span class="hx:absolute hx:-mt-20" id="russian-doll-de-llms"></span>
    <a href="#russian-doll-de-llms" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Essa brincadeira de Opus ➡️ GPT 5.2 ➡️ Kimi 2.5 ➡️ Gemini 3 Pro ➡️ MiniMax V2.1 foi até barato, no total só uns <strong>USD 3</strong>. Mas novamente, é um app menor que um to-do list. &ldquo;Deveria&rdquo; ter sido trivial pra qualquer um deles, não acham?</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129112823_screenshot-2026-01-29_11-24-43.png" alt="openrouter billing"  loading="lazy" /></p>
<p>Vamos rapidamente dar uma olhada no que as LLMs fizeram depois que meu appzinho já estava pronto.</p>
<p>Pra quem não viu o <a href="/2026/01/28/vibe-code-eu-fiz-um-appzinho-100-com-glm-4-7-tv-clipboard/">artigo anterior</a>, o código final está <a href="https://github.com/akitaonrails/tvclipboard"target="_blank" rel="noopener">neste GitHub</a>.</p>
<h3>1. Opus 4.5<span class="hx:absolute hx:-mt-20" id="1-opus-45"></span>
    <a href="#1-opus-45" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://github.com/akitaonrails/tvclipboard/commit/d3e2b1b8dcbe1f0451995623df17b38ad236dbb7"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129105749_screenshot-2026-01-29_10-57-42.png" alt="opus commit"  loading="lazy" /></a></p>
<p>Já tinha dito isso antes e repito, na dúvida, <strong>use Claude Code</strong>. É o melhor balanceado entre velocidade e qualidade. Se usar o plano Max, também é um bom custo-benefício. Concordo que ele é superior ao GLM 4.7, isso nunca foi uma questão.</p>
<p>Ao mesmo tempo, <strong>não é mágica</strong> e nem é infalível, muito longe disso. O processo todo que descrevi no artigo anterior vai ser exatamente o mesmo, só vai demorar bem menos (custando menos da minha paciência).</p>
<p>O GLM realmente apanhou muito na checagem de CORS e como o Opus demonstra, ainda sobraram bugs nessa checagem.</p>
<p>O Opus ajustou algumas coisas que são ok, mas desnecessárias. Melhorar a criptografia da comunicação não era necessária, porque era só uma obfuscação. A encriptação de verdade sempre vai ser TLS. Mas tudo bem, é bom manter ajustado.</p>
<p>Mas o que o GLM deixou passar reto e o Opus também é que na primeira versão o token era encriptado porque ia na URL, mas depois que mudamos pra server-side, não precisava mais. Mas ainda &ldquo;sobrou&rdquo; código velho disso, e o Opus, em vez de remover, tentou melhorar esse código velho!!</p>
<p>Deveria ser óbvio que não precisa: é server-side, o novo token é só um ID aleatório. Não tem nenhuma necessidade de manter um timestamp encriptado no servidor.</p>
<p>Ele também quis consertar a possibilidade de overflow na hora de carregar arquivo de I18n. Mas isso é algo que eu controlo, não o usuário. Não tem como ter overflow. É overengineering, mas tudo bem também.</p>
<blockquote>
  <p>Opus tende a ser proativo demais em aspectos que costumam ser desnecessários, dando impressão que é mais inteligente do que realmente é.</p>

</blockquote>
<p>Sabe aquele estagiário chato que lê muito blog post e em toda reunião fica soltando coisas aleatórias pra mostrar que sabe? Mas que na prática não faz nenhuma diferença? Pois é, é isso.</p>
<h3>2. GPT 5.2 Codex<span class="hx:absolute hx:-mt-20" id="2-gpt-52-codex"></span>
    <a href="#2-gpt-52-codex" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://github.com/akitaonrails/tvclipboard/commit/78dcfb11e58d026e2bc34476d849561724879fa8"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129110815_screenshot-2026-01-29_10-22-30.png" alt="codex commit"  loading="lazy" /></a></p>
<p>Apesar de ser Open<del>Lie</del>AI, eu acho o GPT 5.2 Codex mais &ldquo;honesto&rdquo; do que o Opus. Ele não tenta ser tão <em>smartpants</em> o tempo todo, querendo se mostrar onde não precisa. Eu acho que ele vai mais direto ao ponto e segue mais de perto exatamente o que eu pedi, em vez de tentar fazer tangentes que eu não pedi - e acabam sendo desnecessárias.</p>
<p>Só desta vez, sei lá porque, ele fez o pior commit message de todos (super curto e sem explicação). Mas ele achou uma coisa bem idiota que GLM e Opus não prestaram atenção: configuraçãozinha boba de timeout:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129113126_screenshot-2026-01-29_11-31-18.png" alt="timeout http"  loading="lazy" />
Pro meu app, não é necessário. Mas neste estágio que eu realmente pedi pra analisar EXTENSIVAMENTE, me surpreende que o prolixo do Opus deixou passar isso.</p>
<p>E o GPT esbarrou no problema que falei acima do token encriptado: tive que interromper quando ele começou a tentar reimplementar a encriptação porque viu sobras do passado. Nesse caso eu mandei explicitamente remover essas sobras. Mas nem GLM, nem Opus e nem GPT se ligaram que isso não era mais necessário e continuaram tentando mexer nessa sobra velha.</p>
<h3>3. Kimi 2.5<span class="hx:absolute hx:-mt-20" id="3-kimi-25"></span>
    <a href="#3-kimi-25" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://github.com/akitaonrails/tvclipboard/commit/0b522ab0976ab64b8855ed668c0dc2df062776b8"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129113458_screenshot-2026-01-29_11-34-50.png" alt="kimi commit"  loading="lazy" /></a></p>
<p>Toda semana surge mais uma versão nova que todo mundo fica hypando. 3 semanas atrás era MiniMax v2.1, 2 semanas atrás era GLM 4.7, agora é Kimi 2.5, então resolvi testar ele pelo OpenRouter. E o TL;DR é que eu gostei dele.</p>
<p>Achei mais lento que Opus ou GPT, mas melhor que GLM. E ele realmente continuou achando itens que não sei como o Opus deixou passar.</p>
<p>Novamente, é uma coisa besta e desnecessária pro meu appzinho, mas ninguém até agora se preocupou em configurar headers de segurança:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129113609_screenshot-2026-01-29_11-36-02.png" alt="security headers"  loading="lazy" /></p>
<blockquote>
  <p>Em um web app de verdade, tem que colocar!</p>

</blockquote>
<p>Outra coisa desnecessária mas que precisa num app de verdade: pattern de <strong>WebSocket Ping/Pong</strong> pra não desconectar se ficar idle:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129113738_screenshot-2026-01-29_11-37-29.png" alt="ping pong"  loading="lazy" /></p>
<p>O interessante foi o Opus não ter sugerido isso. Viram? Ele é prolixo, fica sugerindo um monte de pequenas coisinhas desnecessárias pra parecer inteligente, mas as coisas realmente interessantes, ele não lembra.</p>
<p>Mas aí o Kimi também sugeriu colocar de volta uma funcionalidade que eu tinha mandado o GLM tirar: rotina de limpeza do storage de tokens in-memory:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129120302_screenshot-2026-01-29_11-39-05.png" alt="cleanup"  loading="lazy" /></p>
<p>Como já expliquei: isso é server-side. Quando acabar espaço, automaticamente o novo token toma lugar do mais antigo e pronto. Mas o Kimi provavelmente ficou preocupado de deixar tokens expirados sobrando na memória. Só que é overengineering: é um storage hiper pequeno, kilobytes, talvez. Não precisa deixar uma rotina rodando em background pra ficar limpando uma lista em memória.</p>
<p>Mesmo num app de produção, eu não colocaria isso!</p>
<p>Teve <a href="https://x.com/BasedTorba/status/2016203491371315395"target="_blank" rel="noopener">um tweet</a> de brincadeira sugerindo que o Kimi era uma versão pirata do Claude. Apesar de ser piada, esse comportamento de ficar implementando coisas desnecessárias que nem um estagiário que lê blog post e quer ficar se mostrando, é realmente bem parecido com o Claude. 😂</p>
<h3>4. Gemini 3 Pro Preview<span class="hx:absolute hx:-mt-20" id="4-gemini-3-pro-preview"></span>
    <a href="#4-gemini-3-pro-preview" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://github.com/akitaonrails/tvclipboard/commit/3dc2b26291b7b7d2518762994c71ecdb096449b4"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129114149_screenshot-2026-01-29_11-41-43.png" alt="gemini commit"  loading="lazy" /></a></p>
<p>Diferente do Opus, assim como GPT, eu tendo a pensar no Gemini como sendo um pouco mais &ldquo;honesto&rdquo; e direto ao ponto também. Às vezes eu ainda pego ele se enrolando e sugerindo tangentes desnecessárias, mas no geral eu sinto que ele se prende ao meu prompt um pouco mais de perto mesmo.</p>
<p>No caso dele, além da análise, eu pedi explicitamente pra reavaliar a necessidade da rotina de cleanup que o Kimi adicionou a mais.</p>
<blockquote>
  <p>E o Gemini concordou com o Kimi e quis manter, e ainda otimizou a rotina!! 🤦‍♂️</p>

</blockquote>
<p>Mas pelo menos ele ainda achou um <strong>deadlock</strong> durante o shutdown do servidor que ninguém antes pensou. Não é nada grave, e é só na situação de shutdown, mas mesmo assim, eu pedi uma análise EXTENSIVA de todos os anteriores, eles sugeriram coisas desnecessárias, e pularam isso. Num app de produção é importante ter esse cuidado. É o tipo de coisa que pode bloquear restart de containers em produção ou causar delays/timeout sem sentido.</p>
<h3>5. MiniMax v2.1<span class="hx:absolute hx:-mt-20" id="5-minimax-v21"></span>
    <a href="#5-minimax-v21" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://github.com/akitaonrails/tvclipboard/commit/9c4c320a0c946b6a6fb1c8b69270afae96b8bd43"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129114623_screenshot-2026-01-29_11-46-16.png" alt="minimax commit"  loading="lazy" /></a></p>
<p>A esta altura eu realmente espero que não tenha mais muita coisa que precise mexer, mas o MiniMax ainda achou mais coisinhas bestas faltando.</p>
<p>O Kimi colocou headers de segurança faltando no servidor HTTP mas esqueceu de fazer o mesmo pra WebSockets:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129120501_screenshot-2026-01-29_11-47-44.png" alt="security headers extra"  loading="lazy" /></p>
<p>E a esta altura, 5 LLMs depois, ainda tem algumas besteiras de boas práticas de Go que todo mundo deixou passar. Esta bobagem, por exemplo:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129114834_screenshot-2026-01-29_11-48-27.png" alt="for loop go"  loading="lazy" /></p>
<p>De novo: isso não é nada. Mas todo mundo checou, analisou, rodou linters e tudo mais e só o MiniMax consertou essa sobra. <strong>SEMPRE TEM SOBRA</strong></p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Dá pra continuar, eu poderia continuar rodando mais LLMs em cima disso. Mas lembrem-se: isso tudo foi só em cima de um appzinho miseravelmente pequeno, menor que um to-do list. Imagine num app grande, complexo, com usuários de verdade, em produção?</p>
<blockquote>
  <p>Não subestimem programação, é muito mais complexo do que vocês, iniciantes, pensam.</p>

</blockquote>
<p>Eu acabei de rodar o equivalente a dezenas de bilhões de dólares, devo ter queimado energia suficiente pra abastecer uma casa inteira nos datacenters da Z.AI, Anthropic, OpenAI, Google, etc e ainda assim, <strong>tem sobras</strong>.</p>
<p>E mesmo com tudo isso, eu não tenho confiança nenhuma de colocar este appzinho em público. Pro meu uso pessoal privado está overengineered, mas pra produção, eu ainda precisaria gastar horas de revisão manual e testes manuais, QA extensivo, scans de segurança e tudo mais pra garantir que não sobrou nada importante ainda.</p>
<p>LLMs são EXCELENTES, me ajudam muito. Mas são <strong>estagiários muito motivados</strong>, e não gênios que fazem tudo sozinhos. Longe disso.</p>
]]></content:encoded><category>benchmarks-de-llm</category><category>llms</category><category>vibe-coding</category></item><item><title>Vibe Code: Eu fiz um appzinho 100% com GLM 4.7 (TV Clipboard)</title><link>https://www.akitaonrails.com/2026/01/28/vibe-code-eu-fiz-um-appzinho-100-com-glm-4-7-tv-clipboard/</link><guid isPermaLink="true">https://www.akitaonrails.com/2026/01/28/vibe-code-eu-fiz-um-appzinho-100-com-glm-4-7-tv-clipboard/</guid><pubDate>Wed, 28 Jan 2026 08:41:52 -0300</pubDate><description>&lt;p&gt;Ontem eu postei o seguinte no X:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://x.com/AkitaOnRails/status/2016175111414452636"target="_blank" rel="noopener"&gt;&lt;img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128084349_screenshot-2026-01-28_08-43-38.png" alt="tweet" loading="lazy" /&gt;&lt;/a&gt;
Isso é um conceito que falei múltiplas vezes nos vídeos do meu canal e em palestras. Tudo em programação acontece mais ou menos na proporção de Pareto: 80/20:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;80% do código é &amp;ldquo;fácil&amp;rdquo; e normalmente feito em 20% do tempo. Os 20% mais importantes vão ocupar a maioria dos 80% do tempo total.&lt;/li&gt;
&lt;li&gt;Bugs e problemas como performance ou segurança, se consertar os top 20%, vai resolver 80% dos problemas gerais.&lt;/li&gt;
&lt;li&gt;20% do escopo é realmente importante, por isso sempre dá pra simplificar, substituir, mudar, ou até cortar 80% - e é assim que se ajusta um projeto pra caber em orçamento/prazo.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tudo é baseado em Top X. &lt;a href="https://akitaonrails.com/2018/09/18/akitando-11-projetos-aprendendo-a-priorizar/"target="_blank" rel="noopener"&gt;Aprender a priorizar&lt;/a&gt; é aprender a identificar os 20%. A diferença de alguém sênior/experiente vs alguém amador/iniciante, é que iniciantes acham que 100% é importante e não conseguem decidir os 20%. O que a experiência traz é a capacidade de cada vez separar mais rápido esses 20%.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Ontem eu postei o seguinte no X:</p>
<p><a href="https://x.com/AkitaOnRails/status/2016175111414452636"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128084349_screenshot-2026-01-28_08-43-38.png" alt="tweet"  loading="lazy" /></a>
Isso é um conceito que falei múltiplas vezes nos vídeos do meu canal e em palestras. Tudo em programação acontece mais ou menos na proporção de Pareto: 80/20:</p>
<ul>
<li>80% do código é &ldquo;fácil&rdquo; e normalmente feito em 20% do tempo. Os 20% mais importantes vão ocupar a maioria dos 80% do tempo total.</li>
<li>Bugs e problemas como performance ou segurança, se consertar os top 20%, vai resolver 80% dos problemas gerais.</li>
<li>20% do escopo é realmente importante, por isso sempre dá pra simplificar, substituir, mudar, ou até cortar 80% - e é assim que se ajusta um projeto pra caber em orçamento/prazo.</li>
</ul>
<p>Tudo é baseado em Top X. <a href="https://akitaonrails.com/2018/09/18/akitando-11-projetos-aprendendo-a-priorizar/"target="_blank" rel="noopener">Aprender a priorizar</a> é aprender a identificar os 20%. A diferença de alguém sênior/experiente vs alguém amador/iniciante, é que iniciantes acham que 100% é importante e não conseguem decidir os 20%. O que a experiência traz é a capacidade de cada vez separar mais rápido esses 20%.</p>
<p><strong>PORÉM</strong>, é muito fácil só dizer isso. Todo mundo já ouviu isso pelo menos uma vez na vida, mas provavelmente ainda não consegue entender o que significa. Por isso resolvi fazer este (LONGO) artigo: vou mostrar <strong>na prática</strong> o que são os 80% fáceis e os 20% difíceis e como isso realmente afeta a qualidade do código, o tempo gasto e o custo.</p>
<p>Este artigo é para iniciantes, principalmente, mas também pra você: sênior que tem júniors pra mentorar ou professores com alunos. Usem como material de discussão e estudo com seus aprendizes. Esse é o tipo de coisa que se deve ensinar.</p>
<p>Como vai ser longo, lembrem-se que na versão desktop, do lado direito 👉 tem um menu com as seções pra pular direto pra cada assunto e estudar aos poucos.</p>
<h2>O App: TV Clipboard<span class="hx:absolute hx:-mt-20" id="o-app-tv-clipboard"></span>
    <a href="#o-app-tv-clipboard" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Escolhi um appzinho que eu queria faz tempo pro meu uso pessoal. É mais simples que um to-do list, não precisa de banco de dados nem nenhum outro serviço externo. Na prática é um comunicador peer-to-peer (p2p) via websockets.</p>
<p>O caso de uso é o seguinte: eu tenho um mini-PC gamer que conecto na TV da sala. Normalmente deixo um mouse do lado da poltrona pra navegar nele (é um maldito Windows 11). Mas eventualmente eu preciso digitar alguma coisa (senhas, preencher algum formulário). E eu não quero deixar minhas contas importantes logadas nesse PC (não confio em PC gamer).</p>
<p>Ou eu tenho que pegar meu teclado bluetooth, ou usar o teclado virtual do Windows. Ambas as opções são inconvenientes pra uma sala. MAS, e se eu pudesse abrir um site (que já vai estar no bookmark), aparece um QR Code que eu abro via meu smartphone e dali posso digitar ou colar o que está no meu clipboard mobile e transferir pro clipboard do mini-PC imediatamente? Esse é o app:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128090208_20260127_192137.jpg" alt="tvclipboard host screen"  loading="lazy" /></p>
<p>Daí eu abro a câmera do meu celular, escaneio esse QR Code e ele vai abrir esta página:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128090700_Screenshot_20260128_090600_Brave.jpg" alt="tvclipboard client screen"  loading="lazy" /></p>
<p>Pronto, agora posso digitar qualquer coisa ou colar a senha do meu gerenciador de senhas (clicando em &ldquo;paste&rdquo;) e quando enviar, na TV vou ver o seguinte:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128090907_20260127_195954.jpg" alt="tvclipboard host received screen"  loading="lazy" /></p>
<p>Vai aparecer que recebi, e automaticamente já vai pro clipboard da TV. Ele mostra &ldquo;blurred&rdquo; porque se for uma senha não quero que fique legível grande na TV - e sim, o mouse é gigante porque é mais fácil de eu ver na TV.</p>
<p>Isso é útil não só pra TV, mas pra qualquer situação onde eu queira mandar textos do meu celular pra um dos meus PCs também. Muita gente acaba usando WhatsApp ou outros comunicadores pra isso. Eu, por exemplo, tenho um grupo &ldquo;Só eu&rdquo; que só tem eu nele, pra ficar mandando links e coisas bobas do meu celular. Daí eu sempre deixo o WhatsApp Web aberto numa aba do meu PC e posso pegar de lá. WhatsApp pra mim é um clipboard remoto.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128091222_screenshot-2026-01-28_09-12-13.png" alt="whatsapp só eu"  loading="lazy" /></p>
<p>Mas isso não é seguro pra mandar coisas como senhas, endereços de cripto, ou qualquer outra mensagem mais sensível. WhatsApp é uma empresa sem nenhuma transparência. A propaganda &ldquo;diz&rdquo; que eles têm encriptação de ponta a ponta, mas eu não acredito nisso. É igualmente provável que eles abram tudo no meio do caminho e deixem registrado - são dados que valem dinheiro, ainda mais nesta era de treinamento de LLMs.</p>
<blockquote>
  <p>Recomendação: NUNCA COMUNIQUE NADA IMPORTANTE VIA WHATSAPP.</p>

</blockquote>
<p>Pra transferir arquivos eu já uso <a href="https://localsend.org/pt-BR"target="_blank" rel="noopener"><strong>LocalSend</strong></a>:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128091432_screenshot-2026-01-28_09-00-27.png" alt="localsend"  loading="lazy" /></p>
<p>Daí que veio a inspiração: eu queria um LocalSend pra Clipboard. Eu tenho certeza que já existam dezenas de apps que fazem exatamente isso. Mas aí eu queria aproveitar pra fazer este exercício.</p>
<h2>O Plano: 100% Vibe Code<span class="hx:absolute hx:-mt-20" id="o-plano-100-vibe-code"></span>
    <a href="#o-plano-100-vibe-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Honestamente, eu sei o suficiente de front-end, comecei minha carreira sendo &ldquo;web master&rdquo; nos anos 90. Mas eu detesto front-end. É demorado, é trabalhoso, yuck. Em particular, detesto lidar com CSS. Em mais particular, não tenho prazer em fazer Javascript. Por isso, sempre tenho preguiça de começar um web app do zero. Mas com Vibe Coding, essa é uma das partes que eu já posso me livrar, especialmente se eu não me importo com a aparência exata que vai ficar, contanto que não fique torto.</p>
<p>Aproveitando, vou usar uma linguagem que eu sei o básico mas não tenho nem experiência suficiente e muito menos fluência (ou seja, sozinho eu seria lento): <strong>GoLang</strong>. Por que Go:</p>
<ul>
<li>compila binário nativo: eu nem preciso da performance mas ter um compilador vai ajudar a LLM ter feedback de erros óbvios.</li>
<li>sem dependências: depois vou querer fazer deploy no meu home server, e vai ser infinitamente mais fácil fazer o setup de docker com um binário sem dependências, comparado com um Ruby ou Python que tem que instalar muita coisa.</li>
<li>ferramental padrão completo: além de compilador ele já tem formatador, linter, testes e tudo mais que também vai dar bom feedback à LLM</li>
<li>sintaxe simples, curta e pouco ambígua. Gosto como Go é Python-like. Não considero nenhuma das duas &ldquo;bonitas&rdquo; (em termos de estética, eu prefiro muito mais um Swift, Kotlin ou Ruby). Mas diferente de Javascript e até mesmo Rust, é bem menos &ldquo;verbose&rdquo; e bem menos ambígua.</li>
<li>standard library rica. Diferente de Javascript, não precisa ficar instalando 500 libs externas pra coisas básicas.</li>
<li>como vou usar websockets, channels de GoLang vão facilitar minha vida. O mecanismo assíncrono dele é mais simples, robusto e melhor do que reactor de Node.</li>
<li>o mais importante: <strong>porque eu quero</strong>. É um projeto pessoal, que só eu vou usar, eu escolho o que bem entender e ponto final.</li>
</ul>
<p>Aliás, como eu não sou fluente em Go, provavelmente vou deixar passar alguma má prática ou anti-pattern óbvio. Se isso acontecer, você que é sênior de Go, não deixe de comentar abaixo explicando o que acha.</p>
<blockquote>
  <p>Se quiserem já ver como ficou, o código todo está <a href="https://github.com/akitaonrails/tvclipboard"target="_blank" rel="noopener">neste GitHub</a>. Eu vou ficar linkando trechos dele o tempo todo.</p>

</blockquote>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128093018_screenshot-2026-01-27_19-27-29.png" alt="Crush"  loading="lazy" /></p>
<p>Outro objetivo é testar o quão bom o famoso <a href="https://z.ai/blog/glm-4.7"target="_blank" rel="noopener"><strong>GLM 4.7</strong></a> realmente é. Já escrevi posts e tweets sobre ele antes. Já tentei rodar a versão Flash no meu PC local (é bom, mas BEM lento). No geral, minha impressão é que ele tem qualidade similar a um Claude Opus, GPT Codex, Gemini, mas é BEM mais lento (no mínimo 2x mais lento). Significa que se eu tivesse usado Gemini ou Claude, chegaria no mesmo resultado, mas talvez levasse metade do tempo, porém com um custo maior de tokens (GLM é mais &ldquo;barato&rdquo;).</p>
<blockquote>
  <p>Vamos ao <strong>TL;DR</strong>: ele conseguiu implementar o app como eu queria, mas DEMOROU. Eu levei um dia inteiro, almoçando e jantando na frente do PC enquanto o Crush ficava pensando, um total de mais de <strong>10 HORAS</strong>. E, segundo o OpenRouter, gastei pouco mais de <strong>USD 40</strong> (mais de 200 reais). Não foi nem rápido nem barato, considerando o escopo do app. Lembrando: é mais simples que um To-Do List.</p>

</blockquote>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128160317_screenshot-2026-01-28_16-02-19.png" alt="openrouter billing"  loading="lazy" /></p>
<p>Em termos de tamanho, mesmo um app simples destes deu <strong>mais de 3.000 linhas de código (LOC)</strong> de GoLang:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">===</span> Code <span class="nv">Statistics</span> <span class="o">===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Total Go code: <span class="m">3493</span> lines
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Production code: <span class="m">1431</span> lines
</span></span><span class="line"><span class="cl">Test code: <span class="m">2062</span> lines
</span></span><span class="line"><span class="cl">Test ratio: 1.44:1
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Top <span class="m">10</span> largest files:
</span></span><span class="line"><span class="cl">  <span class="m">3493</span>  total
</span></span><span class="line"><span class="cl">  <span class="m">602</span>  ./pkg/server/server_test.go
</span></span><span class="line"><span class="cl">  <span class="m">560</span>  ./pkg/hub/hub_test.go
</span></span><span class="line"><span class="cl">  <span class="m">385</span>  ./pkg/token/token_test.go
</span></span><span class="line"><span class="cl">  <span class="m">304</span>  ./pkg/hub/hub.go
</span></span><span class="line"><span class="cl">  <span class="m">283</span>  ./pkg/server/server.go
</span></span><span class="line"><span class="cl">  <span class="m">263</span>  ./i18n/i18n.go
</span></span><span class="line"><span class="cl">  <span class="m">262</span>  ./pkg/config/config_test.go
</span></span><span class="line"><span class="cl">  <span class="m">253</span>  ./pkg/QR Code/QR Code_test.go
</span></span><span class="line"><span class="cl">  <span class="m">243</span>  ./pkg/config/config.go</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E de Javascript deu <strong>mais de 1.000 LOC</strong>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">===</span> JavaScript Code <span class="nv">Statistics</span> <span class="o">===</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Total JavaScript code: <span class="m">1360</span> lines
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Production code: <span class="m">787</span> lines
</span></span><span class="line"><span class="cl">Test code: <span class="m">573</span> lines
</span></span><span class="line"><span class="cl">Test ratio: 0.73:1
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">All JavaScript files:
</span></span><span class="line"><span class="cl">  <span class="m">1360</span>  total
</span></span><span class="line"><span class="cl">  <span class="m">573</span>  static/js/test.js
</span></span><span class="line"><span class="cl">  <span class="m">313</span>  static/js/client.js
</span></span><span class="line"><span class="cl">  <span class="m">249</span>  static/js/host.js
</span></span><span class="line"><span class="cl">  <span class="m">114</span>  static/js/common.js
</span></span><span class="line"><span class="cl">  <span class="m">111</span>  static/js/i18n.js</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Ah sim, de curiosidade, adicionei scripts <code>loc.sh</code> e <code>loc-js.sh</code> pra fazer essa contagem.</p>
<h2>Fase 1: Fazendo &ldquo;Funcionar&rdquo; - o que isso significa?<span class="hx:absolute hx:-mt-20" id="fase-1-fazendo-funcionar---o-que-isso-significa"></span>
    <a href="#fase-1-fazendo-funcionar---o-que-isso-significa" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Diferente do que muitos amadores podem pensar, não basta digitar a descrição da aplicação (como eu fiz na seção anterior), como um prompt, jogar na LLM e ele vai automaticamente sair com um app que funciona: <strong>NÃO VAI</strong>.</p>
<p>É o que eu falava 2 anos atrás e continua sendo assim: não importa quanto mais as LLMs evoluírem. Eles podem ficar um pouco mais rápido, um pouco mais eficientes, mas a arquitetura ainda é a mesma desde 2022: transformers são geradores de texto glorificados, com um fator de sorteio aleatório que sempre vai embutir entropia no processo (nunca vai ser exato, determinístico). SEMPRE vai precisar de intervenção humana (e bastante).</p>
<p>Agora vou demonstrar com um exemplo real o que isso significa.</p>
<p>No final, eu parei este projeto depois de <strong>43 commits</strong>. Eu segui o processo que sempre digo que é o melhor: fazer prompts pequenos, uma funcionalidade de cada vez, checar que funciona e está correto, mandar fazer git commit e partir pra próxima funcionalidade ou requerimento.</p>
<blockquote>
  <p>Numa conta de padeiro, cada commit ficou pronto depois de uma meia dúzia de prompts, em média. Vamos dizer que foi então 43 commits x 6 prompts = quase <strong>258 prompts</strong> ao longo de mais de 12 horas. Alguns prompts foram uma frase simples que resolveu em 2 segundos. A maioria levava minutos e múltiplas iterações do agente (Crush).</p>

</blockquote>
<p>A janela de contexto do GLM 4.7 é média: uns 200 mil tokens. O Crush tem um mecanismo que quando chegar muito mais de 90% de uso, ele manda resumir o contexto até agora, pra poder resetar e recarregar desse resumo pra continuar. Mas ele ainda tem bugs e às vezes crasheia e perde o contexto:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128094713_screenshot-2026-01-27_21-45-30.png" alt="crush bug"  loading="lazy" /></p>
<p>Aí eu preciso recuperar o último prompt, às vezes eu mesmo digitar parte do contexto que foi interrompido, pra ele conseguir continuar de onde parou. É trabalhoso e precisa ficar prestando atenção. Esse é um problema que um Gemini tem menos porque a janela de contexto dele é MUITO maior (mas a atenção no meio vai degradando, quanto maior vai ficando, é um trade-off).</p>
<blockquote>
  <p><em>&ldquo;Ah, o Akita só pode ser burro. Eu já fiz um app de Todo e não chegou nem perto de 250 prompts. Ele fez errado.&rdquo;</em></p>

</blockquote>
<p>Agora vamos começar a parte que separamos os meninos dos homens. De fato, pra <em>&ldquo;fazer funcionar&rdquo;</em> não precisa de tudo isso de prompts. Dos 43 commits que levaram 10+ horas, eu já tinha um app que <em>&ldquo;funcionava&rdquo;</em> depois de 10 commits pequenos, que levou cerca de 1 hora.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128165251_screenshot-2026-01-28_16-52-34.png" alt="tvclipboard v0.1"  loading="lazy" /></p>
<p>Se quiserem ver a versão &ldquo;v0.1&rdquo;, basta fazer assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone https://github.com/akitaonrails/tvclipboard.git
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> tvclipboard
</span></span><span class="line"><span class="cl">git fetch --tags
</span></span><span class="line"><span class="cl">git checkout v0.1</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Esta versão tem <strong>1.827 LOC</strong> de GO e <strong>255 LOC</strong> de Javascript. Mas o backend Go mesmo tem só <strong>549 LOC</strong> vs <strong>1.431 LOC</strong> no final. E isso porque nesta versão mínima eu já adicionei testes unitários de backend (1.278 LOC), que a maioria dos iniciantes nunca coloca. Então daria pra &ldquo;resolver&rdquo; meu problema em pouco mais de 800 LOC, menos de 1 hora e meia dúzia de prompts.</p>
<p>A estrutura do projeto ficou assim. Guardem esta imagem pra comparar como vai ficar no final:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128100103_screenshot-2026-01-28_09-53-39.png" alt="v0.1 tree"  loading="lazy" /></p>
<p>Nesta versão já abre o que eu chamo de página &ldquo;host&rdquo; que é a página que abriria na minha TV, mostrando o QR Code pra conectar. Já tem a página &ldquo;client&rdquo;, que é o que o QR Code vai abrir no meu celular. E já consegue colar texto no cliente, mandar pro host e copiar no clipboard dele. Bem simples.</p>
<blockquote>
  <p>É aqui que 99% das pessoas iriam parar e dar por completo.</p>

</blockquote>
<h2>Fase 2: Indo além de &ldquo;só funcionar&rdquo;<span class="hx:absolute hx:-mt-20" id="fase-2-indo-além-de-só-funcionar"></span>
    <a href="#fase-2-indo-al%c3%a9m-de-s%c3%b3-funcionar" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Vou repetir: este projeto é mais simples que um todo list e é feito exclusivamente pra somente eu usar, em casa, na minha rede local. Qualquer um que pensar <em>&ldquo;ah, isso tudo é desnecessário pra esse app&rdquo;</em> - <strong>EU SEI!</strong>.</p>
<p>A ideia é demonstrar o mínimo que se precisa pensar num <strong>projeto de verdade</strong>. Se estiver fazendo um brinquedo só pra você sozinho, tanto faz a qualidade.</p>
<p>Pra quem for sênior de Go, ou Javascript (porque vou usar no front), eu sei que vocês vão pensar <em>&ldquo;ah, ele deveria ter usado framework X ou biblioteca Y&rdquo;</em> - <strong>EU SEI!!</strong> 😅 No final vai ter uma seção onde vou discutir um pouco dessas outras opções, esperem antes de sair comentando!!</p>
<p>Voltando ao conceito de Pareto. Eu falei que 20% do trabalho mais importante custa 80% do tempo, mas isso é só uma ordem de grandeza. No meu exemplo, o projeto todo levou mais de 10 horas e &ldquo;pra funcionar&rdquo; levou só 1 hora. Então está mais pra 90/10 em vez de 80/20.</p>
<blockquote>
  <p>O que diabos custou 90% do tempo se em 10% &ldquo;já funcionava&rdquo;??</p>

</blockquote>
<h3>1. Gerenciamento de Tokens<span class="hx:absolute hx:-mt-20" id="1-gerenciamento-de-tokens"></span>
    <a href="#1-gerenciamento-de-tokens" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A primeira coisa que eu penso quando vejo um QR Code é que o link dentro dela seja descartável e volátil. Não é uma regra e obviamente depende do app. Pro meu appzinho é sim &ldquo;overkill&rdquo; mas vou fazer pra dar de exemplo. (não vou ficar repetindo toda hora <em>&ldquo;sim, pro meu app não precisava&rdquo;</em>, esta é a última vez).</p>
<p>Eu não quero que uma visita que use meu app em casa consiga bookmarkar o link e conseguir usar fora daqui (eu vou expor meu serviço na internet pra conseguir usar de qualquer lugar depois). Eu quero que cada uso seja único e que expire depois de alguns minutos, exigindo um novo QR Code.</p>
<blockquote>
  <p>Portanto eu preciso de algum sistema de tokens que expira.</p>

</blockquote>
<p>Este artigo já vai ser muito longo, então não vou ficar copiando o código inteiro de cada funcionalidade. Vou sempre indicar o <a href="https://github.com/akitaonrails/tvclipboard/commit/05dd60290e1d2419855e2f79ed48326415641da8"target="_blank" rel="noopener">commit</a> onde implementei pra vocês pesquisarem depois e mostrar só trechos pra ilustrar.</p>
<p>Enfim, o objetivo vai ser ter uma URL como <code>http://clip.example.com/?token=blablabla</code>. Nada de outro mundo.</p>
<p>Sim, poderia ser um JWT, mas meu uso é muito simples então mandei fazer do zero e o GLM 4.7 inventou uma estrutura JSON com uma chave privada aleatória e um timestamp, que ele resolveu encriptar com AES (não precisava mesmo) e encodar em hexadecimal pra ser o token (fica hiper longo, mais de 100 caracteres).</p>
<p>Ficou um monstrinho assim:</p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/05dd60290e1d2419855e2f79ed48326415641da8"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128101314_screenshot-2026-01-28_10-13-04.png" alt="encrypt token"  loading="lazy" /></a></p>
<p>Eu não gostei mas resolvi deixar assim porque está funcionando. Eu posso mexer nisso mais tarde - inclusive, muita coisa foi feita &ldquo;fora de ordem&rdquo; porque eu fui fazendo à medida que ia lembrando. Não tinha um planejamento passo-a-passo escrito antes. Literalmente tirei da cabeça em tempo real mesmo.</p>
<blockquote>
  <p>O objetivo disso é lembrar que Gerenciamento de Sessão é uma preocupação que todo dev tem que ter na cabeça num sistema de verdade.</p>

</blockquote>
<h3>2. Primeiro Refatoramento<span class="hx:absolute hx:-mt-20" id="2-primeiro-refatoramento"></span>
    <a href="#2-primeiro-refatoramento" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O código principal já estava com mais de 500 LOC, os testes com mais de 1000 LOC. Isso é muito grande pra ficar num arquivo só. Então pedi pro GLM refatorar em pacotes. Esse tipo de coisa eu sei que o Claude Code faz com mais facilidade, mas o GLM se saiu bem também. No final ficou algo assim:</p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/dbbee593e99fbc6d8051884b016386289d17816b"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128101636_screenshot-2026-01-28_10-16-29.png" alt="separation of concerns"  loading="lazy" /></a>
Isso é o que chamamos de <strong>&ldquo;Separation of Concerns&rdquo;</strong>. Cada pacote tem um domínio isolado. Um pacote pra lidar só com opções de configuração, outro pra cuidar só do servidor HTTP, outro pra lidar só com a geração de QR Code e outro pra lidar só com gerenciamento de Tokens. Cada um com testes unitários no mesmo pacote.</p>
<p>Pedir pra fazer testes desde o começo foi uma grande ajuda à LLM, porque vira e mexe ele rodava o teste, ficava confuso do porquê não funcionava e caia a ficha que precisava ajustar a implementação. Todo feedback automatizado é importante pra LLM ter contexto pra trabalhar.</p>
<p>E refatoramento não é algo que se faça apenas uma vez. É um processo que tem que ser feito de tempos em tempos pra consolidar e simplificar o código, melhorando a <strong>mantenabilidade</strong>.</p>
<blockquote>
  <p>O objetivo disso é lembrar que &ldquo;só porque funciona&rdquo;, não significa que o código fica congelado e não se mexe mais.</p>

</blockquote>
<h3>3. Suporte a Docker<span class="hx:absolute hx:-mt-20" id="3-suporte-a-docker"></span>
    <a href="#3-suporte-a-docker" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Como eu iria rodar no meu home server, suporte a Docker é obrigatório. Mas independente disso, é uma boa prática manter um <a href="https://github.com/akitaonrails/tvclipboard/blob/master/Dockerfile"target="_blank" rel="noopener"><code>Dockerfile</code></a> que funciona. É outro tipo de &ldquo;teste automatizado&rdquo;: ver se consegue &ldquo;buildar&rdquo; uma imagem e rodar. E documentar pra outro usuário que ele não precisa instalar nada no sistema dele pra rodar, basta rodar o Docker:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run -d <span class="se">\
</span></span></span><span class="line"><span class="cl">  --name tvclipboard <span class="se">\
</span></span></span><span class="line"><span class="cl">  -p 3333:3333 <span class="se">\
</span></span></span><span class="line"><span class="cl">  akitaonrails/tvclipboard:latest</span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Inclusive, o comando acima funciona, caso você queira testar imediatamente. Eu subi a imagem na minha conta no Docker Hub.</p>
<p>E se já não tinha implementado assim, isso também força que você adicione opções de configuração na forma de variáveis de ambiente (como <code>TVCLIPBOARD_SESSION_TIMEOUT</code>) ou via argumentos em linha de comando (como <code>--expires</code>), expondo o que é configurável.</p>
<blockquote>
  <p>O objetivo aqui é lembrar como é importante automatizar build e documentar como usar pra outras pessoas.</p>

</blockquote>
<h3>4. Organizar Javascript e adicionando Cache Busting<span class="hx:absolute hx:-mt-20" id="4-organizar-javascript-e-adicionando-cache-busting"></span>
    <a href="#4-organizar-javascript-e-adicionando-cache-busting" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Pra &ldquo;só funcionar&rdquo;, lá no começo, bastava fazer um &ldquo;index.html&rdquo; e ir escrevendo <code>if host</code> ou <code>if client</code> no meio do HTML e JS. Mas pelo menos o GLM iniciou duplicando um <code>host.html</code> e um <code>client.html</code>. Mas o javascript estava todo inline entre tags <code>&lt;script&gt;...&lt;/script&gt;</code> e duplicado nos dois arquivos, então outro passo de refatoração foi pedir pra ele separar <code>common.js</code>, <code>client.js</code>, <code>host.js</code>.</p>
<p>É o mínimo do mínimo de mantenabilidade. Novamente: refatoração não acaba num passo só.</p>
<p>Mas eu poderia esbarrar num problema. Sendo um sisteminha muito simples, não tenho nada de <strong>ETAGs</strong> implementado e nem asset pipeline como WebPack ou similares.</p>
<p>Num web framework dinâmico, a maioria vai ter suporte a gerar arquivos de CSS e JS dinamicamente mudando os nomes pra coisas como <code>/assets/host-20260128123456.js</code> e toda vez que editarmos algum JS, o pipeline vai regenerar esse arquivo com outro nome.</p>
<p>Quando abrimos num navegador web, ele vai sempre tentar &ldquo;cachear&rdquo; assets estáticos como imagens, JS, CSS, pra não ficar pedindo a mesma coisa toda hora. Se ele já baixou <code>host.js</code>, no próximo &ldquo;reload&rdquo;, se pedir o mesmo arquivo, basta pegar do cache em vez de pedir pro servidor de novo.</p>
<p>O problema disso é se mudarmos o conteúdo do arquivo e manter o mesmo nome. O navegador vai achar que é a mesma coisa e você vai ficar horas coçando a cabeça pensando <em>&ldquo;porra, eu já consertei isso, porque não aparece??&rdquo;</em> - e pode ser cache.</p>
<p>Pra evitar isso, existem técnicas de <strong>Cache Busting</strong>, como cabeçalhos ETAG. Mas o mais comum é só adicionar algum tipo de indicador de timestamp na URL, por exemplo <code>/static/js/host.js?v=20260128120431</code>. Desta forma, sempre que meu servidor reiniciar, o timestamp vai mudar (não é a melhor regra, mas pra agora funciona). A URL vai ser diferente e o navegador vai ser forçado a baixar a versão mais nova, garantindo que eu não perca tempo caçando problemas de cache.</p>
<p>Novamente, qualquer web framework minimamente competente, com suporte a templates de HTML, vai ter isso, mas eu pedi pra fazer do zero:</p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/4f98f8364bbe8b4832270c5817e0ee85a4dbc547"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128103739_screenshot-2026-01-28_10-37-00.png" alt="cache busting"  loading="lazy" /></a>
Como eu sirvo páginas estáticas de HTML, o código do meu servidor precisa modificar as URLs antes de mandar pro navegador, e é isso que o commit acima faz.</p>
<p>Ao escrever este post, notei que ele adicionou cache busting pros arquivos &ldquo;.js&rdquo; mas esqueceu do &ldquo;style.css&rdquo;, que ficou sem. Eu não notei porque não mexi quase nada no CSS original que ele fez. Mas novamente, pra demonstrar que LLMs nunca são perfeitas e tem que ficar constantemente checando se ele não esqueceu nada ou deixou sobrar coisas. Mandei ele consertar <a href="https://github.com/akitaonrails/tvclipboard/commit/2338999c505f1466c4913f41c8c7cbea72b369c9"target="_blank" rel="noopener">neste último commit</a></p>
<blockquote>
  <p>O objetivo aqui é lembrar que não basta só jogar arquivos no servidor sem saber como eles são servidos. Todo front-end precisa saber como um navegador funciona. Não basta &ldquo;saber HTML&rdquo;.</p>

</blockquote>
<h3>5. Adicionando Argumentos de Linha de Comando e fazendo mais limpeza<span class="hx:absolute hx:-mt-20" id="5-adicionando-argumentos-de-linha-de-comando-e-fazendo-mais-limpeza"></span>
    <a href="#5-adicionando-argumentos-de-linha-de-comando-e-fazendo-mais-limpeza" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Os <a href="https://github.com/akitaonrails/tvclipboard/commit/278604425edf52ae682a0e11e2952780f2783fd7"target="_blank" rel="noopener">próximos</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/59de9fb135c4b9e0f38343f7296527f0b1c5a31c"target="_blank" rel="noopener">três</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/ce0c25f37284f923568417c2543c66923fe449ec"target="_blank" rel="noopener">commits</a> são besteiras que eu deveria ter feito squash num único commit.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128104202_screenshot-2026-01-28_10-40-53.png" alt="commits limpeza"  loading="lazy" /></p>
<p>Aqui é mais pra lembrar que não é inteiramente ruim deixar um agente como Crush fazer git commits automaticamente, mas precisa prestar atenção pra talvez ter que mandar desfazer (git tem como refazer commits, com opções como <code>-amend</code>).</p>
<p>Eu só adicionei opções de linha de comando. Agora o binário tem este suporte:</p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/278604425edf52ae682a0e11e2952780f2783fd7"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128104735_screenshot-2026-01-28_10-47-27.png" alt="cli args"  loading="lazy" /></a></p>
<p>E como ele esqueceu de documentar no README, o outro commit é pra isso. Por isso falei que o certo era dar squash num commit só, porque são a mesma tarefa.</p>
<h3>6. Configuração de ambiente de Dev vs Produção<span class="hx:absolute hx:-mt-20" id="6-configuração-de-ambiente-de-dev-vs-produção"></span>
    <a href="#6-configura%c3%a7%c3%a3o-de-ambiente-de-dev-vs-produ%c3%a7%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Este é outro ponto que todo iniciante ignora. O famoso <em>&ldquo;ah, mas na minha máquina funciona&rdquo;</em>.</p>
<p>Em web, principalmente, a &ldquo;sua máquina&rdquo;, sempre vai ser um <code>localhost</code>, ou <code>127.0.0.1</code> ou <code>0.0.0.0</code> (assista à minha <a href="https://www.youtube.com/watch?v=0TndL-Nh6Ok&amp;list=PLdsnXVqbHDUcTGjNZuRYCVj3AZtdt6oG7"target="_blank" rel="noopener">playlist sobre Redes</a>) pra entender a diferença.</p>
<p>O problema: se subir esta app localmente no meu PC, eu acesso por <code>http://localhost:3333</code>. Quando o QR Code for gerado, ele vai usar &ldquo;localhost&rdquo;. Agora imagine seu celular tentando carregar um link de &ldquo;localhost&rdquo;. <strong>Obviamente, não vai funcionar.</strong> Porque no seu celular não tem o aplicativo rodando nessa porta em localhost. Eu preciso do IP do meu PC. Então, a primeira coisa que preciso pedir pra corrigir é que, caso seja localhost, que ele procure o IP do meu PC - que vai ser algo como <code>192.168.0.xxx</code>. Estando meu PC e meu celular no mesmo DHCP, na mesma rede, eles conseguem se achar por esse IP privado.</p>
<p>Mas e quando eu subir num Docker? O IP interno de um Docker é diferente do IP externo (o container sempre está ou em NAT ou em Bridge). O IP &ldquo;externo&rdquo; do container vai ser algo como <code>192.168.xxx</code>, mas o IP interno vai ser algo como <code>172.xxx</code>. Novamente, se o QR Code usar o IP interno, vai dar problema.</p>
<p>Então eu preciso de uma nova configuração, que normalmente se chama <strong>PUBLIC URL</strong> ou <strong>PUBLIC HOST</strong> ou só <strong>HOST</strong>. Onde eu <strong>declaro explicitamente</strong>.</p>
<p>Por exemplo, quando terminar, como falei, vou deixar meu serviço exposto na internet usando um domínio válido pessoal. Algo como <code>https://clip.example.com</code> com suporte a TLS e tudo mais. Então eu preciso que no <code>docker-compose.yml</code> eu consiga ter uma opção assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">TVCLIPBOARD_PUBLIC_URL = https://clip.example.com</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>E assim o QR Code é gerado com este nome em vez de ter que tentar adivinhar. Mas ao mesmo tempo, se não passar nada, assume &ldquo;localhost&rdquo;.</p>
<p>Neste <a href="https://github.com/akitaonrails/tvclipboard/commit/b129e728101fd9fca400d48c0082b2f3dd679cf3"target="_blank" rel="noopener">primeiro commit</a> ele ainda não faz tudo isso - eu fui fazendo aos poucos. Mas é como se começa esse tipo de funcionalidade.</p>
<blockquote>
  <p>O objetivo aqui é lembrar que &ldquo;rodar na minha máquina&rdquo; (localhost) não significa nada. Não vamos colocar &ldquo;sua máquina&rdquo; em produção. &ldquo;Funcionar&rdquo; significa &ldquo;funcionar FORA da sua máquina&rdquo;.</p>

</blockquote>
<h3>7. Mais limpeza de código<span class="hx:absolute hx:-mt-20" id="7-mais-limpeza-de-código"></span>
    <a href="#7-mais-limpeza-de-c%c3%b3digo" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Estou de propósito mencionando &ldquo;limpeza de código&rdquo; o tempo todo pra que fique bem claro que uma parte considerável do trabalho de programação é &ldquo;limpar&rdquo; código que já foi feito.</p>
<p>Os <a href="https://github.com/akitaonrails/tvclipboard/commit/c9505acd840bbcbead456e3823ecd4a8d6e022a6"target="_blank" rel="noopener">próximos</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/67c5a01f4791257c8f4b19388c9c39bf1ccabb33"target="_blank" rel="noopener">quatro</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/899fef6232c5b8f7081c0ad8681014a681de78ce"target="_blank" rel="noopener">commits</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/63d7fbfaca798dec1eca48ff214561e01b22e670"target="_blank" rel="noopener">são</a> só manutenção.</p>
<p>Por exemplo, sem querer a LLM largou um trecho duplicado dentro do código. Não quebrou nada porque foi na configuração de Docker Compose:</p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/67c5a01f4791257c8f4b19388c9c39bf1ccabb33"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128151806_screenshot-2026-01-28_10-59-40.png" alt="duplicate"  loading="lazy" /></a>
Além disso, tem um comportamento <strong>acidental</strong> na forma como este app funciona que resolvi deixar como &ldquo;feature&rdquo;. Sempre precisa ter um navegador que abre primeiro que automaticamente se torna o Host e exibe um QR Code. O próximo navegador a abrir precisa do link desse QR Code e vira o &ldquo;client&rdquo;.</p>
<p>Mas quando o host fecha, o client perde a conexão e automaticamente é <strong>promovido</strong> a host. Eu não pedi pra ser assim mas acabou ficando, então <a href="https://github.com/akitaonrails/tvclipboard/commit/63d7fbfaca798dec1eca48ff214561e01b22e670"target="_blank" rel="noopener">pedi pra deixar e documentar</a>.</p>
<blockquote>
  <p>O objetivo aqui é lembrar que LLMs não são perfeitas, vão deixar sobras pra trás, e você precisa ficar o tempo todo monitorando.</p>

</blockquote>
<h3>8. Otimizando Javascript<span class="hx:absolute hx:-mt-20" id="8-otimizando-javascript"></span>
    <a href="#8-otimizando-javascript" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Os <a href="https://github.com/akitaonrails/tvclipboard/commit/8db01bb053f8e045708b60293f569655a4cf2311"target="_blank" rel="noopener">próximos</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/8f7080cfce86257d5fc0e594f8098c115858a963"target="_blank" rel="noopener">três</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/97df6c5b125de522c4ca65d276df353a97670ae6"target="_blank" rel="noopener">commits</a> são pra dar uma limpeza no Javascript.</p>
<p>Da última vez que mexemos, eu só tinha pedido pra ele tirar os Javascript inline do HTML e separar em arquivos &ldquo;.js&rdquo; reusáveis. Mas chegou a hora de dar uma olhada neles.</p>
<p>De cara, deu pra ver que tinha duplicação entre client e host, então pedi <a href="https://github.com/akitaonrails/tvclipboard/commit/8db01bb053f8e045708b60293f569655a4cf2311"target="_blank" rel="noopener">pra deduplicar</a> e jogar no &ldquo;common.js&rdquo;.</p>
<p>Além disso tinha vários pequenos probleminhas:</p>
<ul>
<li>um monte de variáveis e funções no escopo global (poluição de global: má prática)</li>
<li>websockets que abrem mas sem nada checando o fechamento</li>
<li>timers sendo ativados mas nunca desativados (leaks)</li>
</ul>
<p>O código Javascript &ldquo;que funciona&rdquo; sempre é assim: tudo global, tudo aberto, nada checado, vários leaks. Isso é normal e toda LLM vai fazer assim.</p>
<p>Também não acho que precisa fazer prompts desde o começo exigindo SOLID e tudo mais. Primeiro faz funcionar, depois faz funcionar direito, finalmente faz funcionar rápido. É sempre nessa ordem, especialmente num escopo simples com este.</p>
<p>Por isso agora é hora de pedir pra refatorar. E a primeira coisa a fazer é sempre isolar o escopo, por exemplo, num bloco assim:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s1">&#39;use strict&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">...</span>
</span></span><span class="line"><span class="cl"><span class="p">})();</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>O problema disso é que toda função que o HTML pedia, como &ldquo;onclick&rdquo;, vai falhar porque agora não tem mais nada no escopo global. Uma vez tudo escondido, agora tem que <a href="https://github.com/akitaonrails/tvclipboard/commit/97df6c5b125de522c4ca65d276df353a97670ae7"target="_blank" rel="noopener">ir expondo</a> somente o que realmente precisa público.</p>
<p>Isso se chama <strong>IIFE Encapsulation</strong> (<em>Immediately Invoked Function Expression</em>). Tem milhares de práticas como essa que Javascript precisa pra ficar &ldquo;mais limpo&rdquo;. Eu sei, eu deveria estar usando módulos, Typescript, bla bla, mas esse é um exemplo simples mesmo.</p>
<blockquote>
  <p>O objetivo aqui é lembrar que, só porque &ldquo;funciona&rdquo;, não significa que &ldquo;está bom&rdquo;, principalmente com Javascript.</p>

</blockquote>
<h3>9. Otimizando o Golang<span class="hx:absolute hx:-mt-20" id="9-otimizando-o-golang"></span>
    <a href="#9-otimizando-o-golang" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esta é a seção mais longa. O código Golang já &ldquo;funciona&rdquo;, mas ele não está bom ainda. Eu sei que tem race conditions em channels, eu sei que o tratamento de erros está ruim, eu sei que ainda tem micro problemas de segurança, então quero consertar o que puder.</p>
<p>Os próximos 6 commits lidam com:</p>
<ul>
<li><a href="https://github.com/akitaonrails/tvclipboard/commit/8d28a12569bcf49fc5f4816b5c964299ff0dbc71"target="_blank" rel="noopener">Refactor: Improve error handling, race conditions, and graceful shutdown</a></li>
<li><a href="https://github.com/akitaonrails/tvclipboard/commit/5bd7e0a8f1c14a9a3c59cbddd1437cfd193f9b6b"target="_blank" rel="noopener">Fix: Critical race conditions, memory leaks, and security issues</a></li>
<li><a href="https://github.com/akitaonrails/tvclipboard/commit/480cc36f93f30369044dfe291477329174566e38"target="_blank" rel="noopener">Security: Add message size validation and rate limiting to prevent abuse</a></li>
<li><a href="https://github.com/akitaonrails/tvclipboard/commit/0c456a203903167182722138c2c11ee63b09ddf3"target="_blank" rel="noopener">Config: Tighten security limits and add origin validation</a></li>
<li><a href="https://github.com/akitaonrails/tvclipboard/commit/5678b0a1768db27bb802a56c13c1498ab4f82040"target="_blank" rel="noopener">Fix: CORS origins, Web Crypto API fallback, and clipboard issues</a></li>
<li><a href="https://github.com/akitaonrails/tvclipboard/commit/5dd813d9ca58324a12b8e6ebf8fcaf8c7be99e9b"target="_blank" rel="noopener">Fix: QR code uses correct port from PublicURL and document Web Crypto API HTTPS requirement</a></li>
</ul>
<p><a href="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128164221_screenshot-2026-01-28_16-41-43.png"target="_blank" rel="noopener">6 commits</a></p>
<p>O próprio GoLang tem ferramenta pra checar <strong>race conditions</strong>, o <a href="https://go.dev/doc/articles/race_detector"target="_blank" rel="noopener"><strong>Data Race Detector</strong></a>. Basta colocar a opção <code>--race</code> em comandos como <code>go build</code> ou <code>go test</code>. E felizmente o GLM foi esperto e imediatamente usou essa ferramenta quando pedi pra checar esses problemas.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128112743_screenshot-2026-01-28_11-24-16.png" alt="race condition"  loading="lazy" /></p>
<p><strong>Race conditions</strong> acontecem quando duas tarefas assíncronas tentam modificar a mesma estrutura de dados, por exemplo. Uma das formas de &ldquo;consertar&rdquo; é colocando Locks ou Mutexes em estruturas que só podem ser modificadas sincronamente. Qualquer aplicação que tenha qualquer coisa assíncrona, eventualmente vai ter problemas de condição de corrida. Não é &ldquo;talvez&rdquo;, é &ldquo;quando&rdquo;.</p>
<p>Um servidor precisa ser &ldquo;bem comportado&rdquo;, não pode simplesmente &ldquo;crashear&rdquo;, dar &ldquo;panic&rdquo;. Mas o primeiro código &ldquo;que funciona&rdquo; que o GLM fez tinha vários trechos assim:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128112136_screenshot-2026-01-28_11-21-28.png" alt="panic"  loading="lazy" /></p>
<p>Isso está errado. Além disso precisamos que o servidor aceite corretamente sinais de sistema como SIGINT, SIGTERM, etc. É quando damos &ldquo;Ctrl-C&rdquo;, por exemplo, ou o Docker manda desligar o container. Ele não vai simplesmente crashear o programa, primeiro ele vai pedir pro programa se desligar sozinho, dando chance dele fazer limpeza, como dar flush em arquivos, fechar portas e coisas assim.</p>
<p>Isso se chama <strong>Graceful Shutdown</strong>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128112344_screenshot-2026-01-28_11-23-37.png" alt="graceful shutdown"  loading="lazy" /></p>
<p>Não parei pra checar o quão certo o GLM estava mas segundo ele mesmo, alguns dos problemas que ele mesmo encontrou, no código que ele mesmo fez, incluem coisas assim:</p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/5bd7e0a8f1c14a9a3c59cbddd1437cfd193f9b6b"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128113944_screenshot-2026-01-28_11-39-23.png" alt="fixing errors 1"  loading="lazy" /></a></p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/5bd7e0a8f1c14a9a3c59cbddd1437cfd193f9b6b"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128114028_screenshot-2026-01-28_11-39-38.png" alt="fixing errors 2"  loading="lazy" /></a></p>
<p>Como podem ver, só porque &ldquo;compila&rdquo;, não quer dizer que está ok. Só porque &ldquo;roda&rdquo;, não quer dizer que está ok. Mesmo pedindo pra checar <strong>MÚLTIPLAS VEZES</strong> a LLM não acha todos os erros de uma só vez. Cada um desses commits levou horas pra conseguir resolver e eu tive que intervir múltiplas vezes, e ainda assim eu ainda não tenho certeza que ele pegou tudo (precisaria revisar com mais calma - mas eu queria publicar este artigo logo 😂).</p>
<h3>10. Adicionando Sistemas de Segurança<span class="hx:absolute hx:-mt-20" id="10-adicionando-sistemas-de-segurança"></span>
    <a href="#10-adicionando-sistemas-de-seguran%c3%a7a" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Novamente, nada disso é necessário num sistema que somente eu vou usar na minha rede local, mas você como desenvolvedor precisa se responsabilizar.</p>
<blockquote>
  <p>SEMPRE considere que todo usuário do seu sistema é MALICIOSO! Essa é a REGRA, não a exceção.</p>

</blockquote>
<p>Um exemplo bobo: o usuário pode colar e transmitir qualquer texto via websockets pro meu servidor. As 3 coisas mais óbvias que imediatamente passaram na minha cabeça foram:</p>
<p><strong>a) Quando o host recebe o texto, ele renderiza no HTML na caixa de &ldquo;Recebido&rdquo;</strong></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128090907_20260127_195954.jpg" alt="tvclipboard host received screen"  loading="lazy" />
E se o usuário mandar um <code>&lt;script&gt;....&lt;/script&gt;</code>? Isso vai renderizar no HTML e o navegador vai EXECUTAR. É assim que funciona <strong>Code Injection</strong>. É assim que funciona <strong>XSS</strong> (<em>Cross-Site Scripting</em>). Isso era muito comum em antigos fóruns ou sistemas de comentários. Pra evitar isso se deve <strong>SANITIZAR</strong> tudo que vem do usuário.</p>
<p>Felizmente, o jeito que o GLM fez já é sanitizado porque ele escreve o conteúdo com <code>textContent</code> em vez de <code>innerHTML</code>, então o texto é escrito literalmente sem executar:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128140317_screenshot-2026-01-28_14-03-09.png" alt="textContent"  loading="lazy" />
Por acaso ele fez certo de primeira, mas precisa checar porque ele poderia ter feito errado.</p>
<p><strong>b) Não tinha checagem de tamanho da mensagem</strong></p>
<p>Nunca um usuário pode conseguir enviar um texto de tamanho qualquer. Ele sempre pode tentar diretamente dar POST com um arquivo de gigabyte pra ver se estoura algum erro de <strong>Overflow</strong> no servidor, expondo algum exploit de segurança.</p>
<p>TODO campo de usuário precisa de limites e precisa checar não só no cliente/front-end, mas mais importante, no backend:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128140558_screenshot-2026-01-28_14-05-51.png" alt="message max size"  loading="lazy" />
<strong>c) Tem que rejeitar Spammers!!</strong></p>
<p>Novamente, não pode assumir que todo mundo vai usar seu web app bonitinho. A regra é achar que todo mundo vai tentar derrubar ou invadir. E uma forma de fazer isso é criar um script pra ficar fazendo milhares de POST por segundo pra tentar algum tipo de <strong>Denial of Service</strong>, onde o servidor enfileira tanta requisição que vai estourar.</p>
<p>O certo é configurar isso num outro serviço que vai ficar na frente do seu, como <a href="https://www.cloudflare.com/ddos/"target="_blank" rel="noopener"><strong>Cloudflare</strong></a>.</p>
<p>Mas não custa implementar um <em>fallback</em> na sua aplicação mesmo. Um jeito de configurar quantas requests um mesmo usuário pode fazer de cada vez (ex. 4 requisições por segundo - ninguém vai colar texto tantas vezes por segundo numa app dessas).</p>
<p>Se tentar mais que isso, o app tem que imediatamente <strong>REJEITAR</strong> e dropar a conexão, sem gastar tempo processando mais, liberando recursos pra outras requisições de verdade. Isso se chama <strong>RATE LIMITING</strong>.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128141010_screenshot-2026-01-28_14-10-02.png" alt="rate limiting"  loading="lazy" />
<strong>d) Outro problema que iniciantes não param pra pensar é em CORS e Origin Check.</strong></p>
<p>Como falei antes, temos Javascripts como <code>/static/js/common.js</code>. Não é nenhum grande problema outra pessoa usar porque não tem nenhum grande segredo neles. Mas no mínimo eu não quero que algum site &ldquo;<a href="http://malware.pk/index.html"target="_blank" rel="noopener">http://malware.pk/index.html</a>&rdquo; da vida inclua dentro dele um <code>https://clip.example.com/static/js/common.js</code> e eu fique pagando pelo tráfego dele.</p>
<p>Eu quero que somente a <strong>MESMA ORIGEM</strong>, no caso &ldquo;clip.example.com&rdquo;, seja capaz de carregar arquivos no mesmo servidor. Ninguém de fora é bem-vindo e isso é a <strong>BOA PRÁTICA</strong>. Estude <strong>CORS</strong> (Cross-Origin Resource Sharing). Isso é mais importante se seu sistema são múltiplos domínios onde um depende do outro, daí você quer - controladamente - permitir somente determinados outros domínios que estão sob seu controle.</p>
<blockquote>
  <p>Regra de tudo em rede é <strong>DENY ALL</strong> e depois <strong>ALLOW [específico]</strong>. NUNCA deve ser <strong>ALLOW ALL</strong>.</p>

</blockquote>
<p>Pra implementar isso demorou um bocado, porque o GLM ficou se enrolando de como permitir &ldquo;localhost&rdquo;, mas também &ldquo;192.168.xxx:3333&rdquo;, mas também &ldquo;clip.example.com&rdquo; (via porta 443), etc. Não é difícil, só o GLM que se complicou todo e eu tive que intervir várias vezes com prompts pra colocar ele na direção certa. Acabou dando múltiplos commits até ele acertar.</p>
<p><strong>e) Outro probleminha que iniciante não pensa: PERMISSÕES de recursos do navegador.</strong></p>
<p>Toda vez que carregar esse sistema em &ldquo;localhost&rdquo; e ele receber a mensagem pra jogar no clipboard, vai aparecer este alerta pedindo permissão:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128114848_screenshot-2026-01-28_11-48-38.png" alt="clipboard permission"  loading="lazy" /></p>
<p>E isso é normal e correto: nada deveria ter acesso a coisas fora do navegador. Por isso o Javascript pede <em>&ldquo;manda pro clipboard&rdquo;</em>, daí o navegador intercepta, checa a permissão e, se não tem ele pede. Toda vez vai pedir. Mas se usar um domínio de verdade (ex. clip.example.com), ele vai perguntar uma vez e depois lembrar pra cada domínio. É um dos motivos pelos quais vou expor na internet: pra ter um domínio de verdade.</p>
<blockquote>
  <p>Expor serviços na internet é PERIGOSO. Por isso eu fiz <a href="https://akitaonrails.com/2025/09/09/acessando-meu-home-server-com-dominio-de-verdade/"target="_blank" rel="noopener">este artigo explicando como usar domínios de verdade</a> e <a href="https://akitaonrails.com/2025/09/10/protegendo-seu-home-server-com-cloudflare-zero-trust/"target="_blank" rel="noopener">este outro artigo sobre como usar Cloudflare Zero Trust</a>.</p>

</blockquote>
<p><strong>f) Tinha outro fator de segurança que é &ldquo;overkill&rdquo; mas eu quis implementar mesmo assim.</strong></p>
<p>Digamos que eu queira rodar somente em localhost. Digamos que seja uma rede privada, mas compartilhada (visitas, companheiro de quarto, escritório, etc). Ao rodar em localhost não vai ter TLS, ou seja, os pacotes HTTP vão trafegar em texto aberto.</p>
<p>Qualquer um com um <a href="https://www.wireshark.org/"target="_blank" rel="noopener"><strong>Wireshark</strong></a> pode ficar &ldquo;escutando&rdquo; (&ldquo;sniffing&rdquo;) os pacotes na rede e interceptar os meus. E se justo eu estiver passando dados sensíveis, como uma senha, vai dar pra ver aberto.</p>
<p>O certo é rodar com TLS (HTTPS). Mas só pra um &ldquo;quebra-galho&rdquo;, pedi pro GLM implementar uma obfuscação simples (não é um processo totalmente seguro, mas usa AES-GCM), mas já serve pra não ficar óbvio com um sniffer. Pra isso ele vai usar <a href="https://w3c.github.io/webcrypto/"target="_blank" rel="noopener"><strong>Web Crypto API</strong></a>.</p>
<p>Mas tem um porém: Web Crypto API está disponível somente se o site abrir com HTTPS ou se for localhost. Mas se eu tentar <code>http://192.168.xxx:3333</code>, não vai estar disponível. Pra isso tem <a href="https://github.com/akitaonrails/tvclipboard/commit/5dd813d9ca58324a12b8e6ebf8fcaf8c7be99e9b"target="_blank" rel="noopener">este commit</a> pra lidar com isso e documentar.</p>
<blockquote>
  <p>O objetivo aqui é mostrar como aspectos de segurança, otimização, mantenabilidade são muito mais do que um iniciante pensa. Aliás, nunca vai pensar, porque precisa ter estudado antes pra saber que existe.</p>

</blockquote>
<h3>11. Licenças Open Source<span class="hx:absolute hx:-mt-20" id="11-licenças-open-source"></span>
    <a href="#11-licen%c3%a7as-open-source" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Esse tema sozinho seria assunto pra um longo artigo, mas na prática eu tendo a pensar simples:</p>
<ul>
<li>se for um projeto que eu não me importo se alguém vai contribuir ou não, basta abrir com licenças PERMISSIVAS, como BSD, MIT</li>
<li>se for um projeto que eu gostaria de ter contribuição e que ninguém use comercialmente, basta colocar uma licença RESTRITIVA, como GPL</li>
<li>se for restritivo e for um app que poderia virar um SaaS (daí o código não é distribuído, então não precisaria contribuir), daí adiciona AGPL (Affero)</li>
</ul>
<p>Foi o que eu fiz neste: coloquei licença <a href="https://github.com/akitaonrails/tvclipboard/blob/master/LICENSE"target="_blank" rel="noopener">AGPL 3.0</a>.</p>
<p>O aviso é pra não deixar LLMs escreverem o arquivo de licença. Sempre pegue textos sobre leis, regulamentos e regras do site oficial. A LLM sempre produz uma versão resumida ou incompleta ou até errada mesmo. Documentos legais precisam ser EXATOS, não pode ter palavras erradas e sentidos ambíguos ou afirmações incompletas.</p>
<blockquote>
  <p>LLMs não são boas pra gerar textos legais corretos de primeira, sempre vai ter erros. Sempre assuma que textos assim estão errados: não existe &ldquo;compilador&rdquo; de leis.</p>

</blockquote>
<h3>12. Resolvendo o problema dos tokens longos demais<span class="hx:absolute hx:-mt-20" id="12-resolvendo-o-problema-dos-tokens-longos-demais"></span>
    <a href="#12-resolvendo-o-problema-dos-tokens-longos-demais" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Lá atrás eu falei que os tokens que o GLM gerou pra mim são grandes demais (mais de 100 caracteres). Isso resulta em links super gigantes.</p>
<p>Não é um problema porque ninguém vai ter que digitar isso e nem é pra bookmarkar. Aqui é mais uma escolha pessoal mesmo: eu gosto de tokens curtos em URL.</p>
<p>A vantagem do token longo é que eu não preciso implementar nenhum tipo de &ldquo;banco de dados&rdquo; (key-value store, ou KV Store - Memcached, Redis). Isso porque toda a informação da sessão (ID e timestamp de expiração), estão encodados dentro desse token, por isso ele é longo. É uma struct json, encriptada com AES, convertida em Hex.</p>
<p>Tentando manter o mesmo conceito, <a href="https://github.com/akitaonrails/tvclipboard/commit/eb274968d8ec83c28abce7ffd7352ad6eebd86bc"target="_blank" rel="noopener">neste primeiro commit</a> eu pedi só pra ele tentar otimizar o tamanho da estrutura. Então o GLM sozinho escolheu tentar mudar de UUID (36 chars) pra um Hex ID aleatório de 24 chars. Trocar de timestamp padrão ISO (24 chars) pra UNIX (10 chars). Isso desceu dos 130 a 140 chars pra uns 112 chars. Uma redução na faixa de 20% a 25%.</p>
<p>Não foi uma tentativa ruim. Mas no final eu realmente não estava engolindo tokens grandes assim (poderia ser cookie também, eu sei, mas queria manter tudo na URL).</p>
<p>No final decidi que queria tokens realmente curtos (de 8 chars, base62) e aí precisaria guardar a Sessão em algum &ldquo;storage&rdquo;. Decidi que ia ser &ldquo;in-memory&rdquo; (afinidade ao processo, mas eu só vou rodar 1, sem load balancer, então tá ótimo). E isso seria um FIFO com rotação (chaves mais velhas vão sendo descartadas pra dar espaço pra chaves mais novas e pronto, elas expiram mesmo).</p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/a34b265b585a5661c2595fe474b4613a5aba13c0"target="_blank" rel="noopener">Este é o commit dessa funcionalidade</a> e ainda tive que intervir várias vezes. Por alguma razão ele achou que seria uma boa idéia ter goroutine em background pra ficar limpando os tokens expirados desse storage. Mas isso é completamente desnecessário se for um FIFO. Ele fez, eu tive que mandar desfazer. Essa é a rotina.</p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/a34b265b585a5661c2595fe474b4613a5aba13c0"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128131103_screenshot-2026-01-28_13-10-53.png" alt="FIFO"  loading="lazy" /></a></p>
<h3>13. Mais refatoramento e limpeza (quase acabando!)<span class="hx:absolute hx:-mt-20" id="13-mais-refatoramento-e-limpeza-quase-acabando"></span>
    <a href="#13-mais-refatoramento-e-limpeza-quase-acabando" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Os últimos <strong>6 commits</strong> são <strong>MAIS MANUTENÇÃO!!</strong></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128162905_screenshot-2026-01-28_16-28-52.png" alt="6 commits"  loading="lazy" /></p>
<p>Parece zoeira, mas isso que eu fiz de manutenção é até pouco. Num projeto de verdade isso faz parte da rotina. Toda vez que você adiciona alguma coisa nova, vai estragar alguma coisa que já tinha, e vai precisar ajustar pra não acumular <strong>débito técnico</strong>.</p>
<p>Lembram que rodamos checagem de <strong>race condition</strong>? Adicionamos código novo, tem que checar de novo. E ele achou mais problemas corrigidos <a href="https://github.com/akitaonrails/tvclipboard/commit/df348dbe170451c38b9c9fdf703b57ce6c253578"target="_blank" rel="noopener">neste commit</a>. Não lembro agora se já tinha lá atrás e ele pulou sem corrigir, ou se nas últimas mexidas acabou gerando esses novos bugs.</p>
<p>Depois de tanto adicionar coisas e refatorar, resolvi aumentar mais um pouco a cobertura de código do Go. O comando <code>go test</code> tem a opção <code>-coverprofile</code> pra ajudar a checar quais partes do seu código não foram testadas ainda. Então pedi pro GLM adicionar testes pras novas funcionalidades como Rate Limiting, checagem de tamanho da mensagem, check origin e <a href="https://github.com/akitaonrails/tvclipboard/commit/fc32a575c53d61b7fce5ab941d069dd45d66fc81"target="_blank" rel="noopener">outras coisas</a>.</p>
<p>Mandei adicionar uma besteira que é um <a href="https://github.com/akitaonrails/tvclipboard/commit/2867f8df052f89febc095644ed28aaaefd0b198a"target="_blank" rel="noopener">botão de &ldquo;Fechar&rdquo;</a> no lado cliente, pra não ficar acumulando tabs no meu smartphone.</p>
<p>Depois que mandei trocar o sistema de tokens, não precisa mais encriptar a estrutura de timestamp. Mas ele deixou pra trás testes unitários sobre isso. Então <a href="https://github.com/akitaonrails/tvclipboard/commit/9b5d9f5d3b6504788e73426cc3dc2c6a2a9fe358"target="_blank" rel="noopener">mandei tirar</a>. Novamente: LLMs vão deixar coisas incompletas pra trás. Muitas vezes ele não sabe se mantém um código pra não quebrar o teste, ou se é pra tirar o teste porque o código não é mais necessário.</p>
<p>Depois pedi pro Go rodar <a href="https://github.com/akitaonrails/tvclipboard/commit/1b0e2efd0dfaedf96b2c54eea62438cc7b081924"target="_blank" rel="noopener">linter</a> e suporte a <a href="https://github.com/akitaonrails/tvclipboard/commit/128cf14feff3f1d36a2568c9af8b9665c6b6377a"target="_blank" rel="noopener">golangci-lint</a> pra últimas checagens. Um Linter é tipo uma checagem de formatação, sintaxe e boas práticas de codificação numa linguagem em particular. Mas a maior parte dos ajustes foi espaços sobrando, poucas indentações erradas e coisas desse tipo. Nada de mais.</p>
<p>Uma última coisa que foi negligência minha mesmo, é que estava esquecendo de adicionar testes de Javascript. Uma parte considerável do código é front-end pra lidar com WebSockets e tudo mais. Precisa adicionar, e isso foi <a href="https://github.com/akitaonrails/tvclipboard/commit/bb0d4895f1cfb5b62c0133930b017c93db5d60a8"target="_blank" rel="noopener">neste commit</a>.</p>
<p>Como fiz tudo de uma vez só, aí já pedi pra adicionar ESLint também, e priorizar testar tudo só unitariamente, com mocks e stubs pra ser tudo headless e sem precisar carregar Chromium. Só testar as funções publicamente expostas mesmo. Isso elevou a cobertura de testes de front-end de zero pra 73%, o que é uma ótima cobertura. Veja o <a href="https://github.com/akitaonrails/tvclipboard/commit/bb0d4895f1cfb5b62c0133930b017c93db5d60a8"target="_blank" rel="noopener">commit</a> pra ver como ficou.</p>
<h3>14. Última coisa: I18n<span class="hx:absolute hx:-mt-20" id="14-última-coisa-i18n"></span>
    <a href="#14-%c3%baltima-coisa-i18n" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Isso foi uma coisa que lembrei enquanto já estava escrevendo este artigo. Completamente desnecessário, mas eu queria ver se o GLM conseguiria fazer. E <a href="https://github.com/akitaonrails/tvclipboard/commit/065c52046551b2d8ff426a7206366455e19638c4"target="_blank" rel="noopener">&ldquo;conseguiu&rdquo;</a>. Eu não detalhei como eu queria e deixei ele pensar na solução. Eu teria feito o servidor em Go fazer a troca dos strings antes de enviar o HTML pro client.</p>
<p>Mas o GLM preferiu adicionar isso no lado do front-end e trocar as strings via Javascript. Não está errado, mas não é o melhor jeito.</p>
<p>Justamente tradução de linguagens é uma coisa que IAs foram feitas originalmente pra serem boas: Processamento de Linguagem Natural (NLP). Mandei extrair todos os textos em inglês e depois fazer uma versão em Português do Brasil, e posso dizer que ele fez direitinho:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128153748_screenshot-2026-01-28_15-36-56.png" alt="pt-BR"  loading="lazy" /></p>
<p>Num web app de verdade é uma coisa que sempre precisamos ficar preocupados: de não deixar mensagens pra usuário &ldquo;hardcoded&rdquo; e sempre ficar extraindo e testando. O que me leva à última parte:</p>
<h2>Não era Melhor usar Framework X ou Biblioteca Y?<span class="hx:absolute hx:-mt-20" id="não-era-melhor-usar-framework-x-ou-biblioteca-y"></span>
    <a href="#n%c3%a3o-era-melhor-usar-framework-x-ou-biblioteca-y" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Povo de Go talvez não goste que eu não tenha usado Gin, Fiber ou outro web framework. Mas realmente neste caso o app é simples demais. Não tem mais que 3 endpoints. A parte mais complicada seria o WebSockets, que não ficaria muito diferente usando um framework. Ou checagens maiores como de CORS, que também não seria muito menos LOC.</p>
<p>No lado do Javascript, talvez I18n valesse a pena eu ter importado algum pacote pronto, mas novamente, é muito simples. Não tinha nenhuma grande funcionalidade que tive que reinventar a roda.</p>
<p>Claro, isso porque este app é miseravelmente pequeno, como falei no começo. Um app maior de verdade, eu recomendaria mesmo usar um framework popular com bom suporte da comunidade, bem documentado e tudo mais. Mas coisas do tamanho de um ToDo List realmente não faz diferença a menos que o exercício fosse aprender a usar determinado framework.</p>
<p>Pensando nisso, tem que tomar cuidado porque nenhuma LLM vai escolher pra você. Se começar com prompts pequenos, uma funcionalidade de cada vez (que é o jeito certo), ele tende a escrever as coisas tudo do zero, sem importar nenhuma biblioteca.</p>
<blockquote>
  <p>Você tem que saber de antemão que biblioteca vai pedir pra cada coisa!</p>

</blockquote>
<p>Novamente, um iniciante não saberia, então acabaria reinventando a roda dezenas de vezes em vez de usar bibliotecas populares.</p>
<p>Lembram que falei, lá no começo, pra guardar como era a estrutura do projeto na versão <strong>v0.1</strong> que &ldquo;já funcionava&rdquo;? Eis como fica no final num projeto de verdade:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260128162157_screenshot-2026-01-28_16-20-40.png" alt="final tree"  loading="lazy" /></p>
<h2>Conclusão<span class="hx:absolute hx:-mt-20" id="conclusão"></span>
    <a href="#conclus%c3%a3o" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Ufa! Foi Longo! Parabéns se chegou até aqui!</p>
<p>Minha experiência neste projetinho foi de ter um <strong>estagiário super motivado</strong> que eu precisei ficar o <strong>dia inteiro</strong> olhando atrás do ombro e toda hora falando:</p>
<p><em>&ldquo;Opa! Para tudo, tá errado!&rdquo;</em>
<em>&ldquo;Opa! Desfaz e faz de novo mas deste outro jeito!&rdquo;</em>
<em>&ldquo;Opa! Não, você já tinha feito isso errado antes e tá repetindo!&rdquo;</em></p>
<p>Não estou brincando quando falei que foram mais de 250 prompts. E isso é uma estimativa por baixo, eu acho que escrevi MUITO mais que isso.</p>
<p>Tão logo terminei o processo de Vibe Coding, já fiz push no GitHub e comecei a escrever este artigo, então ainda faltam horas de <strong>REVISÃO</strong> do código todo, do começo ao fim, pra ver se não deixei passar nada. Não daria pra colocar em produção sem fazer isso! Se alguém ver coisas estranhas no código, fiquem à vontade pra mandar nos comentários.</p>
<p>Foi realmente como se eu estivesse mentorando um estagiário. Mas com um problema: diferente de um estagiário de verdade, a LLM já se esqueceu de tudo que eu ensinei pra ele.</p>
<p>Quando for fazer um novo projeto com o GLM, ele vai cometer praticamente TODOS os mesmos erros e equívocos. É como se eu estivesse mentorando um estagiário cego e com memória de peixe dourado.</p>
<p><strong>É extremamente cansativo!!</strong></p>
<blockquote>
  <p>No final levou umas 12 horas pra codificar tudo, e eu tenho a impressão que um desenvolvedor sênior de Go teria feito sozinho na metade do tempo. Um júnior teria levado esse tempo, ao longo de uns 2 ou 3 dias (parando pra pesquisar).</p>

</blockquote>
<p>Eu já sabia disso, já repeti dezenas de vezes, mas a noção de:</p>
<p><em>&ldquo;Não preciso mais contratar programador, a IA faz tudo sozinha.&rdquo;</em> - é uma <strong>GRANDE FALÁCIA</strong>.</p>
<p>E não vai melhorar muito mais do que isso. Já estamos no topo da curva em S que eu expliquei em todo podcast que participei e nos videos do meu canal. Custa ordens de grandeza mais energia pra treinar a próxima versão do que levou todas as versões anteriores, pra ter uma evolução <strong>incremental</strong>.</p>
<p>Enquanto a arquitetura for a mesma, não vai sair disso. Precisa de alguma NOVA DESCOBERTA (que não aconteceu até hoje, desde 2021 quando saiu Transformers).</p>
<p>Um Claude Code vai ser um pouco mais rápido que GLM. A próxima versão do GLM vai ser um pouco mais rápido. Talvez o tempo de 12 horas que eu levei, vire 6 horas (pra este app miserável). Mas o <strong>PROCESSO</strong> vai ser o mesmo: ele faz errado, eu mando consertar, e faz isso <em>ad eternum</em>.</p>
<p>Não vai existir fazer um único prompt e no final sair um app perfeito, seguro, otimizado, etc. SEMPRE vai sair com bugs na primeira vez. Por isso que alguém que não é programador não deve fazer software pra colocar em produção só com Vibe Coding: <strong>precisa</strong> passar por revisão de um programador experiente de verdade.</p>
<p>Isso não é opcional.</p>
<h2>Post-Mortem<span class="hx:absolute hx:-mt-20" id="post-mortem"></span>
    <a href="#post-mortem" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Esta seção foi escrita no dia seguinte à publicação do artigo original. Depois de terminado, pedi ao <strong>Claude Code</strong> pra fazer uma análise profunda da qualidade, focando em segurança e testes. E ele achou <strong>VÁRIOS</strong> erros. Lembram que falei que não dava pra colocar em produção sem revisar com calma antes?</p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/d3e2b1b8dcbe1f0451995623df17b38ad236dbb7"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129095637_screenshot-2026-01-29_09-56-25.png" alt="claude code analysis"  loading="lazy" /></a></p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/d3e2b1b8dcbe1f0451995623df17b38ad236dbb7"target="_blank" rel="noopener">Este é o commit</a> com os consertos, dêem uma olhada.</p>
<p>Então ele &ldquo;Já funcionava&rdquo; na Fase 1, com um monte de problemas. Eu intervi e gastei 90% do tempo na Fase 2 adicionando a fundação. Não revisei propriamente e publiquei o arquivo, agora retornei e ainda tem problemas.</p>
<blockquote>
  <p>Software NUNCA está &ldquo;finalizado&rdquo;.</p>

</blockquote>
<p>Mesmo isso que o Claude Code encontrou, não significa que corrige TUDO. Significa que corrige ALGUMAS COISAS. Nenhum software nunca tem &ldquo;garantia&rdquo; que está perfeito. Isso não existe.</p>
<p>Mesmo se tivesse começado com Claude Code, como falei antes, teria sim, sido mais rápido. Mas tudo depende do que você vai pedir pra ele. Se você não souber exatamente os requerimentos e especificações de segurança, ou performance, ou escalabilidade, ou mantenabilidade ANTES de pedir, ele vai fazer ALGUMAS COISAS. Não necessariamente as COISAS PERFEITAS, mas só ALGUMAS COISAS.</p>
<p>Daí alguém pode dizer <em>&ldquo;Tá vendo? Agora que revisou com Claude Code, vai estar perfeito&rdquo;</em></p>
<p>Então, depois eu ainda pedi pro <a href="https://github.com/akitaonrails/tvclipboard/commit/78dcfb11e58d026e2bc34476d849561724879fa8"target="_blank" rel="noopener"><strong>GPT 5.2 Codex</strong></a> fazer a mesma análise extensiva e adivinha: ele também encontrou mais probleminhas. Muito menos, claro. Mas veja bem, depois do GLM 4.7, do Opus 4.5, o GPT 5.2 ainda achou mais coisas. NUNCA ACABA!</p>
<p>Em particular, lembram como lá atrás eu tinha feito tokens serem encriptados, depois mudei o requerimento pedindo pra não ser e deixar in-memory no servidor? Ainda sobrou código antigo dessa funcionalidade. O GPT 5.2 achou que era pra terminar de implementar a encriptação e começou a colocar de volta. Tive que interromper e mandar ele parar e remover. Nem o Opus 4.5 removeu esse <strong>dead code</strong>.</p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/78dcfb11e58d026e2bc34476d849561724879fa8"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20260129102239_screenshot-2026-01-29_10-22-30.png" alt="GPT 5.2"  loading="lazy" /></a></p>
<p>Sempre, você que pede é o responsável por pedir exatamente e checar depois.</p>
]]></content:encoded><category>vibe-coding</category><category>engenharia-de-software</category></item><item><title>Rant - LLMs vão evoluir pra sempre? Desmistificando LLMs na programação</title><link>https://www.akitaonrails.com/2025/05/01/rant-llms-vao-evoluir-pra-sempre-desmistificando-llms-na-programacao/</link><guid isPermaLink="true">https://www.akitaonrails.com/2025/05/01/rant-llms-vao-evoluir-pra-sempre-desmistificando-llms-na-programacao/</guid><pubDate>Thu, 01 May 2025 02:30:00 -0300</pubDate><description>&lt;p&gt;Vamos recaptular meus últimos artigos:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.akitaonrails.com/2025/04/25/hello-world-de-llm-criando-seu-proprio-chat-de-i-a-que-roda-local"target="_blank" rel="noopener"&gt;Hello World de LLM&lt;/a&gt; - neste artigo explico como criar um programinha simples (educativo) de chat carregando um LLM (Qwen2.5) e até como usar prompts pra que ele consiga chamar scripts/agentes e executar algumas tarefas simples como carregar arquivos locais. Eu subi o código no GitHub como &lt;a href="https://github.com/akitaonrails/Tiny-Qwen-CLI"target="_blank" rel="noopener"&gt;Tiny-Qwen-CLI&lt;/a&gt; e deixei o código &amp;ldquo;sujo&amp;rdquo; de propósito pra poder testar como as LLMs conseguem (ou não ajustar esse código).&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.akitaonrails.com/2025/04/25/seu-proprio-co-pilot-gratuito-universal-que-funciona-local-aider-ollama-qwen"target="_blank" rel="noopener"&gt;Seu Próprio Co-Pilot Gratuito&lt;/a&gt; - aqui eu explico mais sobre a ferramenta Aider, que é como se fosse um Co-Pilot ou Cursor, mas gratuito e open-source. Ele não exige plugins nem IDEs. Roda no terminal e é fácil de usar. Também mostro como integrar com Ollama e subir seus próprios LLMs locais.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.akitaonrails.com/2025/04/29/dissecando-um-modelfile-de-ollama-ajustando-qwen3-pra-codigo"target="_blank" rel="noopener"&gt;Dissecando um Modelfile de Ollama&lt;/a&gt; - neste eu desço mais na teoria dos LLMs e explico o que é Key Sampling e os como diversos parâmetros afetam a geração de texto das LLMs, como Temperature ou Top_P. Aqui é pra desmistificar ainda mais que não existe nenhuma &amp;ldquo;mágica&amp;rdquo; em como uma LLM consegue gerar textos.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ao longo dos últimos dias eu vim testando e postando no X minhas impressões sobre os mais diversos modelos comerciais e open source como Claude Sonnet, Gemini 2.5, OpenAI O4, Deepseek-R1, o novo Qwen3 e mais. Tem mais posts no blog além dos que mencionei acima, mas estes são os principais.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Vamos recaptular meus últimos artigos:</p>
<ul>
<li><a href="https://www.akitaonrails.com/2025/04/25/hello-world-de-llm-criando-seu-proprio-chat-de-i-a-que-roda-local"target="_blank" rel="noopener">Hello World de LLM</a> - neste artigo explico como criar um programinha simples (educativo) de chat carregando um LLM (Qwen2.5) e até como usar prompts pra que ele consiga chamar scripts/agentes e executar algumas tarefas simples como carregar arquivos locais. Eu subi o código no GitHub como <a href="https://github.com/akitaonrails/Tiny-Qwen-CLI"target="_blank" rel="noopener">Tiny-Qwen-CLI</a> e deixei o código &ldquo;sujo&rdquo; de propósito pra poder testar como as LLMs conseguem (ou não ajustar esse código).</li>
<li><a href="https://www.akitaonrails.com/2025/04/25/seu-proprio-co-pilot-gratuito-universal-que-funciona-local-aider-ollama-qwen"target="_blank" rel="noopener">Seu Próprio Co-Pilot Gratuito</a> - aqui eu explico mais sobre a ferramenta Aider, que é como se fosse um Co-Pilot ou Cursor, mas gratuito e open-source. Ele não exige plugins nem IDEs. Roda no terminal e é fácil de usar. Também mostro como integrar com Ollama e subir seus próprios LLMs locais.</li>
<li><a href="https://www.akitaonrails.com/2025/04/29/dissecando-um-modelfile-de-ollama-ajustando-qwen3-pra-codigo"target="_blank" rel="noopener">Dissecando um Modelfile de Ollama</a> - neste eu desço mais na teoria dos LLMs e explico o que é Key Sampling e os como diversos parâmetros afetam a geração de texto das LLMs, como Temperature ou Top_P. Aqui é pra desmistificar ainda mais que não existe nenhuma &ldquo;mágica&rdquo; em como uma LLM consegue gerar textos.</li>
</ul>
<p>Ao longo dos últimos dias eu vim testando e postando no X minhas impressões sobre os mais diversos modelos comerciais e open source como Claude Sonnet, Gemini 2.5, OpenAI O4, Deepseek-R1, o novo Qwen3 e mais. Tem mais posts no blog além dos que mencionei acima, mas estes são os principais.</p>
<p>Objetivo deste post é desmistificar mais as LLMs e falar mais sobre minha experiência testando a maioria dos LLMs mais populares.</p>
<h3>Desmistificando LLMs - 1. Benchmarks e Rankings<span class="hx:absolute hx:-mt-20" id="desmistificando-llms---1-benchmarks-e-rankings"></span>
    <a href="#desmistificando-llms---1-benchmarks-e-rankings" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quem acompanha minhas palestras e videos faz anos está careca de me ouvir falar sobre o livro <a href="https://www.amazon.com.br/Como-Mentir-Estat%C3%ADstica-Darrell-Huff/dp/858057952X"target="_blank" rel="noopener">&ldquo;Como Mentir com Estatísticas&rdquo;</a>. E o que eu mais vejo nas centenas de posts sobre LLMs é sobre os rankings baseados em benchmarks. Este é um exemplo que viralizou ontem, no lançamento do novo modelo da Alibaba, o Qwen3:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/1kflvwk5uq9lkt5yh8knngwxwfi9" alt="Qwen3 benchmarks"  loading="lazy" /></p>
<p>O problema: os rankings não estão errados. Eles querem dizer o seguinte:</p>
<p><strong>&ldquo;Qwen 3 supera Qwen 2.5 e também outros famosos como OpenAI o1, DeepSeek-R1, Grok 3 Beta, Gemini 2.5, etc &hellip;. NOS BENCHMARKS RODADOS - e APENAS nos benchmarks rodados.&rdquo;</strong></p>
<p>É apenas isso. Mas 99% dos posts publica assim:</p>
<p><strong>&ldquo;Qwen 3 É O MELHOR PRA DESENVOLVER CÓDIGO do que Qwen 2.5 e também outros famosos como OpenAI o1, DeepSeek-R1, Grok 3 Beta, Gemini 2.5, etc&rdquo;</strong>*</p>
<p>Os benchmarks não estão errados, quem está errado é quem interpretou que isso significa ser melhor em tudo. Vamos pra outra analogia:</p>
<p><strong>&ldquo;O Aluno Fulano conseguiu 99% de acerto em todos os simulados do vestibular&rdquo;</strong></p>
<p>A conclusão das pessoas:</p>
<p><strong>&ldquo;O Aluno Fulano é 99% melhor que todo mundo em tudo&rdquo;</strong></p>
<p>Espero que isso soe obviamente errado. Ser melhor em 1 coisa não torna ninguém melhor em tudo, mas em I.A. é assim que toda notícia de uma nova versão de LLM é publicado em posts pessoais e veículos jornalísticos. É um óbvio <strong>CLICKBAIT</strong>. E você cai.</p>
<p>E se você está acostumado a ouvir dessa forma, faz parecer que a cada nova versão de LLM - que é &ldquo;melhor em tudo&rdquo; - que o anterior, falta muito pouco ou já chegou no ponto onde sim, não precisa mais existir programadores humanos, nem matemáticos, nem engenheiros, nem médicos, afinal, a LLM é &ldquo;BOA EM TUDO&rdquo;.</p>
<p>E nada poderia ser mais longe da verdade que isso. Essas pesquisas estatísticas de ranking tem uma METODOLOGIA de testes e um pacote de benchmark fixo. Os testes são SIMPLES, pense coisas no nível &ldquo;faça uma função que calcule fatorial&rdquo;, &ldquo;faça uma função que ordene uma lista de palavras&rdquo;, &ldquo;dado uma lista de palavras, encontre um em específico em tempo logarítmico&rdquo;, etc. Coisas que você vê nos cursos introdutórios de ciências da computação ou Leet Code.</p>
<p>Esses testes são rodados MÚLTIPLAS VEZES e os &ldquo;outliers&rdquo; (testes onde se errou demais, ou testes onde se acertou demais, ou algo que saia da média) são descartados, é feita uma distribuição estatísticas e compilado um &ldquo;número&rdquo; geral genérico como &ldquo;acertou 99%&rdquo; ou algo assim.</p>
<p>Ele errou muitas vezes, mas esse ponto de dados é um outlier, e foi removido da conta. É estatisticamente irrelevante dentro da metodologia. Mas é um fato: nenhuma LLM acerta 100% das vezes nem nunca vai acertar. Eu demonstrei isso no meu post sobre Modelfiles onde eu explico que existe um fator aleatório controlado (temperatura, top_p, top_k, min_p, key sampling). E existe também normalizações e fatores aleatórios no treinamento (softmax, relu). E ainda tem fatores de otimização posterior que &ldquo;arredonda&rdquo; ou &ldquo;trunca&rdquo; as probabilidades como quantização (fp8, Q4, etc). Ou seja, existem diversos fatores de &ldquo;micro erros&rdquo; e aleatoriedade embutidos no processo inteiro. Nunca vai ser uma resposta &ldquo;certa&rdquo;, só &ldquo;possivelmente uma das mais certas&rdquo; e esse &ldquo;possivelmente&rdquo; pode variar MUITO.</p>
<h3>Dissecando Mitos de I.A. - 2. Evolução Exponencial<span class="hx:absolute hx:-mt-20" id="dissecando-mitos-de-ia---2-evolução-exponencial"></span>
    <a href="#dissecando-mitos-de-ia---2-evolu%c3%a7%c3%a3o-exponencial" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Quem chegou só agora e está vendo notícias frequentes sobre lançamentos de novas versões de LLMs como o Qwen3 da Alibaba agora, o novo MiMo da Xiaomi, a versão 2.5 do Gemini, a versão 4.1 do ChatGPT, etc tem a impressão que estamos evoluindo a passos largos.</p>
<p>Mas a verdade é que sim, 2022 foi um marco histórico com o ChatGPT original, principalmente GPT 2. Mas de lá pra cá a evolução desacelerou. Cada nova versão não é mais visivelmente &ldquo;o dobro melhor&rdquo; que a anterior, a não ser em certos benchmarks, em certas condições específicas - que não condizem com a realidade. Já vou demonstrar.</p>
<p>Um amigo meu me lembrou sobre a evolução das tecnologias de deep learning em reconhecimento de imagens, anos antes:</p>
<ul>
<li>
<p>2012 – AlexNet: Primeira grande vitória no ImageNet com 8 camadas, alcançando 84,7% de top-5 accuracy.​</p>
</li>
<li>
<p>2014 – VGG: Redes com 16–19 camadas, melhorias incrementais, mas com aumento significativo de parâmetros.​</p>
</li>
<li>
<p>2015 – ResNet: Introdução de conexões residuais, permitindo redes com mais de 100 camadas e superando desempenho humano em top-5 accuracy.​</p>
</li>
<li>
<p>2016–2017 – Inception, ResNeXt, SENet: Modelos mais complexos com ganhos marginais.​</p>
</li>
<li>
<p>Pós-2018: Acurácia top-1 se estabiliza em torno de 85–88%, com ganhos marginais mesmo em modelos como EfficientNet e NasNet.</p>
</li>
</ul>
<p>I.A. é estudada desde literalmente o início da computação moderna no fim dos anos 1930, com ninguém menos que o próprio Alan Turing ou John Von Neumann. Eu contei sobre eles aqui:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/G4MvFT8TGII"
    title="YouTube video player"
    frameborder="0"
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
    referrerpolicy="strict-origin-when-cross-origin"
    allowfullscreen>
  </iframe>
</div>

<p>Pense um &ldquo;início lento&rdquo;: de 1930 até cerca de 2012 quando ImageNet e coisas como AlphaGo, GANs e coisas assim começaram a ganhar velocidade. Saímos da perna do &ldquo;S&rdquo; pro meio a partir dos anos 2010 e viemos acelerando até agora. Eu &ldquo;chuto&rdquo; que ou já estamos ou estamos caminhando pro topo do &ldquo;S&rdquo;, onde a curva estabiliza e desacelera. Foi isso que aconteceu com deep learning de imagens:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/lgr7uodnbmqfujjw39h69mnybi8t" alt="ImageNet"  loading="lazy" /></p>
<p>Lembram do AlphaGo? Eu acompanhei ao vivo. Como amador-hobista em Go, eu assisti ao vivo os jogos com me mestres sul-coreanos como Lee Sedol, na época que aconteceu, acho que 2016. Vocês devem ter visto o <a href="https://www.netflix.com/br-en/title/80190844"target="_blank" rel="noopener">documentário na Netflix</a> só agora, mas isso vem acontecendo faz anos. E agora, onde está o AlphaGo evoluindo exponencialmente? Todo mundo já esqueceu, porque o quente da moda são LLMs, que é só mais uma categoria no mundo de I.A.</p>
<p>A grande maioria das otimizações já foram ou já estão sendo feitas. Se fosse resumir só alguns dos milestones mais importantes desta parte da geração seria mais ou menos assim:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/z14qvijztqvy6prvbpokk02fqrjt" alt="timeline llm"  loading="lazy" /></p>
<p>Todo engenheiro e cientista sabe disso: quando você não tem nada, qualquer coisa é um salto quântico. Quando você só tem 1 real, 2 reais já é &ldquo;o dobro&rdquo;. Quando tem 10 reais, 20 já é bastante. Mas quando chega em 20 mil, mais 100 já não adiciona tanto valor assim do que quando você só tinha 1 (ali seria 100x).</p>
<p>Cada nova gota de otimização agora CUSTA MUITO. Mesmo em otimização com perda de precisão (quantização) já saímos de fp32, fomos pra fp16, fp8, já chegamos em 1-bit. Não tem mais pra onde descer depois disso. Estamos no ponto onde estamos trocando precisão por performance.</p>
<p>Dobro de parâmetros não resulta mais em dobro de qualidade de resultado. Precisa dobrar pra ter 5%? Talvez? E não adianta continuar aumentando, o resultado começa a piorar em vez de melhorar. Por isso já pulamos pra coisas como LLMs especialistas, MoE (Mixture of Experts).</p>
<p>Deep thinking/Chain-of-Though/Reasoning foi um grande salto, mas só aumentar mais thnking também já não dá mais tanto resultado, na verdade, em muitos casos piora o resultado com &ldquo;over-thinking&rdquo;. Pensar demais não é linear também.</p>
<p>Aumentar contexto também não ajuda a melhorar as respostas. Pelo contrário, já vemos que a partir de um certo ponto, mais contexto atrapalha o resultado, porque a arquitetura de atenção (o grande diferencial das LLMs), tem limites. Sim, um Deepseek ou Gemini dizem suportar &ldquo;1 milhão de tokens&rdquo;. Mas eu já expliquei nos artigos anteriores que se trata de <strong>Sliding Window Attention</strong> como SDPA ou Flash onde a atenção é voltada só a uma PARTE desse grande contexto: ele não consegue dar atenção pra tudo o tempo todo.</p>
<p>Todo lugar onde dava pra colocar coisas óbvias, como caching, seja no treinamento, seja na geração, já foram colocadas. Estamos ficando sem opções aqui. E sim, novas descobertas continuam acontecendo, mas como falei, nenhuma ainda conseguiu ultrapassar os limites que sabemos que existem.</p>
<h3>Desmistificando Mitos de I.A. - 3. Acabar com Programadores<span class="hx:absolute hx:-mt-20" id="desmistificando-mitos-de-ia---3-acabar-com-programadores"></span>
    <a href="#desmistificando-mitos-de-ia---3-acabar-com-programadores" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>O que eu mais detesto do hype atual são as falsas promessas de &ldquo;AGI&rdquo;, &ldquo;Vibe Coding&rdquo;, &ldquo;trocar funcionários por MCPs&rdquo;. Vou afirmar de novo: é tudo propaganda.</p>
<p>Propaganda mentirosa e falsas promessas de óleo de cobra (coisa que sempre existiu e sempre vai existir na história) DESVALORIZA a realidade as inovações que temos hoje.</p>
<p><em>&ldquo;Mas o CEO da Anthropic e o tio Zuck disseram que vão substituir programadores. Microsoft e Google disseram que 30% do código deles já sai de I.A.&rdquo;</em></p>
<p>Como falei antes sobre estatística. Sim: muita coisa VAI ser feita com I.A. mesmo. Não precisa de I.A. pra otimizar trabalho em várias área:</p>
<ul>
<li>
<p>um pedaço de pau e um bloco de argila são suficientes pra substituir a memória das pessoas. Ninguém lembra de detalhes, especialmente numéricos, permanentemente. Faz milênios que evoluímos em ajudar nossa memória com algum tipo de armazenamento externo. Seja bloco de argila, seja papel, seja post-it, seja um arquivo .txt na sua máquina.</p>
</li>
<li>
<p>a primeira câmera fotográfica já removeu a necessidade de contratar um pintor de quadros pra se ter uma &ldquo;selfie&rdquo;.</p>
</li>
<li>
<p>o primeiro computador mais primitivo já eliminou a necessidade de um prédio inteiro de matemáticos fazendo e refazendo cálculos no papel. Mesmo assim, foi desse jeito que os primeiros foguetes foram pro espaço. Já assistiu o filme <a href="https://www.netflix.com/title/80123775"target="_blank" rel="noopener">Hidden Figures</a>? Recomendo</p>
</li>
<li>
<p>carros já são montados quase que totalmente por robôs, faz anos. robótica não é novidade na indústria. Hoje em dia, um tablet já substituiu caixas de supermercado e de McDonalds. Tudo isso sem precisar de I.A. URAs e botzinhos já substituíram muito de telemarketing e suporte.</p>
</li>
</ul>
<p>Então sim, na prática, I.A. também vai substituir uma classe inteira de programadores: os de baixo valor agregado. Os que não sabem fazer muito mais que copy e paste ou cuja rotina de trabalho seja repetitivo. Se seu trabalho envolve tarefas repetitivas, é óbvio que é questão de tempo até serem automatizados, seja por I.A. seja por meros scripts. Coisa que já fazemos hoje.</p>
<p>O trabalho de um programador sempre foi substituir o trabalho dos outros. Você que trabalha fazendo sistema de e-commerce: você ajudou a substituir o trabalho de vendedores, vitrinistas, decoradores de loja, caixas. Você que faz sistemas de tickets e suporte, você já substituiu atendentes humanos. Você que faz sistemas de entregas, já substituiu telefonistas. Você que faz sistemas financeiros, já substituiu bancários, quem constrói agências de banco, etc.</p>
<p>Você, programador, sempre trabalhou substituindo o trabalho dos outros. Era questão de tempo até programadores substituírem trabalho de programadores. Chame isso de <strong>justiça poética</strong>. É só evolução mesmo. Acontece. Nada no mundo é feito pra ser estático e nem garantido.</p>
<p>Aqui vai a boa notícia: como sempre acontece. Ainda existem pintores, mesmo existindo fotógrafos. Ainda existem vendedores, mesmo existindo e-commerce. Ainda existe construção, mesmo não precisando tanto de lojas ou até escritórios (com home office). Ainda existe e vai continuar existindo, médicos, advogados, contadores, várias profissões. A diferença é que antes, uma empresa de contabilidade precisava de um prédio cheio de pessoas que sabiam usar ábacos e escrever planilhas em papel. Hoje um único contador que saiba usar Excel substitui um andar inteiro. Antigamente, uma empresa de publicidade era um prédio inteiro, hoje é uma pessoa que usa Canva, CapCut e Photoshop.</p>
<p>Programador é a mesma coisa. Antes era um prédio inteiro com piscina de bolinha roxa. Amanhã vai ser um, BOM, programador com Cursor e Aider. Quem some nessa brincadeira? Quem fazia o trabalho braçal de baixo valor agregado.</p>
<p>Eu avisei que isso ia acontecer. Durante 5 anos, antes de existir esta geração de LLMs e I.A. Porque é o rumo natural das coisas. Programador de curso online de fim de semana. Esse é o primeiro que vai desaparecer.</p>
<p>Você sabe cálculo? Sabe Álgebra? Sabe Álgebra Linear? Sabe Estatística de Probabilidade? Sabe o que é uma distribuição estatística? Sabe o que são vetores ou tensors? Sabe o que é um espaço de Hilbert? Não? Eu me precaveria se fosse você. Se todos os artigos que eu publiquei neste mês de Abril/2025 foram muito difíceis, vocês já sabem o que precisam estudar.</p>
<h3>Minha experiência com LLMs<span class="hx:absolute hx:-mt-20" id="minha-experiência-com-llms"></span>
    <a href="#minha-experi%c3%aancia-com-llms" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Eu venho usando LLMs faz 2 anos. Todo tipo de pesquisa ou código que faço passa por alguma LLM em alguma capacidade. Eu uso pesado mesmo. Meu histórico de ChatGPT é gigante.</p>
<p>E depois de fazer mais um intensivão pra ter certeza. Sim, o4-mini-high, Sonnet 3.7, Gemini 2.5, Qwen3, Deepseek-R1, etc são todos extremamente úteis e nenhum deles, absolutamente nenhum, consegue me satisfazer.</p>
<p>Vibe Coding é uma enorme bobagem. Ninguém sem conhecimentos de programação consegue fazer mais que software super simples (e cheio de bugs) usando só LLMs e prompts ou MCPs. Isso é um fato.</p>
<p><em>&ldquo;Mas a evolução é exponencial, ano que vem vai ser perfeito.&rdquo;</em></p>
<p>Não vai. Eu já argumentei esse ponto acima.</p>
<p><em>&ldquo;Mas os benchmarks dizem que eles já conseguem fazer 99% de código perfeito.&rdquo;</em></p>
<p>Também já argumentei esse ponto, sua interpretação está equivocada.</p>
<p>Estamos perto ou já no começo do topo da curva &ldquo;S&rdquo;. E eu não vejo problema nenhum nisso. Eu entendo como LLMs são feitos, entendo como são otimizados, entendo o que conseguem fazer e, mais importante, entendo o que são incapazes de fazer.</p>
<p>Vejam os <a href="https://github.com/akitaonrails/Tiny-Qwen-CLI/pulls"target="_blank" rel="noopener">pull requests</a> de todas as tentativas que tentei fazer de refatoração de código e testes unitários com as principais LLMs do mercado, abertas e fechadas. O que eu vejo:</p>
<ul>
<li>precisa de MUITO prompt e muita instrução pra fazer eles começarem a cuspir código que funciona.</li>
<li>eles parecem um estagiário, o famoso &ldquo;na minha máquina funciona&rdquo;. Saem cuspindo, um monte de copy e paste de código, e não re-checam se faz sentido ou se precisava. O tanto de código inútil que eles geram é assustador.</li>
<li>eles repetem os mesmos erros, mesmo avisando pra não cometer.</li>
<li>mesmo com &ldquo;agentes&rdquo;, &ldquo;MCP&rdquo;, mesmo rodando o código que foi gerado e dando o stacktrace de erros, eles facilmente desfocam, perdem atenção e não consguem corrigir. Mesmo re-tentando diversas vezes pra corrigir o mesmo erro. Mesmo dando dicas. Mesmo escrevendo longamento o que está errado. Rapidamente eles entram em loop. Especialmente se tiver o tal &ldquo;thinking&rdquo; ativado. Eles pensam demais e não resolvem.</li>
<li>cansei de ver tentativa de sair mexendo aleatoriamente no código, em partes que nem pedi pra mexer, e quebrar mais do que consertar.</li>
<li>nenhum deles foi capaz de gerar testes unitários que funcionam de primeira. Alguns conseguiram fazer alguns simples, depois de dar o erro e pedir pra consertar. Alguns ficam em loop de pensamento, confusos, e saem mudando sem corrigir. No final fracassam.</li>
<li>Do nada começam a alucinar. O Qwen 3 mesmo, se eu tentar aumentar só um pouco temperature, de 0.1 pra 0.2 do nada começa a sair caracter chinês no meio do código, assim:</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/j0rnlba6loxmo32pyyjxy8diteor" alt="chinese char"  loading="lazy" /></p>
<ul>
<li>Vira e mexe, mais de uma LLM, conseguiu a proeza de gerar teste unitário que dava erro recursivo. Isso enchia tanto o stacktrace que se eu tentava passar pra ele (mais de 12 mil tokens), isso estourava a janela de contexto e dali ele não conseguia dar atenção pra mais nada:</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/eqh6ytb84mvcpzr92dk7kge2exaj" alt="recursive error"  loading="lazy" /></p>
<ul>
<li>
<p>como erram demais, eu fico repetidamente e exaustivamente dando chance pra ele consertar. Eu rodo o código que ele gerou, mando o stacktrace de erro, peço pra consertar. Ele mexe outra parte do código. Rodo de novo, dá erro e repito. Se fizer isso mais que meia dúzia de vezes, o contexto começa a ficar cheio, sliding window faz ele dividir atenção, ele &ldquo;esquece&rdquo; o que veio antes, começa a repetir o mesmo erro e fica nisso eternamente.</p>
</li>
<li>
<p>não importa se suporta 8 mil tokens, 30 mil tokens, 100 mil tokens ou 1 milhão de tokens no contexto. Isso não influenciou a qualidade do código e nem melhorou essa rotina de repetição de erros. Ele nunca consegue dar atenção pra tudo.</p>
</li>
<li>
<p>não adianta tentar tunar o key sampling. seja com temperature 0, 0.1, 0.6. Só faz ele errar um pouco depois ou um pouco antes, mas ele rapidamente erra e rapidamente fica incapaz de corrigir os próprios erros. Ferramentas como Aider pegam o código que ele sugeriu e grava em cima do arquivo de código de verdade. Várias vezes, por encher demais o contexto, ele começa a &ldquo;esquecer&rdquo; e apagar trechos do código. Ainda bem que GIT existe, porque várias vezes precisei dar <code>git checkout</code> pra voltar pro começo. Se você não checa, cuidado, ele apaga código ou sobrescreve com bobagem, e isso é frequente.</p>
</li>
<li>
<p>deep thinking/reasoning/chain-of-thought, seja lá como você prefere chamar, só significa que ele fica pensando em excesso e enchendo o contexto de linguiça, comendo seus tokens. Se sua API comercial, cuidado: você vai acabar com seus créditos muito rápido e o resultado não é significativamente melhor. São casos isolados onde isso ajuda. Eu recomendo deixar desligado por padrão e ligar só em alguns casos especiais.</p>
</li>
</ul>
<p>E veja: meu projeto de exemplo é ridículo: são 5 arquivos. 4 deles tem menos de 100 linhas de código, o maior tem 400 e tantas linhas. Um projeto de verdade, grande, são MILHÕES de linhas de código. Não cabe em nenhum contexto de nenhum Gemini ou Deepseek. E mesmo se coubesse, só deixaria ele mais confuso. A boa prática em qualquer ferramenta é dar o mínimo de arquivos pra ele. E isso restringe os casos de uso a refatorações locais e isoladas, jamais coisas como renomear uma classe ou função que é usada em centenas de arquivos. Pra isso, continue usando seu IntelliJ.</p>
<p>Nenhum modelo que testei é perfeito. Alguns modelos se dão um pouco melhor em alguns casos. Na primeira tentativa que fiz com o Gemini 2.5, ele até que conseguiu executar as tarefas que pedi. Não deu um código maravilhoso, mas pelo menos não quebrou nada.</p>
<p>Na segunda vez, dei <code>git reset --hard</code> e re-comecei, com os mesmos arquivos e os mesmos prompts. Dessa vez foi extremamente decepcionante. Ele quebrou o código principal na refatoração e foi incapaz de terminar um teste unitário que funcione mesmo eu forçando ele várias vezes. Desisti pelo cansaço.</p>
<p>Na terceira vez, ele deu resultado diferente, melhorou um pouco o código e ele conseguiu terminar o mesmo teste. Mas também precisou de algumas rodadas de teste quebrado, dar stacktrace e mandar corrigir erros óbvios. E quanto eu digo &ldquo;óbvio&rdquo; é ele consertar um mock que ele mesmo colocou sem precisar.</p>
<p>E isso é consistente em todos os outros modelos. Se eu só rodo uma vez, tenho uma impressão. Por exemplo, que Claude é melhor que Qwen3. Mas se eu rodo de novo, o mesmo teste, agora o Claude me decepciona e o Qwen3 passa na frente. Nunca tem um vencedor objetivamente melhor. Estão todos mais ou menos na mesma posição e ficam mudando no ranking dependendo de quantas vezes você repete exatamente o mesmo teste, com o mesmo código e os mesmos prompts.</p>
<p>E quem fica repetindo a mesma coisa? Ninguém. Todo mundo aceita a primeira resposta. E a primeira resposta pode estar BEM errada. Num nível óbvio de errada. Se aceitar automaticamente, vai inserir lixo no repositório. E esse é meu problema com coisas como MCP ou agentes: se deixar tudo no automático e pedir coisas como &ldquo;pegue todos os arquivos de código deste diretório e corrija todos os bugs&rdquo;. Ele vai, com toda certeza, gerar mais bugs, apagar código, sumir com arquivos e deixar o repositório final num estado pior do que antes.</p>
<p>Vibe Coding puro, com nenhuma interação de um programador, não funciona. Eu testei todos os modelos principais. Nenhum foi capaz de passar testes simples. Nenhum vai ser capaz de passar por testes maiores de verdade. Não sem muita interação de um bom programador pra rejeitar os erros e mandar repetir, ou corrigir manualmente o que saiu errado.</p>
<p>E adivinhem: codificador que só sabe fazer copy e paste do stackoverflow (e agora do ChatGPT), é incapaz de reconhecer muitos desses erros. Então em vez de ajudar, ele pode piorar.</p>
<p>Pra mim, que sei exatamente o que eu quero, os erros são óbvios e eu automaticamente rejeito. E eu rejeito o que toda LLM faz mais do que aceito. Muitas vezes eu só pego um trecho do que ele sugeriu e descarto todo o resto. Esse é o dia a dia de verdade. Todo mundo que diz o contrário não fez código mais complicado do que um Hello World.</p>
<p>Por que você acham que todo exemplo de <em>&ldquo;uau, veja a LLM fazendo código sozinha&rdquo;</em> sempre é fazendo uma função de fatorial? Ou uma página web tosca? Porque é só isso que ele consegue fazer mesmo. Nenhum exemplo de MCP que eu vi até agora me deixou impressionado. Foi tudo fumaça. E você foi enganado pela propaganda.</p>
<p>Agora você sabe.</p>
<p>Como faz pra saber mais e não ser enganado? Comece aprendendo os fundamentos. 1 ano atrás eu fiz vários videos explicando como LLMs funcionam. Você já deveria ter assistido:</p>
<p><a href="https://www.youtube.com/playlist?list=PLdsnXVqbHDUeowsAO0sChHDY4D65T5s1U"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/7tshjfwbly7wudczbni2pa863f11" alt="Playlist I.A."  loading="lazy" /></a></p>
<p>E pra virar programador que não vai ser substituído pela I.A.? É pra isso que eu fiz <a href="https://www.youtube.com/@Akitando"target="_blank" rel="noopener">meu canal</a>. Quando tudo que eu disse virar comum pra você, parabéns, você tem boas chances de sobreviver.</p>
]]></content:encoded><category>llms</category><category>vibe-coding</category><category>benchmarks-de-llm</category><category>off-topic</category></item></channel></rss>