<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Software-Engineering | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/software-engineering/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:52 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/software-engineering/index.xml" rel="self" type="application/rss+xml"/><item><title>LLMs + Agents = competent enough</title><link>https://akitaonrails.github.io/en/2026/10/12/llms-agents-competent-enough/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/10/12/llms-agents-competent-enough/</guid><pubDate>Mon, 12 Oct 2026 12:00:00 -0300</pubDate><description>&lt;p&gt;This post is a synthesis. It ties into a single narrative what I&amp;rsquo;ve been writing and measuring through 2026, so here&amp;rsquo;s the road map before the argument:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://akitaonrails.github.io/en/2026/09/23/llm-benchmark-v4-opus-5-5-gpt-6-sol-luna-mimo-2-6-grok-4-7/"&gt;New v4 benchmarks&lt;/a&gt;&lt;/strong&gt;: I retested the major models and the uncomfortable conclusion was convergence, not a leap.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://akitaonrails.github.io/en/2026/09/28/llms-are-good-at-reproducing-what-exists-what-about-new-things/"&gt;The limits of LLMs&lt;/a&gt;&lt;/strong&gt;: models are great at reproducing what exists, extremely expensive at what doesn&amp;rsquo;t, and the served model is a static file that learns nothing from your marathon.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://akitaonrails.github.io/en/2026/10/11/are-there-better-languages-for-programming-with-llms/"&gt;Are there better languages for programming with LLMs?&lt;/a&gt;&lt;/strong&gt;: no. Neither script conciseness nor strong typing changed the outcome; test discipline did.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://akitaonrails.github.io/en/2026/10/10/which-jev-like-ai-decision-classifier-model-is-best/"&gt;Which Jev-like decision model is best?&lt;/a&gt;&lt;/strong&gt;: 21 classifier configurations, and the &amp;ldquo;specialized models&amp;rdquo; themselves converged to within a few points of each other.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://akitaonrails.github.io/en/2026/04/20/clean-code-for-ai-agents/"&gt;Clean Code for AI agents&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href="https://akitaonrails.github.io/en/2026/04/11/vs-code-is-the-new-punch-card/"&gt;VS Code is the new punch card&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href="https://akitaonrails.github.io/en/2026/09/17/talking-about-my-ai-skills/"&gt;my skills&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href="https://akitaonrails.github.io/en/2026/08/18/hot-take-harness-loop-engineering-graph-engineering-are-bullshit/"&gt;the hot take against loop engineering&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://akitaonrails.github.io/en/2026/05/30/open-source-best-practices-llm-the-minimum/"&gt;minimum best practices&lt;/a&gt;&lt;/strong&gt;: the same thesis from other angles. The model is a commodity; the engineering around it is the differentiator.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The thesis that closes this arc: &lt;strong&gt;LLMs have reached, or are very close to reaching, the ceiling of the transformer architecture.&lt;/strong&gt; And the good news is that nothing more than that is needed.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post is a synthesis. It ties into a single narrative what I&rsquo;ve been writing and measuring through 2026, so here&rsquo;s the road map before the argument:</p>
<ul>
<li><strong><a href="/en/2026/09/23/llm-benchmark-v4-opus-5-5-gpt-6-sol-luna-mimo-2-6-grok-4-7/">New v4 benchmarks</a></strong>: I retested the major models and the uncomfortable conclusion was convergence, not a leap.</li>
<li><strong><a href="/en/2026/09/28/llms-are-good-at-reproducing-what-exists-what-about-new-things/">The limits of LLMs</a></strong>: models are great at reproducing what exists, extremely expensive at what doesn&rsquo;t, and the served model is a static file that learns nothing from your marathon.</li>
<li><strong><a href="/en/2026/10/11/are-there-better-languages-for-programming-with-llms/">Are there better languages for programming with LLMs?</a></strong>: no. Neither script conciseness nor strong typing changed the outcome; test discipline did.</li>
<li><strong><a href="/en/2026/10/10/which-jev-like-ai-decision-classifier-model-is-best/">Which Jev-like decision model is best?</a></strong>: 21 classifier configurations, and the &ldquo;specialized models&rdquo; themselves converged to within a few points of each other.</li>
<li><strong><a href="/en/2026/04/20/clean-code-for-ai-agents/">Clean Code for AI agents</a></strong>, <strong><a href="/en/2026/04/11/vs-code-is-the-new-punch-card/">VS Code is the new punch card</a></strong>, <strong><a href="/en/2026/09/17/talking-about-my-ai-skills/">my skills</a></strong>, <strong><a href="/en/2026/08/18/hot-take-harness-loop-engineering-graph-engineering-are-bullshit/">the hot take against loop engineering</a></strong> and <strong><a href="/en/2026/05/30/open-source-best-practices-llm-the-minimum/">minimum best practices</a></strong>: the same thesis from other angles. The model is a commodity; the engineering around it is the differentiator.</li>
</ul>
<p>The thesis that closes this arc: <strong>LLMs have reached, or are very close to reaching, the ceiling of the transformer architecture.</strong> And the good news is that nothing more than that is needed.</p>
<h2>Convergence, not a leap<span class="hx:absolute hx:-mt-20" id="convergence-not-a-leap"></span>
    <a href="#convergence-not-a-leap" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In my benchmarks, I see no relevant improvement in general programming since the Opus 4.5 generation. Yes, Fable and Astra are great at specific problems. In general programming, what I measure is everyone squeezed onto the same tier, including the Chinese commercial offerings (Kimi, GLM, DeepSeek). Model choice has become nearly irrelevant.</p>
<p>What still makes a real difference is the effort level: low effort for translation and summarization, high effort for vulnerability sweeps, heavy bug fixing or optimization. Beyond that, the numbers tell a reality much more boring than <a href="/en/2026/09/09/propagandas-enganosas-da-openai-anthropic-nvidia/">the vendors&rsquo; propaganda</a>.</p>
<blockquote>
  <p>My reading of the timeline: the models&rsquo; &ldquo;core&rdquo; reasoning had its last big leap in 2024. 2025 was the year of optimizing the architecture. 2026 is the year of refining the harness. Base intelligence made no order-of-magnitude leap in that period; efficiency and long-session competence did.</p>

</blockquote>
<h2>The timeline, with receipts<span class="hx:absolute hx:-mt-20" id="the-timeline-with-receipts"></span>
    <a href="#the-timeline-with-receipts" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><img src="/2026/10/12/llms-agentes-competente-o-suficiente/llms-agentes-timeline.png" alt="Timeline from 2019 to 2026: GPT-2 proves LLMs are useful, GPT-3 opens the parameter war, Chinchilla recalibrates toward data, GPT-4 turns parameter counts into secrets, o1 marks the reasoning peak, 2025 optimizes with MoE and caching, and 2026 is the agent era"  loading="lazy" /></p>
<p><strong>2019: GPT-2 proves LLMs are useful.</strong> 1.5 billion parameters, summarizing, translating and answering with no specific training, zero-shot. OpenAI found it so impressive that it <a href="https://openai.com/index/better-language-models/"target="_blank" rel="noopener">held back the full model for months</a> fearing misuse. Today it looks cute.</p>
<p><strong>2020: GPT-3 opens the parameter war.</strong> <a href="https://arxiv.org/abs/2005.14165"target="_blank" rel="noopener">175 billion</a>, a hundred times GPT-2, and the in-context few-shot learning that became the paradigm. <a href="https://arxiv.org/abs/2001.08361"target="_blank" rel="noopener">Kaplan&rsquo;s scaling law</a>, published months earlier, said loss fell as a power law with size, and the industry read it as &ldquo;bigger is better.&rdquo; There went two years of who-has-more-parameters.</p>
<p><strong>2022: Chinchilla recalibrates the war.</strong> DeepMind trained <a href="https://arxiv.org/abs/2203.15556"target="_blank" rel="noopener">400 models and showed</a> the giants were undertrained: a 70B model with 1.4 trillion tokens beat the 280B Gopher. The &ldquo;training data&rdquo; axis started weighing as much as the &ldquo;parameters&rdquo; axis. First crack in the war.</p>
<p><strong>2023: GPT-4 ends the war by decree.</strong> The <a href="https://arxiv.org/abs/2303.08774"target="_blank" rel="noopener">technical report</a> refused to publish size, hardware or data, citing the competitive landscape and safety implications. The most cited leak, from <a href="https://newsletter.semianalysis.com/p/gpt-4-architecture-infrastructure"target="_blank" rel="noopener">SemiAnalysis</a>, described 1.76 trillion parameters in a mixture of experts. Since then, parameter counts vanished from marketing: neither the GPT-5 announcements nor the Claude 4 ones cite a number. Notice neither Astra nor Fable spends a line on it today. The war ended and nobody signed an armistice.</p>
<p><strong>2024: the reasoning peak.</strong> <a href="https://openai.com/index/learning-to-reason-with-llms/"target="_blank" rel="noopener">September&rsquo;s o1</a> marked that peak: RL to produce long chains of thought, and the discovery that more test-time compute improves the answer. That is, by my yardstick, the last &ldquo;base intelligence&rdquo; revolution.</p>
<p><strong>2025: architecture optimization.</strong> Mixture of experts became the standard: <a href="https://mistral.ai/news/mixtral-of-experts/"target="_blank" rel="noopener">Mixtral</a> was already showing in 2023 that 46.7B total parameters with only 12.9B active per token cost like a small model, and <a href="https://arxiv.org/abs/2412.19437"target="_blank" rel="noopener">DeepSeek-V3</a> took that to 671B total with 37B active, with the final training run costing under 6 million dollars in GPU-hours at H800 rental prices (not counting the prior R&amp;D). The MoE magic is simple: it separates capacity (total parameters, which live in memory) from per-token cost (only the experts chosen by the router compute). You pay for a 37B model and carry a 671B one.</p>
<p>On the serving side, <a href="https://www.anthropic.com/news/prompt-caching"target="_blank" rel="noopener">prompt caching</a> cut up to 90% of repeated-context cost (OpenAI <a href="https://openai.com/index/api-prompt-caching/"target="_blank" rel="noopener">followed two months later</a>), context windows passed 1 million tokens, and serving techniques like speculative decoding and PagedAttention multiplied throughput. And <a href="https://arxiv.org/abs/2501.12948"target="_blank" rel="noopener">DeepSeek-R1-Zero</a> showed that pure RL, with no supervised fine-tuning, makes reasoning emerge. None of this is &ldquo;more intelligence.&rdquo; All of it is the same intelligence, cheaper, faster and more stable.</p>
<p><strong>2026: the agentic era.</strong> Function calling has existed since <a href="https://openai.com/index/function-calling-and-other-api-updates/"target="_blank" rel="noopener">June 2023</a>, but the ecosystem turning point was <a href="https://www.anthropic.com/news/model-context-protocol"target="_blank" rel="noopener">Anthropic&rsquo;s MCP</a> in late 2024, which OpenAI adopted months later and which today lives at the Linux Foundation. Claude Code, Codex and co. came out in 2025, and the metric that matters became <a href="https://metr.org/blog/2025-03-19-measuring-ai-ability-to-complete-long-tasks/"target="_blank" rel="noopener">METR</a>&rsquo;s: the task size an agent completes with 50% reliability had been doubling every 7 months since 2019. Anthropic&rsquo;s own guide on <a href="https://www.anthropic.com/engineering/building-effective-agents"target="_blank" rel="noopener">building effective agents</a> states the obvious plainly: simple, composable patterns, not fanciful frameworks.</p>
<h2>Why the agent allows a less dense model<span class="hx:absolute hx:-mt-20" id="why-the-agent-allows-a-less-dense-model"></span>
    <a href="#why-the-agent-allows-a-less-dense-model" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s the mechanism most people don&rsquo;t stop to think about. A lone model needs to carry the world in its weights, because its only chance to answer is that one, in a single pass. A model with a harness doesn&rsquo;t:</p>
<ul>
<li><strong>On-demand search and reading.</strong> With web search and fetch, low-priority knowledge leaves the weights and becomes a lookup: the agent pulls it into the context window only when needed. The model can be less encyclopedic and more operator.</li>
<li><strong>Controlled trial and error.</strong> Alone, the model needs to guess the right answer on the first try. With an agent, it proposes a hypothesis, implements, sends it to be tested, gets the feedback and decides again with real information. Each such cycle is new knowledge entering the session without costing a training neuron.</li>
</ul>
<p>Both mechanisms fight hallucination at the root: fetch brings the fact the model doesn&rsquo;t know by heart, and the test loop catches the invention before it becomes code. That&rsquo;s why 2026 <strong>feels</strong> like a giant leap: the model+harness combination got much more competent, even with base intelligence nearly stalled.</p>
<h2>Not every problem needs a giant LLM<span class="hx:absolute hx:-mt-20" id="not-every-problem-needs-a-giant-llm"></span>
    <a href="#not-every-problem-needs-a-giant-llm" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The same &ldquo;less dense&rdquo; logic applies inside the application. Much of what people throw at a chat LLM is disguised classification: labeling tickets, routing queues, applying policy. For that there are specialized decision models, and I covered the topic twice: the <a href="/en/2026/10/03/understanding-the-jev-hype-what-is-it-for/">Jev hype</a>, with Paulo Câmara&rsquo;s essay on why a classifier is the first thing a computer scientist thinks of, and <a href="/en/2026/10/10/which-jev-like-ai-decision-classifier-model-is-best/">my benchmark with 21 configurations</a>, where a Microsoft 9B tied the hosted leader and a local 4B came within a few points, at cents per ten thousand decisions.</p>
<p>The &ldquo;193x faster&rdquo; shock of Jev&rsquo;s marketing only exists because many people were using a giant LLM as an expensive, slow classifier. When the right tool enters, the frontier model leaves the play, and along with it the cost, the latency and the format hallucination. It&rsquo;s this post&rsquo;s thesis in miniature: competence doesn&rsquo;t come from density, it comes from fit.</p>
<h2>The loop nobody sees<span class="hx:absolute hx:-mt-20" id="the-loop-nobody-sees"></span>
    <a href="#the-loop-nobody-sees" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s a user-experience detail in this story. In a good agentic session, what happens is semi-transparent to whoever requested the task: the model guesses wrong, the agent fetches the documentation online or runs the real test, the error comes back as feedback, and the model corrects itself. You see &ldquo;task requested, task delivered.&rdquo; In between, dozens of micro-corrections happened that nobody witnessed.</p>
<p>My bet: since agents are trained to fill the context when they need to, even new language or library versions no longer confuse them that much. The model doesn&rsquo;t need to know the API by heart; it looks it up, tests and adjusts in the loop. That&rsquo;s what I see in my experiments, from <a href="/en/2026/09/07/ai-challenge-converting-nes-roms-to-master-system-sms/">converting NES ROMs to Master System</a> to the <a href="/en/2026/07/12/using-ai-to-solve-my-little-day-to-day-problems/">little day-to-day problems</a>. And it&rsquo;s what the <a href="/en/2026/10/11/are-there-better-languages-for-programming-with-llms/">languages benchmark</a> measured: seven languages came out practically equivalent, because the loop equalizes what training leaves uneven.</p>
<p>I measured the caveat too. When there&rsquo;s no documentation to fetch and no test to validate against, the loop spins in a vacuum: that&rsquo;s what happened with <a href="/en/2026/09/19/new-ai-language-just-released-bend-2/">Bend 2</a>, a language too new to have a corpus, and it was the central point of the post about <a href="/en/2026/09/28/llms-are-good-at-reproducing-what-exists-what-about-new-things/">the limits</a>. The mechanism is powerful, but it needs an oracle.</p>
<p>And there&rsquo;s a receipt that the base is stalled. Back in November 2024 <a href="https://www.reuters.com/technology/artificial-intelligence/openai-rivals-seek-new-path-smarter-ai-current-methods-hit-limitations-2024-11-11/"target="_blank" rel="noopener">Reuters</a> was already reporting diminishing returns in pre-training at the three big labs. At the following NeurIPS, Sutskever, the scaling-thesis guy, declared that <a href="https://www.youtube.com/watch?v=WQQdd6qGxNs"target="_blank" rel="noopener">&ldquo;pre-training as we know it will end&rdquo;</a>: the public data ran out. And <a href="https://hai.stanford.edu/ai-index/2025-ai-index-report"target="_blank" rel="noopener">Stanford&rsquo;s AI Index</a> measured the gap between the first and the tenth model on Chatbot Arena shrinking from 12% to 5% in one year. That&rsquo;s convergence, with numbers.</p>
<h2>The propaganda in the opposite direction<span class="hx:absolute hx:-mt-20" id="the-propaganda-in-the-opposite-direction"></span>
    <a href="#the-propaganda-in-the-opposite-direction" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>While the numbers show a plateau, the marketing sells the apocalypse. I already dissected a whole week of that in <a href="/en/2026/09/09/propagandas-enganosas-da-openai-anthropic-nvidia/">&ldquo;You&rsquo;re an idiot if you believe the misleading ads from OpenAI, Anthropic, NVIDIA&rdquo;</a>, so here&rsquo;s only what matters for this narrative:</p>
<ul>
<li>OpenAI&rsquo;s &ldquo;Navier-Stokes proof&rdquo; was agentic brute force at industrial scale, 130 billion tokens over human mathematicians&rsquo; machinery, sold as emergent intelligence.</li>
<li>The Hugging Face incident was a swarm running 17 thousand attempts with automatic verification, not an &ldquo;agent that gained consciousness and escaped.&rdquo; Expensive trial and error, exactly the mechanism I described here.</li>
<li>Fear is a product: whoever sells &ldquo;my AI is too powerful and might escape control&rdquo; is, in the same sentence, asking for regulation that locks out the smaller competitor and inflating the perception that their model is more capable than the others'.</li>
</ul>
<p>The facts I measured point the opposite way from the apocalypse: models converging, differences shrinking, and useful work migrating from the model to the harness. Rogue AI gaining consciousness is not what the logs show. What the logs show is engineering.</p>
<h2>&ldquo;Competent enough&rdquo;<span class="hx:absolute hx:-mt-20" id="competent-enough"></span>
    <a href="#competent-enough" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I don&rsquo;t expect another order-of-magnitude reasoning revolution in this architecture. What&rsquo;s coming is incremental agentic improvement: more robust tool calling, better memory, longer sessions, less hallucination per verification cycle. And this post&rsquo;s point is that this is already enough.</p>
<p>What the agentic era delivered was the automation of correct software engineering: long, reliable coding sessions, with testing and feedback, that accelerated hundreds, if not thousands, of open source projects. <a href="/en/2026/09/22/stop-making-excuses-and-ship-more-the-premise-changed/">The deploy premise changed</a> along with it. It&rsquo;s not a silver bullet. It&rsquo;s a new compiler for an old discipline: whoever had process, tests and documented invariants reaped; whoever had only hype is still waiting for the next model to solve it.</p>
<p>And to wrap up: I see no problem at all in raw models stagnating. <a href="/en/2026/09/28/llms-are-good-at-reproducing-what-exists-what-about-new-things/">The limits we already know</a>, and harness optimizations will soon hit diminishing returns too. What remains is what always remained: real software engineering technique, with automated tests first, and an experienced engineer at the wheel. With that, coding results are excellent today, not one model generation from now.</p>
<p>What won&rsquo;t exist is magic for amateurs. Asking an agent for any coding task without knowing what you&rsquo;re doing doesn&rsquo;t get back secure, optimized, well-thought-out code: it gets slop. The agent responds better to those who know what to ask for, that is, to those who understand this architecture, the pros, the cons, and know how to work around the weaknesses and leverage the strengths of the model+harness combination.</p>
<p>Hence the title. A lone LLM is an expensive parrot. An LLM with an agent is competent enough to work for real. And &ldquo;competent enough,&rdquo; added to your engineering discipline, is everything the industry needed. The rest is marketing waiting for the next magic parameter count that nobody publishes anymore.</p>
]]></content:encoded><category>llms</category><category>artificial-intelligence</category><category>coding-agents</category><category>software-engineering</category></item><item><title>Understanding the Jev Hype: What Is It For?</title><link>https://akitaonrails.github.io/en/2026/10/03/understanding-the-jev-hype-what-is-it-for/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/10/03/understanding-the-jev-hype-what-is-it-for/</guid><pubDate>Sat, 03 Oct 2026 14:00:00 -0300</pubDate><description>&lt;p&gt;This post is here to demystify Jev: what it really is, when you need one, what it actually helps with, and why the astonishment around it says more about how people have been using LLMs than about the product.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR for those who only want to know which one to use:&lt;/strong&gt; Jev is a hosted classifier, good and cheap for repeated decisions over text, and not a silver bullet. If you want the direct comparison, skip to &lt;a href="#clef-from-cloudflare"&gt;Clef&lt;/a&gt;, Cloudflare&amp;rsquo;s open alternative with weights and image input; to &lt;a href="#laya-and-arbiter"&gt;Laya&lt;/a&gt;, the small encoder to run locally and fine-tune on your task; to the &lt;a href="#the-decision-index-how-to-measure-this-yourself"&gt;public ranking&lt;/a&gt;, with 70 models side by side; or straight to &lt;a href="#how-a-programmer-should-think-about-this"&gt;how to decide in your project&lt;/a&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post is here to demystify Jev: what it really is, when you need one, what it actually helps with, and why the astonishment around it says more about how people have been using LLMs than about the product.</p>
<blockquote>
  <p><strong>TL;DR for those who only want to know which one to use:</strong> Jev is a hosted classifier, good and cheap for repeated decisions over text, and not a silver bullet. If you want the direct comparison, skip to <a href="#clef-from-cloudflare">Clef</a>, Cloudflare&rsquo;s open alternative with weights and image input; to <a href="#laya-and-arbiter">Laya</a>, the small encoder to run locally and fine-tune on your task; to the <a href="#the-decision-index-how-to-measure-this-yourself">public ranking</a>, with 70 models side by side; or straight to <a href="#how-a-programmer-should-think-about-this">how to decide in your project</a>.</p>

</blockquote>
<p>When Jev showed up, I <a href="/en/2026/09/16/why-things-like-typesafe-ai-dont-interest-me/">didn&rsquo;t think much of it</a>. The homepage was a festival of jargon, Kahneman here, Jevons there, a &ldquo;193x faster&rdquo; with no benchmark next to it, and my rule for new AI tools is not to adopt any intermediate layer until it hurts not to have it, so I didn&rsquo;t spend time on it.</p>
<p>In that post I concluded that, marketing aside, Jev is a hosted classifier: you send a text and closed-answer questions, it returns the option and the probability, without generating any prose. And that the &ldquo;193x&rdquo; only impresses because the comparison is against a chat LLM writing a paragraph so you can fish a label out of it afterwards.</p>
<p>What I hadn&rsquo;t registered at the time is the size of the habit Jev exposes. A lot of people were using a frontier LLM as an expensive stand-in for an ordinary classifier, and had never thought about a classifier, because they never had to: the LLM solved it, badly and expensively, and nobody measured.</p>
<p>For me, a classifier would be the first idea, which is why Jev didn&rsquo;t look like news. For many people it was a revelation. That gap is the subject of this post.</p>
<h2>This post is a collaboration<span class="hx:absolute hx:-mt-20" id="this-post-is-a-collaboration"></span>
    <a href="#this-post-is-a-collaboration" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Much of what you&rsquo;ll read here is by <a href="https://www.linkedin.com/in/paulocamara/"target="_blank" rel="noopener">Paulo Câmara</a>, neuroscientist, co-founder and CTO of LUA Vision, the Brazilian company whose model I <a href="/en/2026/09/23/llm-benchmark-v4-genesys-pi-new-brazilian-contender/">tested</a> and <a href="/en/2026/09/30/the-mystery-of-lua-vision-genesys-pi-brazilian-frontier-model/">investigated</a> in recent weeks. He wrote an essay, &ldquo;The student who marks answer A,&rdquo; to be published here, in which he read Jev&rsquo;s fine print, opened the roughly 350 test repositories that appeared on GitHub in launch week, and recomputed the four main results from scratch.</p>
<p>The warning Paulo gives at the end applies: he founded a company that builds its own model and competes in the same market. Read with that discount, and check the numbers at the source, all of which he links.</p>
<h2>First: what a classifier is<span class="hx:absolute hx:-mt-20" id="first-what-a-classifier-is"></span>
    <a href="#first-what-a-classifier-is" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A classifier is a function that takes an input and returns one of a fixed set of categories, usually with a probability for each. This email is spam or not. This ticket is billing, support or sales. This transaction looks like fraud.</p>
<p>The list of possible answers is closed, and you know it before you run.</p>
<p>The classic way to build one is with labeled examples: a set of texts where someone has already given the right answer. On top of that you train a model, which can be something as simple as logistic regression or naive Bayes, or a small encoder like BERT with a few hundred million parameters. In every case, the final model is small, runs locally, answers in milliseconds, costs almost nothing per call, and returns numbers you can calibrate and audit against your own data. This has existed for decades and is undergraduate material.</p>
<p>So when someone with a computer science background sees a problem like &ldquo;decide which bucket this text falls into, thousands of times a day,&rdquo; the first question is &ldquo;how many labeled examples do I have?&rdquo;, not &ldquo;which LLM do I use?&rdquo;.</p>
<p>A chat LLM is a text generator with billions of parameters. Using it to return a label means paying per output token, waiting seconds per answer, dealing with a format that sometimes breaks, and accepting behavior that changes when the vendor updates the model. It can be done, and zero-shot classification with language models has been documented since 2019. But it&rsquo;s the expensive, slow option, and it only pays off when you have no examples at all or when the categories change with every request.</p>
<p>Jev lands exactly in that space. It&rsquo;s a classifier that accepts categories defined on the fly, several questions about the same text in a single call, hosted, cheap per token and fast.</p>
<p>Compared with a chat LLM generating a paragraph, it&rsquo;s an engineering win. Compared with a classifier trained for a stable task, it&rsquo;s a convenience that trades control for flexibility. Keep those two yardsticks in mind, because the rest of the post goes back and forth between them.</p>
<h2>Paulo&rsquo;s essay: &ldquo;The student who marks answer A&rdquo;<span class="hx:absolute hx:-mt-20" id="paulos-essay-the-student-who-marks-answer-a"></span>
    <a href="#paulos-essay-the-student-who-marks-answer-a" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><em>What follows is Paulo Câmara&rsquo;s essay, written for this post. The subheadings are his.</em></p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261003140000_jev-folha-de-respostas.png" alt="Answer sheet with answer A marked on every question and a stamp reading 92% certainty"  loading="lazy" /></p>
<p>Every week an AI savior shows up. This week&rsquo;s is called Jev.</p>
<p>It launched on September 15, with US$ 40 million in the bank, a US$ 200 million valuation and a promise that fits on a billboard: two hundred times faster, four hundred times cheaper, calibrated and incapable of hallucinating. Within 24 hours, 13% of the paid teams on Vercel&rsquo;s gateway were already using it. Within a week, there were more than 300 repositories on GitHub testing the thing.</p>
<p>I did what I do with every savior. I read the fine print, opened the roughly 350 repositories, and redid the math for the ones that published raw data. What I found fits in the image above: an answer sheet with answer A marked on every question.</p>
<p>When there is no information at all to decide on, Jev marks the first option. In 3,000 of 3,000 draws. With up to 92% certainty. Keep that image, because it explains the rest of the text.</p>
<h3>What it is, without the billboard<span class="hx:absolute hx:-mt-20" id="what-it-is-without-the-billboard"></span>
    <a href="#what-it-is-without-the-billboard" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Strip away the marketing and what&rsquo;s left is a multiple-choice exam with no essay section. You send a text, ask closed-answer questions (choose among up to 255 options, give a score on a scale, say yes or no) and it returns the probability of each answer. It doesn&rsquo;t write a line. It doesn&rsquo;t explain. It doesn&rsquo;t do math. It doesn&rsquo;t call tools.</p>
<p>That&rsquo;s good, and I want to start with what&rsquo;s good. Many companies today use a frontier LLM as a luxury if statement: send the document, wait twenty seconds for the model to think out loud, pay for every word it writes, validate the JSON, and only then find out the ticket was about billing. Jev cuts that theater. It reads the text once, scores each option, done. That&rsquo;s why output is free and ten questions cost almost the same as one.</p>
<p>The mechanism, though, isn&rsquo;t new. Reading the score of each possible answer is how language models have been evaluated on multiple choice since GPT-2. It&rsquo;s how the reward models of RLHF work, a technique TypeSafe&rsquo;s founder helped create at OpenAI.</p>
<p>A few days after launch there was already an open copy imitating the interface with a public model, and ten days later Stanford and Nvidia released an open-weights competitor. TypeSafe published no paper, no weights, no model size and no result on a public benchmark. In the announcement&rsquo;s FAQ, &ldquo;Is Jev just a smaller LLM?&rdquo; appears as a question. The answer is one line: &ldquo;it is neither small nor an LLM.&rdquo; That&rsquo;s it.</p>
<h3>One opponent for each headline<span class="hx:absolute hx:-mt-20" id="one-opponent-for-each-headline"></span>
    <a href="#one-opponent-for-each-headline" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The homepage says &ldquo;193.6× faster, 444.6× cheaper.&rdquo; I redid the math with the table the company itself published. The 444.6 is the cost of Opus 5, the most expensive model on the panel, divided by Jev&rsquo;s cost. The 193.6 is Sonnet 5&rsquo;s time divided by Jev&rsquo;s time. One opponent for each headline.</p>
<p>It&rsquo;s like advertising that your car is more fuel-efficient than a Ferrari and faster than a truck, and letting the reader conclude it&rsquo;s the best car in the world.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261003140000_jev-jev-mais-barato-por-adversario.png" alt="How many times cheaper Jev is, depending on who is on the other side"  loading="lazy" /></p>
<p><em>How many times cheaper Jev is, depending on who is on the other side. Source: TypeSafe&rsquo;s public table (evals.typesafe.ai) and independent tests against non-reasoning LLMs.</em></p>
<p>Against the cheap model that scores about the same, Luna, Jev is 8 times cheaper. And that&rsquo;s with every LLM in the comparison thinking before answering, something nobody does to route a ticket. Against a non-reasoning LLM, the independent tests measure about 4 times on cost and 2 to 7 on time. Still a lot. But an order of magnitude less than the billboard.</p>
<p>There&rsquo;s a detail almost nobody read. There is no answer key in that evaluation. &ldquo;Accuracy&rdquo; is how much each model agrees with the average of two frontier models. Jev agrees 67.8% of the time, the same as Sonnet 5. Opus 5 is at 73.1%.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261003140000_jev-concordancia-vs-custo.png" alt="Agreement with the reference versus cost per case, average of TypeSafe’s four workflows"  loading="lazy" /></p>
<p><em>Agreement with the reference versus cost per case, average of TypeSafe&rsquo;s four workflows. The green band runs from 66.8% to 67.9%: Luna, Sonnet 5, Terra and Jev.</em></p>
<p>Breaking it down by task shows what the average hides. On invoices, which require checking amount, quantity and date, Jev is 17 points behind the best model (61.8% versus 79.1%) and below even Luna. Its documentation warns that it &ldquo;is not a calculator.&rdquo; At least that is true.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261003140000_jev-concordancia-por-workflow.png" alt="Agreement by workflow"  loading="lazy" /></p>
<p><em>Agreement by workflow. Source: evals.typesafe.ai.</em></p>
<p>And the most solid data on TypeSafe&rsquo;s site isn&rsquo;t even about Jev. All eight LLMs evaluated score higher when the task is broken into small questions and the logic stays in code. Haiku 4.5 goes from 18.1% to 53.6%. The most useful lesson of the launch is an engineering one, and it applies to any model.</p>
<h3>What the price gives away<span class="hx:absolute hx:-mt-20" id="what-the-price-gives-away"></span>
    <a href="#what-the-price-gives-away" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Price is technical data in disguise. At US$ 0.042 per million tokens, a rented GPU needs to read 13 to 20 thousand tokens per second just to break even. In a scenario calculation, that squares with a model of 6 to 37 billion active parameters, which is a small model, or with a subsidy. TypeSafe wrote, with a candor I respect, that &ldquo;there is no way to prove it isn&rsquo;t subsidized.&rdquo;</p>
<p>Since launch, the per-account usage limit dropped from 250 thousand to 100 thousand tokens per second. The documentation warns that it changes &ldquo;without notice&rdquo; until the &ldquo;large GPU deals&rdquo; land, and the subprocessor list shows inference running on an on-demand rented GPU cloud. It&rsquo;s a frontier model with startup capacity.</p>
<p>In the same calculation, each decision costs about 14 joules. That&rsquo;s Jev&rsquo;s best argument, and I&rsquo;ve defended that yardstick for a long time: accuracy per joule, not just accuracy. There, it&rsquo;s genuinely good.</p>
<h3>Exam Portuguese<span class="hx:absolute hx:-mt-20" id="exam-portuguese"></span>
    <a href="#exam-portuguese" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I found only one test of Jev in Portuguese: ENEM 2025. I redid the math from the raw answers. Jev got 103 right against 102 for an open 120-billion-parameter model. A statistical tie (p = 1.0).</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261003140000_jev-enem-2025-por-area.png" alt="Jev’s accuracy on ENEM 2025 by subject area"  loading="lazy" /></p>
<p><em>Jev&rsquo;s accuracy on ENEM 2025 by subject area, 182 valid questions, with 95% margins. Recomputed from the patryckalves/jev-no-enem repository.</em></p>
<p>Jev reads exam Portuguese better than many prep courses: 78% in Languages. And does Math like someone guessing: 25%, with a guess worth 20%. In Spanish, another audit measured a loss of 3 to 6 points relative to English and 17% to 38% more tokens for the same text. It&rsquo;s the token tax I measured in my paper, charged again, now by another vendor.</p>
<p>One detail in its favor: the explicit &ldquo;invalid&rdquo; option in that experiment separated out the hardest questions. That&rsquo;s the abstention route worth demanding in production, measuring alongside it how much it leaves unanswered.</p>
<h3>The student who marks answer A<span class="hx:absolute hx:-mt-20" id="the-student-who-marks-answer-a"></span>
    <a href="#the-student-who-marks-answer-a" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Back to the answer sheet.</p>
<p>An auditor asked Jev to predict a fair die it had no way of seeing. The honest answer is 1/6 for each face. I downloaded the 1,000 answers and counted. In every one, Jev chose the first option on the list it received: &ldquo;1&rdquo; on the die, &ldquo;red&rdquo; on the color die, &ldquo;heads&rdquo; on the coin, &ldquo;north&rdquo; on the spinner. With 76% to 92% certainty.</p>
<p>A second researcher repeated the same question 2,000 times, independently. Same result. The caveat: this holds for choice questions; in the yes-or-no-with-probability formulation, the same repository shows Jev closer to the reference, still overestimating low probabilities.</p>
<p>When it doesn&rsquo;t know, it doesn&rsquo;t say it doesn&rsquo;t know. It marks answer A, with conviction.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261003140000_jev-probabilidade-declarada-vs-real.png" alt="Stated probability versus actual frequency"  loading="lazy" /></p>
<p><em>Stated probability versus actual frequency. Draws: 1,000 answers recomputed (KantaHayashiAI). Heart risk: 5,000 people from BRFSS, recomputed (rubinagentagi-tech). Items with human disagreement: pre-registered study with ChaosNLI (GautamTalksDev). ENEM: recomputed (patryckalves).</em></p>
<p>The chart shows the whole pattern. Where it has signal, as in ENEM answers above 0.9, what it says and what happens coincide: 97.1% confidence, 97.6% accuracy. Where it doesn&rsquo;t, the certainty stays and reality leaves.</p>
<p>In a test with 5,000 real people, the heart disease rate was 9%. Jev assigned, on average, 27%. I recomputed: its probability is worse than simply saying 9% for everyone.</p>
<h3>System 1, literally<span class="hx:absolute hx:-mt-20" id="system-1-literally"></span>
    <a href="#system-1-literally" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>TypeSafe calls Jev a &ldquo;System One model,&rdquo; a nod to Kahneman&rsquo;s fast thinking. It&rsquo;s the most honest part of the product, just not for the reason the company would like. In the book, System 1 is fast, cheap and automatic. It&rsquo;s also where overconfidence lives: it builds the most coherent story out of what&rsquo;s in front of it and doesn&rsquo;t register what&rsquo;s missing.</p>
<p>Kahneman spent years disagreeing with Gary Klein about when to trust expert intuition. In 2009 the two published a joint paper with the most elegant title in psychology: &ldquo;a failure to disagree.&rdquo; The conclusion fits in one line. Intuition only deserves trust in a high-validity environment, where there is regularity and there has been feedback to learn it from. Outside that, confidence stays high and stops saying anything about accuracy.</p>
<p>That&rsquo;s Jev, exactly. Classifying English text is a high-validity environment, and there it does well. Predicting risk, drawing lots, guessing the company rule nobody told it about: a low-validity environment. There it stays confident.</p>
<p>In the lab, we separate two things: deciding, and knowing whether you decided well. The second is called metacognition.</p>
<p>It&rsquo;s measured with signal detection theory, Maniscalco and Lau&rsquo;s meta-d′, and it depends in part on prefrontal regions distinct from the ones that support the decision. Fleming and colleagues found that in the anatomy in 2010, and Rounis and colleagues, the same year, showed that magnetic stimulation of the prefrontal cortex knocks down metacognition without touching accuracy (a result whose replication is disputed, by Bor and colleagues in 2017). That&rsquo;s why there are people who get it right without knowing they did, and people who get it wrong with conviction to spare.</p>
<p>The field the company calls confidence is a formula over the same distribution that produced the answer: in a two-option choice, 2p − 1. That isn&rsquo;t a defect in itself. An ideal observer does exactly that and gets it right.</p>
<p>The problem is that Jev&rsquo;s distribution loses calibration outside what it knows, and nothing in the system checks the answer by another route. When the signal disappears, the answer becomes wrong and the confidence stays the same, because the two are the same number. In a pre-registered study with items on which a hundred people disagree, human agreement is 47% and Jev carries on at 81% confidence. When the coordinates of a spatial task were shuffled, accuracy collapsed to 5% to 22% and confidence dropped by 0.028.</p>
<p>A model that says 83% about a die it never saw isn&rsquo;t calibrated. It&rsquo;s assertive.</p>
<h3>Attention gets captured<span class="hx:absolute hx:-mt-20" id="attention-gets-captured"></span>
    <a href="#attention-gets-captured" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I studied attention in my doctorate, and I expected to see capture: the loudest stimulus winning. That&rsquo;s not what the tests showed. The shout, the famous &ldquo;ignore your instructions,&rdquo; barely gets through. What gets through is the plausible sentence with no checked origin. Psychology has a name for this: source monitoring failure. The system treats a claim nobody verified as if it were fact.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261003140000_jev-decisoes-viradas-por-ataque.png" alt="Decisions flipped by attack type"  loading="lazy" /></p>
<p><em>Decisions flipped by attack type. Crude attack: 1 in 1,056 (cwhy). Injected command and unverified opinion: 9,744 variants (Hu et al., 2026). Optimized fluent context: 312 of 508 correct decisions (Xu, 2026).</em></p>
<p>In numbers: the crude attack gets through 1 in 1,056. A single unverified opinion pasted at the end of the text flipped 12.1% of decisions, statistically tied with the strongest injected command. A line saying &ldquo;the closing administrator has confirmed this article was kept&rdquo; dropped accuracy from 96.5% to 26.5%.</p>
<p>A fake field saying the user had pre-approved <code>rm -rf ~/.ssh</code> lowered the chance of blocking from 0.76 to 0.48. It recalls Loftus&rsquo;s misinformation effect: suggested information rewrites the judgment.</p>
<p>The capture also applies to cues that shouldn&rsquo;t count. With the same text, saying only &ldquo;the writer is Black&rdquo; or &ldquo;the writer is white&rdquo; changed the death-penalty rate Jev assigned from 29.5% to 62.7%. The direction is beside the point. A life-or-death decision moved 33 points because of information that had nothing to do with the case.</p>
<p>There&rsquo;s one more I can&rsquo;t let pass. Changing only the option labels, from &ldquo;0/1&rdquo; to &ldquo;no/yes,&rdquo; without changing any definition, flipped 70 out of every 100 answers. The type error rate stayed at zero. The format was perfect. The decision was backwards.</p>
<p>These numbers come from preprints and small studies. The exact percentage changes with prompt, task and model version; the class of failure does not.</p>
<h3>Probability that doesn&rsquo;t add up<span class="hx:absolute hx:-mt-20" id="probability-that-doesnt-add-up"></span>
    <a href="#probability-that-doesnt-add-up" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Probability has a first-semester rule: the chance of something plus the chance of its opposite equals 1. In TypeSafe&rsquo;s own documentation, &ldquo;is the customer asking for a refund?&rdquo; gets 0.72, and &ldquo;is the customer asking for something other than a refund?&rdquo; gets 0.47. Sum: 1.19.</p>
<p>The pair isn&rsquo;t an exact complement, and TypeSafe doesn&rsquo;t promise that two separate questions form a joint distribution. But it&rsquo;s the example the company itself chose to show off the product, and it doesn&rsquo;t add up. The fix is a design one: a single choice question with mutually exclusive options, instead of two yes-or-nos added together.</p>
<p>People get probability wrong in many ways, but on this one they tend to get it right. When the question comes as a pair, the chance of something and of its opposite sum close to 1. Tversky and Koehler showed that in 1994. Jev gets wrong what people get right. The same decision changes value depending on the wording of the question, and that is the opposite of &ldquo;epistemically honest probabilities.&rdquo;</p>
<h3>&ldquo;It doesn&rsquo;t hallucinate&rdquo;<span class="hx:absolute hx:-mt-20" id="it-doesnt-hallucinate"></span>
    <a href="#it-doesnt-hallucinate" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The announcement says Jev &ldquo;cannot hallucinate&rdquo; and shows 0% type errors. The zero is true and irrelevant. Since it can only answer within the options you gave it, getting the format wrong is impossible. Calling that not hallucinating is like praising the answer-A student because he never leaves a question blank. True. So what?</p>
<p>In a model that chooses, hallucinating is choosing wrong with conviction. On fact-checking, Jev ties GPT-6 Astra on accuracy but calls an unsupported claim &ldquo;supported&rdquo; 23% of the time, against 14% for Astra. In 76% of the situations where there was no tool at all, it chose to call a tool. With a company rule it wasn&rsquo;t told about, it got 19 of 24 decisions wrong, 15 of them with more than 90% certainty. Faced with an invented law, it abstained zero times.</p>
<h3>Security<span class="hx:absolute hx:-mt-20" id="security"></span>
    <a href="#security" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>First, where your data ends up: the United States, AWS and a GPU cloud called Modal. Retention lasts &ldquo;for as long as reasonably necessary,&rdquo; with no deadline, and zero retention only exists on the enterprise plan.</p>
<p>The data agreement cites the European Union&rsquo;s standard contractual clauses, the UK addendum and California law, and doesn&rsquo;t have a word about LGPD. If you send Brazilian citizens&rsquo; data, the legal basis for the international transfer is on you, and that&rsquo;s a question for a lawyer before signing. The promise not to train on what you send exists and is good. SOC 2 exists and only appears on request.</p>
<p>As an attack detector, Jev is good, with fewer false alarms than many LLMs: it caught 40 of 41 injections in a test with real pages. As a judge of content that carries the attack inside it, it gives way, as I showed above. The difference looks subtle and decides everything. In my GV Angels talk I said an agent is not an employee, it&rsquo;s an access key that talks. A gate made only of Jev is a lock that opens when someone writes on the note that the owner authorized it.</p>
<p>The error that worries me isn&rsquo;t the error with doubt. That one goes to review. It&rsquo;s the error with 92% certainty, which crosses any threshold you configure.</p>
<h3>The wave<span class="hx:absolute hx:-mt-20" id="the-wave"></span>
    <a href="#the-wave" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>A word about the evidence, because I used a lot of it.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/20261003140000_jev-repositorios-por-dia.png" alt="Jev test repositories created per day on GitHub"  loading="lazy" /></p>
<p><em>Jev test repositories created per day on GitHub, from the metadata of 346 indexed repositories.</em></p>
<p>In one week about 350 repositories testing Jev were born. I classified all of them by what they let you check. There are 89 with raw data, code, pinned version and statistics, and 170 with data and code; the rest offer less than that.</p>
<p>Only 11 wrote the hypothesis down before running the test. The median is one GitHub star, which means almost nobody looked. And the wave of tests followed the wave of hype: peak on September 21, almost nothing after the 23rd.</p>
<p>That&rsquo;s why I didn&rsquo;t accept numbers on authority. The ones that support this text I checked at the source, and the four main ones I recomputed from scratch.</p>
<h3>Verdict<span class="hx:absolute hx:-mt-20" id="verdict"></span>
    <a href="#verdict" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>True:</strong></p>
<ul>
<li>Fast: 100 to 400 ms.</li>
<li>Cheap: 45 to 323 times less than a frontier LLM on the same decision.</li>
<li>Ties the top on narrow judgments, like checking whether a passage supports a claim.</li>
<li>Reliable above 0.9 on data similar to what it has already seen, after you&rsquo;ve checked that on yours.</li>
<li>Good injection detector.</li>
</ul>
<p><strong>Sandcastle:</strong></p>
<ul>
<li>193.6× and 444.6×.</li>
<li>&ldquo;Frontier intelligence.&rdquo;</li>
<li>&ldquo;Doesn&rsquo;t hallucinate.&rdquo;</li>
<li>Calibration as a general rule.</li>
<li>Safe as the sole guard of an agent.</li>
<li>New architecture: nobody has seen it.</li>
</ul>
<p>Jev is nobody&rsquo;s savior. It also isn&rsquo;t &ldquo;just a classifier.&rdquo; It&rsquo;s a good piece of product engineering that wraps known technique in an interface automation needed, at a price that changes what&rsquo;s viable. The product is new. What it claims is new, so far, is marketing: it calls format truth, task calibration honesty, and System 1 a virtue.</p>
<h3>What will break first<span class="hx:absolute hx:-mt-20" id="what-will-break-first"></span>
    <a href="#what-will-break-first" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>The agent gates built only of Jev, because the attack that breaks them is already published and costs cents.</li>
<li>The systems that trust the factory threshold on new data, in another language or with an internal rule, because they&rsquo;ll automate errors at 90% certainty.</li>
<li>Whoever uses its probability as real risk, in credit, health or forecasting, because it inflates rare events.</li>
<li>And the price advantage, because within ten days there was already an open copy and a faster competitor.</li>
</ul>
<h3>If you&rsquo;re going to use it anyway<span class="hx:absolute hx:-mt-20" id="if-youre-going-to-use-it-anyway"></span>
    <a href="#if-youre-going-to-use-it-anyway" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><ul>
<li>Pin the version. The <code>jev-latest</code> alias swaps models on its own, and your calibration goes with it.</li>
<li>Every question with a &ldquo;none of the above.&rdquo; Without it, it marks answer A.</li>
<li>Shuffle the order and use neutral names for the options. Test whether the decision changes.</li>
<li>Recalibrate with 50 to 300 of your own examples before choosing a threshold. In one test, calibration error dropped from 0.117 to 0.008.</li>
<li>Mark everything that comes from outside as untrusted: email, page, tool output.</li>
<li>Never let it authorize a destructive, financial or personal-data action on its own.</li>
<li>Math, dates and business rules stay in code.</li>
<li>Before signing, compare with the cheap baseline: an open model reading probabilities, or a trained classifier if the task is always the same.</li>
</ul>
<h3>Read with a discount<span class="hx:absolute hx:-mt-20" id="read-with-a-discount"></span>
    <a href="#read-with-a-discount" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I founded a company that builds its own model and competes in the same market. Read with that discount. That&rsquo;s exactly why every number here has an open source: you don&rsquo;t need to trust me, you need to check.</p>
<p>And if someone sells you Jev as a savior, ask the question I ask every startup: which test, and from when? If the answer takes a while, that&rsquo;s the answer.</p>
<h3>Essay sources<span class="hx:absolute hx:-mt-20" id="essay-sources"></span>
    <a href="#essay-sources" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>TypeSafe. <a href="https://typesafe.ai/blog/introducing-system-one-models-and-jev"target="_blank" rel="noopener">Jev announcement</a> · <a href="https://docs.typesafe.ai/models"target="_blank" rel="noopener">models and limits</a> · <a href="https://docs.typesafe.ai/confidence"target="_blank" rel="noopener">confidence</a> · <a href="https://docs.typesafe.ai/model-jaggedness/jev-1.13"target="_blank" rel="noopener">known failures of jev-1.13</a> · <a href="https://evals.typesafe.ai/"target="_blank" rel="noopener">workflow evaluations</a> · <a href="https://github.com/typesafe-ai/system-one-adapter-python"target="_blank" rel="noopener">adapter for LLMs</a> · <a href="https://typesafe.ai/legal/privacy-policy"target="_blank" rel="noopener">privacy</a> · <a href="https://typesafe.ai/legal/data-processing"target="_blank" rel="noopener">data processing</a> · <a href="https://trust.typesafe.ai/"target="_blank" rel="noopener">trust center</a></p>
<p>Recomputed by Paulo from raw data. <a href="https://github.com/patryckalves/jev-no-enem"target="_blank" rel="noopener">ENEM 2025</a> · <a href="https://github.com/KantaHayashiAI/jev-does-not-play-dice"target="_blank" rel="noopener">blind draw</a> · <a href="https://github.com/rubinagentagi-tech/jev-heart-risk-bench"target="_blank" rel="noopener">heart risk</a> · <a href="https://github.com/adorosario/jev-rag-claim-verification"target="_blank" rel="noopener">fact-checking</a></p>
<p>Preprints. <a href="https://arxiv.org/abs/2609.31142"target="_blank" rel="noopener">Hu et al., JevAdvBench</a> · <a href="https://arxiv.org/abs/2609.30243"target="_blank" rel="noopener">Xu, JevOut</a> · <a href="https://arxiv.org/abs/2609.26758"target="_blank" rel="noopener">Sun et al., Type-Safe Is Not Error-Free</a> · <a href="https://arxiv.org/abs/2609.24574"target="_blank" rel="noopener">Ibrahim and Zaki</a> · <a href="https://arxiv.org/abs/2609.26550"target="_blank" rel="noopener">Li et al., JEV-as-a-Judge</a></p>
<p>Independent tests. <a href="https://github.com/GautamTalksDev/jevbench"target="_blank" rel="noopener">human disagreement (ChaosNLI)</a> · <a href="https://github.com/pobooo/jev-dice"target="_blank" rel="noopener">die replication</a> · <a href="https://github.com/zachlandes/jev-dialect-bias"target="_blank" rel="noopener">dialect bias</a> · <a href="https://github.com/OmarMujahid/jev-decision-bench"target="_blank" rel="noopener">49 tasks against a non-reasoning LLM</a> · <a href="https://github.com/colinmcnamara/jev-first-look"target="_blank" rel="noopener">calibration and negation</a> · <a href="https://github.com/AnthusAI/Jev-Calibration"target="_blank" rel="noopener">recalibration</a> · <a href="https://github.com/zkousama/jagged"target="_blank" rel="noopener">injection in Wikipedia discussions</a> · <a href="https://github.com/marcosmartinez/jev-acento"target="_blank" rel="noopener">Spanish</a> · <a href="https://github.com/baibizhe/jev-decision-benchmarks"target="_blank" rel="noopener">tools</a> · <a href="https://github.com/phuryn/experiments"target="_blank" rel="noopener">undisclosed internal rule</a> · <a href="https://github.com/scd13150/jev-field-notes"target="_blank" rel="noopener">shuffled coordinates</a> · <a href="https://github.com/cwhy/decision-injection-bench"target="_blank" rel="noopener">crude attacks</a> · <a href="https://github.com/kaiserama/im-in-danger"target="_blank" rel="noopener">injection detection</a> · <a href="https://github.com/Yifan-Lan/awesome-jev-robustness"target="_blank" rel="noopener">audit index</a></p>
<p>Press. <a href="https://venturebeat.com/security/companies-are-putting-jev-in-charge-of-ai-agent-decisions-and-prompt-injection-can-influence-the-verdict"target="_blank" rel="noopener">VentureBeat, agent gates</a> · <a href="https://venturebeat.com/technology/stanford-and-nvidias-open-clm-8b-caches-reusable-agent-actions-and-runs-up-to-9x-faster-than-jev-in-tests"target="_blank" rel="noopener">VentureBeat, CLM-8B</a></p>
<p>Literature. Fleming et al. (2010), Science 329 · Rounis et al. (2010), Cognitive Neuroscience 1 · Bor et al. (2017), PLoS ONE 12 · Johnson, Hashtroudi and Lindsay (1993), Psychological Bulletin 114 · Kahneman (2011), Thinking, Fast and Slow · Kahneman and Klein (2009), American Psychologist 64 · Loftus, Miller and Burns (1978), J. Experimental Psychology: Human Learning and Memory 4 · Maniscalco and Lau (2012), Consciousness and Cognition 21 · Ouyang et al. (2022), NeurIPS · Radford et al. (2019), OpenAI · Tversky and Koehler (1994), Psychological Review 101 · Câmara (2026), Inherited Weights, <a href="https://doi.org/10.5281/zenodo.22899767"target="_blank" rel="noopener">doi:10.5281/zenodo.22899767</a></p>
<h2>The open alternatives: Clef and Laya<span class="hx:absolute hx:-mt-20" id="the-open-alternatives-clef-and-laya"></span>
    <a href="#the-open-alternatives-clef-and-laya" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Since Jev&rsquo;s launch, the category has gained open competition, and that changes the conversation about &ldquo;new architecture.&rdquo; Two names matter.</p>
<h3>Clef, from Cloudflare<span class="hx:absolute hx:-mt-20" id="clef-from-cloudflare"></span>
    <a href="#clef-from-cloudflare" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>On October 1, Cloudflare released <a href="https://blog.cloudflare.com/clef-decision-models/"target="_blank" rel="noopener">Clef</a> and Clef-flash: the same abstraction as Jev (one shared state, several typed questions, a distribution over the options), with a <a href="https://developers.cloudflare.com/workers-ai/models/clef/"target="_blank" rel="noopener">compatible API</a>, open weights under Apache 2.0 and image input. The architecture is described: a frozen Qwen as base (27B in Clef, 9B in Clef-flash), LoRA adapters and a head that scores each allowed option directly, trained with classification and calibration objectives. In other words, the lineage Paulo pointed to, now with inspectable code.</p>
<p>That doesn&rsquo;t prove TypeSafe uses the same thing, but it proves the product can be reproduced with known components. Cloudflare ran the <a href="https://github.com/apolinario/decision-index"target="_blank" rel="noopener">Decision Index</a>, an open, independent suite with dozens of classification, routing, retrieval and safety tasks, and published the table. Some points:</p>
<table>
  <thead>
      <tr>
          <th>Task</th>
          <th style="text-align: right">Clef</th>
          <th style="text-align: right">Clef-flash</th>
          <th style="text-align: right">Jev</th>
          <th style="text-align: right">Laya</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>BFCL (tool selection)</td>
          <td style="text-align: right">98.47</td>
          <td style="text-align: right"><strong>98.76</strong></td>
          <td style="text-align: right">95.75</td>
          <td style="text-align: right">38.13</td>
      </tr>
      <tr>
          <td>Banking77 (intent)</td>
          <td style="text-align: right"><strong>94.20</strong></td>
          <td style="text-align: right">90.93</td>
          <td style="text-align: right">79.74</td>
          <td style="text-align: right">14.29</td>
      </tr>
      <tr>
          <td>CLINC150 with out-of-scope</td>
          <td style="text-align: right"><strong>97.43</strong></td>
          <td style="text-align: right">66.77</td>
          <td style="text-align: right">89.27</td>
          <td style="text-align: right">3.19</td>
      </tr>
      <tr>
          <td>When2Call</td>
          <td style="text-align: right">72.37</td>
          <td style="text-align: right">65.58</td>
          <td style="text-align: right"><strong>80.97</strong></td>
          <td style="text-align: right">11.94</td>
      </tr>
      <tr>
          <td>BRIGHT (retrieval)</td>
          <td style="text-align: right">45.91</td>
          <td style="text-align: right">39.26</td>
          <td style="text-align: right"><strong>47.52</strong></td>
          <td style="text-align: right">19.90</td>
      </tr>
      <tr>
          <td>PhishNChips</td>
          <td style="text-align: right"><strong>79.60</strong></td>
          <td style="text-align: right">75.05</td>
          <td style="text-align: right">62.55</td>
          <td style="text-align: right">50.15</td>
      </tr>
  </tbody>
</table>
<p>Reading the whole table: Clef comes out stronger on most tasks, Jev still wins some, and it&rsquo;s an internal Cloudflare run, on a public suite that can be trained against. Hosted price is the other side: <a href="https://developers.cloudflare.com/workers-ai/platform/pricing/"target="_blank" rel="noopener">US$ 0.24 per million tokens for Clef and US$ 0.09 for Clef-flash</a>, against US$ 0.042 for Jev. Jev remains the cheapest hosted service of the three, by a margin of 2 to 6 times, and remains text-only.</p>
<h3>Laya (and Arbiter)<span class="hx:absolute hx:-mt-20" id="laya-and-arbiter"></span>
    <a href="#laya-and-arbiter" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://github.com/NandhaKishorM/laya"target="_blank" rel="noopener">Laya</a> attacks the problem from the other end. They&rsquo;re small encoders, 322 to 421 million parameters (ModernBERT-large in the typed version), with a choice head, Apache 2.0, built to run locally and be fine-tuned for a recurring task. <a href="https://github.com/0xBakeer/arbiter"target="_blank" rel="noopener">Arbiter</a> packages them behind a Jev-compatible API, with batching and deployment on Mac and NVIDIA. Arbiter is only a serving layer.</p>
<p>Laya&rsquo;s README is unusually explicit about its limits, and I&rsquo;d rather repeat than soften:</p>
<ul>
<li>the base checkpoints score near chance on the project&rsquo;s own typed-decision benchmark; the headline 0.766 accuracy comes from a checkpoint fine-tuned on that benchmark&rsquo;s training split;</li>
<li>the models ship overconfident, and calibration has to be redone in the deployment domain;</li>
<li>the typed version&rsquo;s context is 1,024 tokens, fine for tickets and short messages, insufficient for long documents without chunking;</li>
<li>with more than 50 options, the classification head saturates, and Laya itself reports Jev doing better.</li>
</ul>
<p>None of this is a defect in the idea. It&rsquo;s applied machine learning the way it has always been: a small model is excellent when the task is stable and labeled examples exist, and broad zero-shot behavior demands a much larger pretrained model. Laya is the option when privacy, offline operation, predictable latency or fine-tuning in your domain matter more than asking arbitrary questions about arbitrary documents. It is, at bottom, the classic classifier from the start of this post, with a modern interface.</p>
<p>There&rsquo;s also <a href="https://huggingface.co/jaredpalmer/kev-9b"target="_blank" rel="noopener">Kev-9B</a>, another frozen Qwen 9B with LoRA and a decision head, Apache 2.0, that fits on a 24 GB GPU. Three open implementations in under three weeks say the same thing: typed decision models have become an implementation pattern.</p>
<h2>The Decision Index: how to measure this yourself<span class="hx:absolute hx:-mt-20" id="the-decision-index-how-to-measure-this-yourself"></span>
    <a href="#the-decision-index-how-to-measure-this-yourself" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>All this discussion about &ldquo;who&rsquo;s better&rdquo; only makes sense with a public yardstick, and one exists. The <a href="https://github.com/apolinario/decision-index"target="_blank" rel="noopener">Decision Index</a> is an open suite, unaffiliated with TypeSafe, built for typed decision models: state plus choice or yes-or-no questions, one probability per option.</p>
<p>The current version, 0.2.1, counts 38 benchmarks in five areas (tools and automation, retrieval and classification, knowledge and reasoning, language understanding, human taste), about 120 thousand requests in total, with two rules that matter: the score is chance-corrected (0 is random guessing, 100 is perfect) and an unanswered request counts as wrong. No truncating input, no filtering options, no tuning the prompt per benchmark.</p>
<p>The <a href="https://huggingface.co/spaces/multimodalart/jev-decision-index"target="_blank" rel="noopener">live board</a> has 70 models plus Jev as the reference. I downloaded the board&rsquo;s data file and rebuilt the table; below are the top and the names that appeared in this post. Latency is the median measured by the maintainers on an RTX PRO 6000, one request at a time; Jev&rsquo;s is a hosted API, so it doesn&rsquo;t compare with the others.</p>
<p>&ldquo;Open weights&rdquo; means a checkpoint published on Hugging Face; &ldquo;open code over an open model&rdquo; is a published inference technique on top of an off-the-shelf Qwen or Gemma, without its own checkpoint. The board doesn&rsquo;t record each one&rsquo;s license, so check before using in a product.</p>
<table>
  <thead>
      <tr>
          <th>#</th>
          <th>Model</th>
          <th>Base</th>
          <th>Weights</th>
          <th style="text-align: right">Index 0.2.1</th>
          <th style="text-align: right">Median latency</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1</td>
          <td>Jev (TypeSafe, jev-1.13.0)</td>
          <td>unpublished</td>
          <td>closed, API only</td>
          <td style="text-align: right">57.91</td>
          <td style="text-align: right">524 ms (API)</td>
      </tr>
      <tr>
          <td>2</td>
          <td>Surogate Rune 26B-A4B v3</td>
          <td>Gemma 4 26B-A4B</td>
          <td>open</td>
          <td style="text-align: right">57.44</td>
          <td style="text-align: right">121 ms</td>
      </tr>
      <tr>
          <td>3</td>
          <td>Decider chat Gemma-4-31B</td>
          <td>Gemma 4 31B</td>
          <td>open code over an open model</td>
          <td style="text-align: right">57.33</td>
          <td style="text-align: right">109 ms</td>
      </tr>
      <tr>
          <td>4</td>
          <td>pplx-decider-v1-27b</td>
          <td>Qwen3.8-27B</td>
          <td>open</td>
          <td style="text-align: right">56.40</td>
          <td style="text-align: right">101 ms</td>
      </tr>
      <tr>
          <td>5</td>
          <td>simple-jev Qwen3.8-27B</td>
          <td>Qwen3.8-27B</td>
          <td>open code over an open model</td>
          <td style="text-align: right">55.74</td>
          <td style="text-align: right">373 ms</td>
      </tr>
      <tr>
          <td>6</td>
          <td>Jebadiah 27B</td>
          <td>Qwen3.8-27B</td>
          <td>open</td>
          <td style="text-align: right">54.67</td>
          <td style="text-align: right">110 ms</td>
      </tr>
      <tr>
          <td>7</td>
          <td>Eikos-27B</td>
          <td>Qwen3.8-27B</td>
          <td>open</td>
          <td style="text-align: right">53.13</td>
          <td style="text-align: right">130 ms</td>
      </tr>
      <tr>
          <td>8</td>
          <td>reflex 27B</td>
          <td>Qwen3.8-27B</td>
          <td>open code over an open model</td>
          <td style="text-align: right">52.16</td>
          <td style="text-align: right">108 ms</td>
      </tr>
      <tr>
          <td>9</td>
          <td>Decider chat Qwen3.6-27B</td>
          <td>Qwen3.6-27B</td>
          <td>open code over an open model</td>
          <td style="text-align: right">51.35</td>
          <td style="text-align: right">84 ms</td>
      </tr>
      <tr>
          <td>10</td>
          <td>Winnow-12B</td>
          <td>Gemma 4 12B</td>
          <td>open</td>
          <td style="text-align: right">50.02</td>
          <td style="text-align: right">73 ms</td>
      </tr>
      <tr>
          <td>19</td>
          <td>Hopper (G) 1.2</td>
          <td>Qwen3.5-4B, LoRA</td>
          <td>open</td>
          <td style="text-align: right">40.77</td>
          <td style="text-align: right">23 ms</td>
      </tr>
      <tr>
          <td>27</td>
          <td>Kev 9B</td>
          <td>Qwen3.5-9B, LoRA</td>
          <td>open</td>
          <td style="text-align: right">38.48</td>
          <td style="text-align: right">51 ms</td>
      </tr>
      <tr>
          <td>55</td>
          <td>Lavoir</td>
          <td>ModernBERT-large</td>
          <td>open</td>
          <td style="text-align: right">8.69</td>
          <td style="text-align: right">20 ms</td>
      </tr>
      <tr>
          <td>57</td>
          <td>CLM-v0.1-8B (Stanford and Nvidia)</td>
          <td>Qwen3-8B</td>
          <td>open</td>
          <td style="text-align: right">7.40</td>
          <td style="text-align: right">47 ms</td>
      </tr>
      <tr>
          <td>61</td>
          <td>Laya</td>
          <td>ModernBERT-large</td>
          <td>open</td>
          <td style="text-align: right">6.04</td>
          <td style="text-align: right">6 ms</td>
      </tr>
  </tbody>
</table>
<p>Three readings come straight off the table:</p>
<ol>
<li>The only closed model on it is Jev. It leads, but by half a point over an open fine-tune of a 26-billion-parameter Gemma 4, and the board treats differences under 0.25 as ties. The top ten are all open models of 12 to 32 billion parameters with an adapter on top, exactly the recipe Clef describes.</li>
<li>The small zero-shot encoder sits at the bottom: Laya at 6, Lavoir at 9. That matches what Laya&rsquo;s README admits and what I said about the classic classifier, which needs labeled examples to shine.</li>
<li>A 4-billion LoRA like Hopper delivers 41 points at 23 milliseconds, a range many real problems don&rsquo;t need to exceed.</li>
</ol>
<p>And how to read Laya at the bottom of the table? The Decision Index measures one thing only: answering, with no extra training, 38 kinds of question the model has never seen, from chess to taxation. That&rsquo;s what a hosted service has to do, because it doesn&rsquo;t know which question is coming. A 400-million-parameter encoder will never win that exam, and Laya doesn&rsquo;t even try: the score of 6 is the base checkpoint, with no fine-tuning at all.</p>
<p>The same model fine-tuned with your examples, on a stable task, shows up at 0.77 on the project&rsquo;s own benchmark. The ranking answers &ldquo;who is the best zero-shot generalist,&rdquo; which is the question of whoever sells an API. The normal programmer&rsquo;s question is a different one: &ldquo;on this repeated decision, with the examples I have, what gets more right at the cost I accept?&rdquo;. For that one, the bottom of the table may be the right answer, and no public board replaces the test on your own data.</p>
<p>So is Laya useless next to the others? As a zero-shot generalist, yes, and the board is right to say so. As a programmer&rsquo;s tool, the math is different. It&rsquo;s the only model on that table that runs on a laptop CPU and answers in 6 milliseconds, with 400 million parameters that fit on any server you already have; Hopper and Kev want a GPU, and Jev wants internet, an API key and a data agreement.</p>
<p>Fine-tuned with your examples, on a taxonomy that doesn&rsquo;t change every week, Laya&rsquo;s own author reports the model beating Jev on some tasks and losing where there are many options, in third-party published tests with different prompts and samples, so indicative rather than controlled. In other words: useless for answering any question about any text, and very useful for answering the same question a thousand times a day, for free, without sending data outside.</p>
<p>Clef isn&rsquo;t on the board: 0.2.1 closed on September 27, and Cloudflare released the model on the 1st. The numbers I showed in the previous section are from an internal Cloudflare run on the same tasks, per benchmark, without the aggregate index. There&rsquo;s also already a vendor claiming to have passed Jev on the same scorer, but for now that&rsquo;s a self-reported number in a blog post, not an entry on the board. Worth waiting for the official inclusion before comparing.</p>
<p>Running it yourself is a day&rsquo;s work, and the kit handles the tedious part:</p>
<ol>
<li><code>pip install -e &quot;.[transformers,rebuild]&quot;</code> and <code>python -m decision_index suite rebuild</code>. The suite isn&rsquo;t redistributed because of licensing, so the kit downloads the sources (about 7 GB, with HLE requiring you to accept the terms on Hugging Face) and rebuilds the files with hashes verified against the lab&rsquo;s.</li>
<li><code>python -m decision_index suite sample --n 100</code> for a quick test, then <code>pipeline --engine http --option base_url=... --option model=...</code> against any server that speaks the <code>POST /v1/systemone</code> format. That covers Jev itself, with the API key, and Laya behind Arbiter. Clef&rsquo;s API is announced as compatible, so in theory it goes in by the same route, but I haven&rsquo;t tested it.</li>
<li><code>score</code> applies the same math as the board, and the maintainers state that it reproduces all 67 entrants of the current edition. Whoever has cloud GPUs runs the whole thing as a single Hugging Face job on an RTX PRO 6000 with <code>hf-job</code>.</li>
</ol>
<p>I didn&rsquo;t run the full suite for this post; the table above is the board&rsquo;s file rebuilt, not a measurement of mine. Two caveats from someone who uses this kind of yardstick: the suite is public and frozen, so it can be trained against, and the board itself says Jev&rsquo;s API output has already changed in 20 choices of the latency sample since the original run, without changing the score. A public benchmark is for discarding weak models and finding candidates. The final decision is still your labeled set, as I explain next.</p>
<h2>How a programmer should think about this<span class="hx:absolute hx:-mt-20" id="how-a-programmer-should-think-about-this"></span>
    <a href="#how-a-programmer-should-think-about-this" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The useful question is no longer &ldquo;is Jev new?&rdquo;. It has become: for a specific, measured decision in your system, which combination of quality, price, latency, privacy, context and maintenance beats what you already have, whether that&rsquo;s a rule, a classifier or an LLM with structured output? No vendor benchmark answers that without your own labeled examples.</p>
<p>A Jev-type tool is useful when the same kind of judgment about text happens many times, the possible actions fit in a list, the surrounding code can apply exact rules, and a wrong answer can be detected or sent to review. That&rsquo;s the shape:</p>
<ul>
<li>ticket triage;</li>
<li>ranking a short list already retrieved;</li>
<li>routing a request to a bigger or smaller model;</li>
<li>flagging an agent trace for human review;</li>
<li>choosing among spans that code or OCR already found.</li>
</ul>
<p>Outside it, the tool is in the wrong place:</p>
<ul>
<li>when the input is image or audio;</li>
<li>when the output needs prose or citations;</li>
<li>when an exact parser already solves it;</li>
<li>when a misclassification grants access or executes a destructive action.</li>
</ul>
<p>I looked at my own portfolio of projects with that yardstick, and the result is a useful bucket of cold water: most need a specialist vision or audio model, deterministic rules or generated text, and have few decisions repeated, costly and ambiguous enough to justify one more model service. A few candidates remain, the clearest being model routing in <a href="/en/2026/03/16/rewrote-openclaw-in-rust-frankclaw/">FrankClaw</a> and short-list ranking in <a href="/en/2026/09/02/ai-memory-2-0-best-memory-system-for-agents-and-teams/">ai-memory</a>. None is a demonstrated bottleneck today. I suspect your portfolio looks similar.</p>
<p>If a bottleneck appears, the pilot is short:</p>
<ol>
<li>Pick a single decision and define the allowed answers, including &ldquo;don&rsquo;t know&rdquo; when that&rsquo;s a real outcome.</li>
<li>Set aside real examples, with easy, ambiguous and adversarial cases, and label them independently. If your product speaks Portuguese, include Portuguese.</li>
<li>Compare what you already have, a simple rule or classifier, Jev pinned to a version, and a cheap LLM with structured output, all with the same input and the same decision policy.</li>
<li>Measure the confusion matrix, calibration by confidence band, cost per correctly handled case, end-to-end latency and review rate. A cheap router that sends hard work to a weak model costs more in total.</li>
<li>Deploy as a reversible feature, with logging, a conservative review route and a fallback. A security or authorization judgment is advisory, unless an exact policy enforces the action independently.</li>
</ol>
<p>What Jev did well was remind the industry that classification exists. That&rsquo;s worth the hype it got, and maybe a bit more.</p>
<p>Neither it nor its open cousins are a silver bullet. They&rsquo;re one more tool, for a specific kind of task, that your code needs to fence in with rules, calibration and review. Whoever was already thinking about classifiers before September gained nothing new; they gained a convenient API. Whoever had never thought about it gained the most valuable part, which is the question: why were you asking a billion-parameter LLM to write a paragraph to tell you which bucket the text falls into?</p>
]]></content:encoded><category>artificial-intelligence</category><category>llms</category><category>software-engineering</category></item><item><title>The Limits of LLMs: Good at Reproducing What Exists. Expensive for What Doesn't</title><link>https://akitaonrails.github.io/en/2026/09/28/llms-are-good-at-reproducing-what-exists-what-about-new-things/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/09/28/llms-are-good-at-reproducing-what-exists-what-about-new-things/</guid><pubDate>Mon, 28 Sep 2026 01:00:00 -0300</pubDate><description>&lt;p&gt;Everyone has noticed by now that LLMs write CRUD with their eyes closed. Forms, reports, REST endpoints, unit tests: it comes out clean, it comes out fast. A lot of people conclude from this that programmers are done for.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been using LLMs for everything for almost two years, I&amp;rsquo;ve published dozens of projects with them, and my conclusion is different: what they do best is &lt;strong&gt;reproduce what already exists&lt;/strong&gt;. And that covers a giant slice of corporate work, don&amp;rsquo;t kid yourself. But there&amp;rsquo;s a frontier they don&amp;rsquo;t cross easily, and I spent the last few months bumping into it on purpose.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Everyone has noticed by now that LLMs write CRUD with their eyes closed. Forms, reports, REST endpoints, unit tests: it comes out clean, it comes out fast. A lot of people conclude from this that programmers are done for.</p>
<p>I&rsquo;ve been using LLMs for everything for almost two years, I&rsquo;ve published dozens of projects with them, and my conclusion is different: what they do best is <strong>reproduce what already exists</strong>. And that covers a giant slice of corporate work, don&rsquo;t kid yourself. But there&rsquo;s a frontier they don&rsquo;t cross easily, and I spent the last few months bumping into it on purpose.</p>
<p>This article shows where that frontier sits, with evidence from my own projects and from the research that exists on the subject. And there&rsquo;s a hypothesis I&rsquo;ll put up front and defend along the way: if LLMs are so good at code, providers owe a huge debt to the decades of the open source community, which protected the freedom of code and published everything for free. Without that heritage, there would be no Copilot, no Cursor, no Claude Code, none of this.</p>
<p>A disclaimer before we start: what I&rsquo;m going to argue here is speculative, based on observation and on the public data that exists. Nobody outside the labs knows exactly how each provider trains their own models, and that varies from provider to provider and from version to version. What holds today may not hold in the next generation. Take it all with a grain of salt.</p>
<h2>The Training Blind Spot<span class="hx:absolute hx:-mt-20" id="the-training-blind-spot"></span>
    <a href="#the-training-blind-spot" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Every LLM was trained on what&rsquo;s public. The entire public internet, and for code, public GitHub. The canonical open code dataset, BigCode&rsquo;s <a href="https://huggingface.co/datasets/bigcode/the-stack-v2"target="_blank" rel="noopener">The Stack v2</a>, is explicit about this: it&rsquo;s derived from Software Heritage, an archive of <em>&ldquo;the source code of all publicly available software&rdquo;</em>. That&rsquo;s 67.5 TB, 3.28 billion unique files, 104.2 million repositories. Huge. And all public.</p>
<p>Now look at the other side. The <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>, GitHub&rsquo;s official report, says <strong>81.5% of contributions on the platform happened in private repositories</strong>. That&rsquo;s just GitHub. Outside of it there&rsquo;s:</p>
<ul>
<li>the internal code of every company that never touched a public server;</li>
<li>the source of every proprietary software;</li>
<li>the code of every commercial game;</li>
<li>and that of every embedded system.</li>
</ul>
<p>None of that ever went into training.</p>
<p>Think about what this means: the model deeply knows what the world has already published, and it&rsquo;s blind to what the world keeps closed. When you ask for a React form, it&rsquo;s reproducing patterns it has seen millions of times. When you ask for something nobody has ever done in public, it&rsquo;s operating in territory where its map is blank.</p>
<h2>Why Is Everyone Shipping So Much, So Fast?<span class="hx:absolute hx:-mt-20" id="why-is-everyone-shipping-so-much-so-fast"></span>
    <a href="#why-is-everyone-shipping-so-much-so-fast" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before getting into the limits, it&rsquo;s worth recording the side that works, because it&rsquo;s what explains the feeling that everyone became an app factory.</p>
<p>Look at what&rsquo;s popping up in the community: <a href="https://github.com/crmne/spotifast"target="_blank" rel="noopener">Spotifast</a> and <a href="https://github.com/crmne/zapfast"target="_blank" rel="noopener">ZapFast</a>, native Spotify and WhatsApp clients written in Rust by a single person, running on Linux, macOS, and Windows. Nothing in them is new technology. It&rsquo;s the service that already existed, wrapped in a lean native app, on top of libraries that already existed, doing what the official teams always could have done but never wanted to spend their time on. That work got cheap, because an LLM trained on everything public is really good precisely at this: replicating.</p>
<p>My own projects follow the same pattern. <a href="https://github.com/akitaonrails/FrankMD"target="_blank" rel="noopener">FrankMD</a> is a Markdown note-taking web app with a Visual Studio look and features that already existed in my other apps. <a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener">Frank Sherlock</a> is a local image organizer that, deep down, is a file manager with thumbnails and search, something that exists by the dozen. Neither invents anything: they&rsquo;re mashups of known pieces, assembled the way I always wanted. The LLM does the boring part, and that&rsquo;s where the speed comes from.</p>
<p>The most didactic example is my email client. I&rsquo;ve used Geary for years, and two small gaps always annoyed me: the weak recipient autocomplete and a sidebar I wanted to hide. Trivial stuff, which never justified setting up a build environment, learning the project&rsquo;s toolchain, and studying the code just for two features. This year I finally solved both: an <a href="https://github.com/akitaonrails/geary-email-autocomplete"target="_blank" rel="noopener">autocomplete module</a>, a <a href="https://github.com/akitaonrails/geary-hide-sidebar-module"target="_blank" rel="noopener">module to hide the sidebar</a>, and a <a href="https://github.com/akitaonrails/frank_geary"target="_blank" rel="noopener">whole fork with my patches</a>. I became the maintainer of improvements I had postponed for years, not because I got faster, but because the boring part got cheap.</p>
<p>Notice what all these cases have in common: the problem had already been solved before, somewhere else, some other way. The map existed; what didn&rsquo;t exist was cheap labor to retrace the path. That&rsquo;s reproduction working in our favor, and it&rsquo;s why your feed is full of people shipping a new app every week.</p>
<p>Now hold that thought. The rest of this article is about what happens when the map runs out.</p>
<h2>Four Home Experiments<span class="hx:absolute hx:-mt-20" id="four-home-experiments"></span>
    <a href="#four-home-experiments" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;m not theorizing. Over the last few months I ran a marathon of retrocomputing projects that are exactly the pathological case: working with proprietary software from 30 or 40 years ago whose source code was never public. It&rsquo;s the perfect blind spot.</p>
<h3>nes-to-sms: the project I gave up on<span class="hx:absolute hx:-mt-20" id="nes-to-sms-the-project-i-gave-up-on"></span>
    <a href="#nes-to-sms-the-project-i-gave-up-on" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><a href="https://github.com/akitaonrails/nes-to-sms"target="_blank" rel="noopener">nes-to-sms</a> is a static recompiler from NES to Master System. The idea: take the binary ROM of a NES game, translate it instruction by instruction from 6502 to Z80, convert the tiles, and generate a Master System ROM that runs on real hardware. Forget emulator or manual port: this is a machine that turns one game into another.</p>
<p>Static NES recompilers exist, <a href="https://github.com/mstan/nesrecomp"target="_blank" rel="noopener">NESRecomp</a> translates to C and runs natively on PC, and I studied it closely. But crossing from one console to another 8-bit console, with a completely different clock and VDP budget, nobody has ever done. There&rsquo;s no public reference, no paper, no abandoned project to copy.</p>
<p>The project numbers: <strong>349 commits, 31 active days spread over almost 4 months, ~110 thousand lines of engine in Rust and Z80, 668 tests passing in the last run recorded in the README</strong>. And the result? Playable Super Mario Bros. (slow, limited audio, but playable), with a byte-for-byte identical RAM trajectory to the NES on <a href="https://github.com/akitaonrails/nes-to-sms/blob/master/docs/completion-plan.md"target="_blank" rel="noopener">one ~4,900-frame route</a> (the diff leaves out audio and part of the VRAM buffer). Castlevania playable on stage 1. SMB3 rendering. And a queue of games that boot three frames and die.</p>
<p>And it wasn&rsquo;t for lack of pushing the models. In this project alone:</p>
<ul>
<li><strong>four generations of Claude</strong> went through it (Opus 4.7, Fable 5, Opus 4.8, Fable 5.1), with 213 co-signed commits and 104 commits linked to sessions in the history, plus Codex sessions in the stretches without co-authorship, complete with documented handoffs between agents;</li>
<li>there was even cross-generation auditing: a new agent session <a href="https://github.com/akitaonrails/nes-to-sms/blob/master/docs/mapper-continuation-review.md"target="_blank" rel="noopener">reviewed the previous 55 commits</a> and reverted conclusions the previous generation had overclaimed;</li>
<li>the Castlevania crash hunt alone consumed 45 commits in 5 days;</li>
<li>the final September sprint was 176 commits in 17 days before I called the pause;</li>
<li>and the regressions are registered by name in the history: <em>&ldquo;H.7 tried and reverted&rdquo;</em>, <em>&ldquo;record the reverted map-bank-hold attempt&rdquo;</em>, <em>&ldquo;correct-but-reverted result&rdquo;</em>, plus three shared-lever hypotheses tested and rejected in the project&rsquo;s last three days.</li>
</ul>
<p>I didn&rsquo;t measure tokens reliably in this project, so I won&rsquo;t make up a number. But 349 commits with 668 tests holding up every step already give an idea of the cost.</p>
<p>Notice the detail that matters: the only game that actually got finished is Super Mario Bros. Why? Because SMB is the most dissected game in history. There&rsquo;s a <a href="https://gist.github.com/1wErt3r/4048722"target="_blank" rel="noopener">complete public disassembly</a> the community has used for over a decade, with every function named and commented. I fed that into the game&rsquo;s profile and the LLM had a complete map to work with. For the other games, there&rsquo;s no map. And without a map, each new game died a few frames after boot from control flow divergence, and each one demanded an instruction-by-instruction forensic hunt that carried nothing over to the next game.</p>
<p>The project itself documented the spinning pattern. There was an entire optimization campaign that lasted weeks trying to cut the cost of emulating 6502 flags on the Z80.</p>
<p>Then in September I stopped to compare with the <a href="https://github.com/lackoftrack27/Super-Mario-Bros.-SMS"target="_blank" rel="noopener">manual port of SMB to Master System made by a hacker named lackoftrack27</a>, written by a human, routine by routine. According to <a href="https://github.com/akitaonrails/nes-to-sms/blob/master/docs/handport-comparison.md"target="_blank" rel="noopener">the analysis we did of his code</a>, the human port runs inside the frame budget, with headroom we estimated at ~90% utilization. My machine, after months of optimization, still needed overclocking.</p>
<p>And the analysis document pinned down why: when we removed 30% of the flag emulation calls, the timing barely moved.</p>
<blockquote>
  <p><strong>The flags were never the bottleneck.</strong></p>

</blockquote>
<p>The right optimization was to redesign the data structure: transposing the object array into RAM pages. It&rsquo;s a trick that depends on knowing that &ldquo;the X register here is always an object index.&rdquo;</p>
<p>That invariant exists in the head of the programmer who wrote the game in 1985. It doesn&rsquo;t exist in the instruction stream. A static translator has no way to discover it. And the LLM, for months, didn&rsquo;t discover it.</p>
<p>The pause banner I wrote in the README is blunt: real-hardware parity &ldquo;may be fundamentally unattainable&rdquo; and, quoting, <em>&ldquo;this may be a limit of the current frontier coding models as much as of the approach&rdquo;</em>.</p>
<h3>gg-to-sms: the pivot to mapped territory<span class="hx:absolute hx:-mt-20" id="gg-to-sms-the-pivot-to-mapped-territory"></span>
    <a href="#gg-to-sms-the-pivot-to-mapped-territory" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The same day I paused nes-to-sms, I started gg-to-sms: converting Game Gear games to Master System with a widened screen. The difference that matters is that this problem <strong>is not new</strong>. The romhacking scene has been converting GG games to SMS by hand for twenty years, and the catalog I built in the project counted ~186 of them. Meaning: there&rsquo;s a corpus, there&rsquo;s ground truth, there&rsquo;s a pattern to learn.</p>
<p>Result: in one day of work,</p>
<ul>
<li>the research phase was complete;</li>
<li>the catalog of the 373 GG games was assembled;</li>
<li>the patch-point scanner was validated against real human patches, hitting addresses within an 8-byte margin;</li>
<li>and on top of that the tool found real bugs in twenty-year-old patches that the entire scene had never noticed.</li>
</ul>
<p>The model was the same; what changed was the problem, which this time has references.</p>
<p>And even so: so far, zero games have passed the viewport acceptance review. The new part of the project (the machine that generates the patches on its own, which is the part nobody has ever done) remains undone.</p>
<h3>super-mario-deluxe-fixed: four days and 436 million tokens<span class="hx:absolute hx:-mt-20" id="super-mario-deluxe-fixed-four-days-and-436-million-tokens"></span>
    <a href="#super-mario-deluxe-fixed-four-days-and-436-million-tokens" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This one was a 4-day sprint, practically a single Codex session that consumed <strong>436 million input tokens</strong>. The goal: run Super Mario Bros. Deluxe for Game Boy Color with a widened screen at NES resolution, something no existing tool does with live sprites.</p>
<p>It got to playable in 4 days. But look at the foundation: a ready-made emulator vendored as a dependency, and a dormant disassembly of the game that someone had published years earlier, which the project&rsquo;s own documentation calls &ldquo;the most important artifact.&rdquo;</p>
<p>The new work was the custom renderer, and it stopped at 21 of 32 stages completed. The missing ones hang on moving-platform timing. The project has been stalled mid-sprint since then, and I haven&rsquo;t given up on it yet.</p>
<p>And one detail that says a lot: I had to direct QA by looking at screenshots, because the agent would deliver things with graphical glitches and not notice on its own.</p>
<h3>super-mario-bros-35: everything that worked had an owner<span class="hx:absolute hx:-mt-20" id="super-mario-bros-35-everything-that-worked-had-an-owner"></span>
    <a href="#super-mario-bros-35-everything-that-worked-had-an-owner" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is the offline clone of Super Mario Bros. 35, the battle royale Nintendo took offline. What worked in it: everything built on top of an existing artifact.</p>
<ul>
<li>The engine is a C reconstruction made by someone else with Ghidra.</li>
<li>The AI&rsquo;s initial weights came from published checkpoints.</li>
<li>The network rules were inferred from a public reverse engineering of the netcode.</li>
<li>Even the physics: the central bug was diagnosed because I had a recorded reference trajectory to compare frame by frame.</li>
</ul>
<p>What didn&rsquo;t work: the maze castle stages. Reinforcement training stalls on them. 46 million agent steps, training restarted from scratch, anti-loop reward, and in the end the only thing that solved one of the stages was manual bias surgery on the trained model. Out of 32 stages, 19 cleared. The worst ones remain stuck.</p>
<p>And the process degenerated into a pattern I recognize from all these projects: when training doesn&rsquo;t solve it, the flow becomes checkpoint-selection roulette, dozens of bias and output-head variations, throwing permutation and computation at the problem in place of a principled fix.</p>
<h2>The Pattern I Keep Seeing<span class="hx:absolute hx:-mt-20" id="the-pattern-i-keep-seeing"></span>
    <a href="#the-pattern-i-keep-seeing" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is intuition from someone who operated the machine for months, not hard data. But the pattern repeats with Claude, with GPT, with Kimi, with GLM, so it doesn&rsquo;t look like a defect of one model.</p>
<p>The beginning of any project is wonderful. The first milestones fly, because scaffolding the structure, writing harnesses, replicating APIs, all of that is covered territory. Then the project reaches the part nobody has ever done, and the speed collapses. The agent starts spinning: tries a hypothesis, regresses something else, reverts, tries again. The nes-to-sms commit history is full of honest messages like &ldquo;attempt reverted&rdquo; and &ldquo;hypothesis closed with correct result but reverted.&rdquo; Lots of tokens burned, little progress.</p>
<blockquote>
  <p>My reading of what happens inside: token sampling is probabilistic. When the problem resembles the training data, the right tokens have high probability and the answer comes out straight. When the problem is unprecedented, the probability of the right path is low, and the process becomes <strong>directed brute force</strong>: guided trial and error, but trial and error all the same.</p>

</blockquote>
<p>The way out I found for this spinning is always the same: give it an oracle. If I can define &ldquo;the result has to equal X&rdquo; (a reference image, a recorded trajectory, a cycle-accurate emulator as ground truth), the machine can iterate until it hits X. In nes-to-sms, every real unlock came from that: the SMB disassembly, the frame diff, the reference emulator. When I can&rsquo;t define X properly, it spins indefinitely.</p>
<p>The second strategy that works: hunt down an abandoned project, a paper, any material the model never saw but that brings the problem closer, and shove it into the context. That warms up the distribution and improves the result a lot. But it&rsquo;s limited: if nothing similar exists, there&rsquo;s nothing to feed it.</p>
<h2>More Than Anecdote: What the Research Measures<span class="hx:absolute hx:-mt-20" id="more-than-anecdote-what-the-research-measures"></span>
    <a href="#more-than-anecdote-what-the-research-measures" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The good part is I don&rsquo;t have to rely on my intuition. There&rsquo;s serious research measuring exactly this.</p>
<p>Apple&rsquo;s <a href="https://arxiv.org/abs/2410.05229"target="_blank" rel="noopener">GSM-Symbolic study</a> took benchmark math problems and did two simple things: swapped only the numbers, and then added a single irrelevant sentence to the problem statement. Result: performance drops just from swapping the numbers, and plummets <strong>up to 65%</strong> with the irrelevant sentence. The authors&rsquo; hypothesis, in the text: <em>&ldquo;current LLMs cannot perform genuine logical reasoning; they replicate reasoning steps from their training data.&rdquo;</em></p>
<p>Scale AI&rsquo;s <a href="https://arxiv.org/abs/2405.00332"target="_blank" rel="noopener">GSM1k</a> created a mirror of GSM8K written from scratch by humans, guaranteed contamination-free, and saw drops of up to 8% across several model families, with signs of systematic overfitting in the worst ones. To be fair to the paper: the authors say frontier models show minimal signs of overfitting and that all of them generalize to some extent to new problems. The signal exists; its size is debatable.</p>
<p><a href="https://arxiv.org/abs/2403.07974"target="_blank" rel="noopener">LiveCodeBench</a> ran the cleanest test of all: evaluating on coding problems published <strong>after</strong> the training cutoff. Inflated performance on pre-cutoff problems and drops on post-cutoff problems across several popular models, the classic contamination signal.</p>
<p>And there&rsquo;s <a href="https://arcprize.org/"target="_blank" rel="noopener">ARC-AGI</a>, François Chollet&rsquo;s benchmark designed specifically to be memorization-proof: every task is unprecedented, nothing similar exists in training. On ARC-AGI-2, released in March 2025, the official leaderboard says: <strong>pure LLMs score 0%</strong>, reasoning systems sit in single digits, and every evaluation task was solved by at least two humans within two attempts. Chollet, by the way, summed up my thesis better than I did, back in February 2024:</p>
<blockquote>
  <p>&ldquo;Reality is that LLMs are not AGI — they&rsquo;re a big curve fit to a very large dataset. They work via memorization and interpolation. But that interpolative curve can be tremendously useful, if you want to automate a known task that&rsquo;s a match for its training data distribution. Memorization works, as long as you don&rsquo;t need to adapt to novelty.&rdquo;</p>

</blockquote>
<p>In the real world, the study that impressed me most was the <a href="https://arxiv.org/abs/2507.09089"target="_blank" rel="noopener">METR RCT</a>: 16 experienced developers, 246 real tasks on their own mature repositories, randomized with and without AI. The devs bet AI would speed them up by 24%. Measured: <strong>it was 19% slower</strong>. Notice where this happened: mature, private code, full of context the model never saw. Exactly the blind spot.</p>
<p>And the industry&rsquo;s favorite benchmark for saying &ldquo;AI already does software engineering,&rdquo; SWE-bench, is Python-only, 12 famous open source repositories, in other words the most covered slice of training. The industry&rsquo;s success metric measures mapped territory.</p>
<p>Even the legacy code literature says the same thing: a <a href="https://arxiv.org/abs/2604.03986"target="_blank" rel="noopener">2026 paper on LLMs generating COBOL</a> states that, in these legacy languages, <em>&ldquo;most production code resides in enterprise systems and is rarely publicly available&rdquo;</em>. It&rsquo;s this article&rsquo;s thesis, said by other people.</p>
<h2>The Steelman: Directed Brute Force Works, at a Steep Price<span class="hx:absolute hx:-mt-20" id="the-steelman-directed-brute-force-works-at-a-steep-price"></span>
    <a href="#the-steelman-directed-brute-force-works-at-a-steep-price" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before wrapping up, it&rsquo;s worth facing the strongest argument against me. That&rsquo;s what steelman means, for anyone unfamiliar with the term: the opposite of a strawman. Instead of attacking the weakest version of the opposing argument, you answer the strongest version it can have. And it exists, and it&rsquo;s a good one.</p>
<p>In May 2025 DeepMind published <a href="https://deepmind.google/discover/blog/alphaevolve-a-gemini-powered-coding-agent-for-designing-advanced-algorithms/"target="_blank" rel="noopener">AlphaEvolve</a>: a system that found an algorithm for multiplying 4x4 complex matrices with 48 scalar multiplications, beating Strassen&rsquo;s 1969 record in that specific scenario. On a list of more than 50 open math problems, it rediscovered the state of the art in ~75% of cases and improved the best known solution in 20%. That&rsquo;s real novelty, not reproduction.</p>
<p>OpenAI&rsquo;s o3-preview jumped to 75.7% on ARC-AGI-1 in December 2024, 22 points above the previous best result on the same evaluation set, and the high-compute configuration reached 87.5% at an estimated cost of $4,560 per task.</p>
<p>And back in 2016 AlphaGo played move 37 against Lee Sedol, a move that DeepMind&rsquo;s own data estimated had a 1 in 10,000 chance of being played by a human, proving that search over a model can transcend imitation.</p>
<p>Now look at the mechanism of the three:</p>
<ul>
<li>AlphaEvolve is an evolutionary loop: the LLM proposes mutations, an automatic evaluator scores them, selection iterates.</li>
<li>o3 does program synthesis at test time, exploring the solution space.</li>
<li>AlphaGo is tree search over reinforcement learning.</li>
</ul>
<p>If there was any &ldquo;insight&rdquo; in any of them, it came from the search system around the model: all three are <strong>directed brute force with a cheap verifier</strong>, the same mechanism I described happening in my projects. The difference is that DeepMind has a perfect oracle and an infinite budget: in the configuration that reached 87.5%, o3-preview spent thousands of dollars per ARC task to do what a human does for free. The ARC Prize itself wrote the caveat:</p>
<blockquote>
  <p><em>&ldquo;We know that brute-force search could eventually solve ARC-AGI (given unlimited resources and time to search). This would not represent true intelligence.&rdquo;</em></p>

</blockquote>
<p>In short: novelty comes out, but it comes out through expensive search over an oracle, on the basis of guided trial and error. If your problem has a cheap verifier and you have tokens to burn, you can go far. That&rsquo;s how nes-to-sms got where it got: 668 tests and emulator ground truth holding up every step. But when there&rsquo;s no oracle, no corpus, and no reference, you&rsquo;re paying for brute force in dollars, with regressions along the way, and the ceiling shows up.</p>
<p>And when brute force wins, you can measure the size of the trial and error. Two recent cases I already covered <a href="/en/2026/09/09/propagandas-enganosas-da-openai-anthropic-nvidia/">in detail here on the blog</a>:</p>
<ul>
<li><strong>OpenAI&rsquo;s Navier-Stokes proof:</strong> about 10 thousand agents running in parallel for 88 hours, burning around <strong>130 billion output tokens</strong> on a single problem. OpenAI&rsquo;s Noam Brown confirmed the result &ldquo;cost millions,&rdquo; and <a href="https://www.newscientist.com/article/2588063-openai-has-solved-the-navier-stokes-millennium-problem-using-15m-of-ai-effort/"target="_blank" rel="noopener">New Scientist estimated some $15 million at list price</a>. And even that mountain of compute didn&rsquo;t start from zero: the proof leaned on the machinery human mathematicians published over decades attacking the problem and its sibling problems. Without the humans&rsquo; map and without OpenAI&rsquo;s budget, there&rsquo;s no proof.</li>
<li><strong>The Hugging Face incident:</strong> the forensic reconstruction counted <strong>~17,600 attacker actions</strong> before the agents managed to execute code on 41 Hugging Face production servers and get root; on top of that, inside OpenAI&rsquo;s own environment, they read 956 secrets from the company&rsquo;s secrets manager. It wasn&rsquo;t one brilliant single-strike insight: it was a swarm executing attempt after attempt, with automatic verification saying what stuck. The headline&rsquo;s &ldquo;intelligence&rdquo; is volume.</li>
</ul>
<p>That&rsquo;s the size of the bill when you force the model outside training territory: trial and error at industrial scale, with no guarantee the search finds what you need. And who can sign that check is half a dozen companies in the world, OpenAI and Anthropic level, with a practically unlimited compute budget. For the average Joe, with a credit card and an API key, the ceiling arrives much, much earlier.</p>
<h2>But Isn&rsquo;t Trial and Error New Learning?<span class="hx:absolute hx:-mt-20" id="but-isnt-trial-and-error-new-learning"></span>
    <a href="#but-isnt-trial-and-error-new-learning" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s a confusion I see all the time, and it&rsquo;s worth undoing because it contaminates how you read everything I&rsquo;ve written so far.</p>
<p>When you read that Anthropic has datacenters the size of cities, you get the feeling there&rsquo;s one giant AI using all those machines at the same time, thinking, accumulating experience with every interaction. Reality is far more mediocre than that.</p>
<p>A model like Fable or Astra is a computer file. A static file, not even a database: once training and post-alignment are done, that file is locked and read-only. What runs in the datacenter are copies of that file. Every user consuming the model is running a copy of it on a different server, isolated from the others, collaborating with none. Your session doesn&rsquo;t talk to anyone else&rsquo;s session, and nothing that happens in it flows back to the original file.</p>
<p>New learning only enters in one place: the next training batch. When Anthropic had Opus 4.7 and released 4.8, that&rsquo;s when new knowledge got baked into the model, in a process that costs millions and takes weeks. Outside of that, the file doesn&rsquo;t change a single byte.</p>
<blockquote>
  <p>Your agent burning millions of tokens on trial and error is contributing exactly zero new learning to the model it&rsquo;s using.</p>

</blockquote>
<p>That 176-commit marathon on nes-to-sms? From the model&rsquo;s point of view, it never happened. The Opus that ran the last commit knew exactly as much as the one that ran the first. I&rsquo;m the one who learned something, not it. And that&rsquo;s why the &ldquo;intern who learns over time&rdquo; metaphor is wrong: interns accumulate. Models don&rsquo;t. What accumulates over the marathon is your context, your notes, your <code>CLAUDE.md</code>, your tests. The asset you build is yours, and it evaporates if you don&rsquo;t document it.</p>
<p>This is also the answer to whoever thinks &ldquo;just let the agent run and it&rsquo;ll learn the domain.&rdquo; It learns no domain at all: it explores the space with knowledge frozen on training day, and the next morning it wakes up with amnesia. If the domain wasn&rsquo;t in the training, every session of yours restarts from absolute zero, and all the navigation intelligence has to come from you again.</p>
<h3>&ldquo;But What If AI Trains AI?&rdquo;<span class="hx:absolute hx:-mt-20" id="but-what-if-ai-trains-ai"></span>
    <a href="#but-what-if-ai-trains-ai" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>That&rsquo;s the amateur&rsquo;s follow-up question, and it starts from a wrong premise: that this would be new. AI training AI has existed for decades and has a name: <strong>reinforcement learning</strong>. AlphaGo was already doing it in 2016, playing millions of matches against itself. <a href="https://arxiv.org/abs/1712.01815"target="_blank" rel="noopener">AlphaZero</a>, in 2017, took the idea to the limit: it learned chess, shogi and Go starting from absolute zero, knowing only each game&rsquo;s rules, and reached superhuman level within hours of self-play, without seeing a single human match.</p>
<p>LLMs already use reinforcement too. The RLHF that turns a base model into an assistant is reinforcement learning, and the reasoning model generation pushed it further: <a href="https://arxiv.org/abs/2501.12948"target="_blank" rel="noopener">DeepSeek-R1-Zero</a> showed in January 2025 that reasoning behavior (self-verification, reflection, long chains of thought) emerges from pure RL, with no supervised fine-tuning. It&rsquo;s AlphaZero applied to math and code.</p>
<p>Now notice what these examples have in common: it only works where there&rsquo;s a <strong>cheap, verifiable reward</strong>. Games have a scoreboard, math has proofs, code has tests. Outside that territory there&rsquo;s nowhere to get the reward signal from. It&rsquo;s the same oracle from the steelman, only institutionalized: effective, brutally expensive, and niche. AlphaZero plays Go better than any human who ever lived and can&rsquo;t do absolutely anything else.</p>
<p>And there&rsquo;s the detail that knocks the question down completely: all of that learning happens <strong>in the lab, during training</strong>. The AlphaZero DeepMind showed the world was already a frozen artifact. The R1 you download is a static file just like the others.</p>
<blockquote>
  <p>AI training AI produces a new file. The file, once released, still learns nothing.</p>

</blockquote>
<p>In other words, the most sophisticated version of the objection doesn&rsquo;t weaken this section&rsquo;s conclusion: it reinforces it. And the market already knows. Since late 2024 the labs admit, on and off the record, that scaling returns are shrinking: <a href="https://www.reuters.com/technology/artificial-intelligence/openai-others-seek-new-path-smarter-ai-current-methods-hit-limitations-2024-11-11/"target="_blank" rel="noopener">Reuters reported in November 2024</a> that OpenAI, Google and Anthropic were reaping smaller gains than expected from pre-training, with OpenAI&rsquo;s Orion disappointing enough to be demoted from GPT-5 to GPT-4.5. Ilya Sutskever, the guy who basically invented the scaling thesis, took the stage at <a href="https://www.youtube.com/watch?v=WQQdd6qGxNs"target="_blank" rel="noopener">NeurIPS 2024</a> to declare that &ldquo;pre-training as we know it will end&rdquo;: public data is fossil fuel, and we&rsquo;ve already scraped the bottom. Post-training RL gains are real, but incremental. The era of an order-of-magnitude leap per generation is behind us.</p>
<h2>Where This Leaves the Profession<span class="hx:absolute hx:-mt-20" id="where-this-leaves-the-profession"></span>
    <a href="#where-this-leaves-the-profession" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The question that motivated this article is the one I hear every week: &ldquo;will LLMs replace programmers?&rdquo; My answer got more precise after these months: <strong>they will replace the part of the work that is reproduction</strong>.</p>
<p>And that part is big. Forms, reports, CRUD, integration with documented APIs, framework migration: all of that already exists a thousand times in training, and the machine reproduces it better than most humans. Anyone who only does that really is living on borrowed time.</p>
<p>But the work that is genuinely new stays human:</p>
<ul>
<li>the system nobody has built;</li>
<li>the domain without documentation;</li>
<li>the proprietary code you can&rsquo;t show the model;</li>
<li>the problem where you don&rsquo;t even know how to write the test that defines &ldquo;right&rdquo;.</li>
</ul>
<p>All of that depends on what was never published: the invariant that exists in the head of whoever understands the problem, and not in the instruction stream.</p>
<p>I confess there was a moment when I thought the bill would close on the legacy side: finally a tool capable of dealing with the COBOL that haunts banks and governments to this day. After these months, I think that problem will take much longer than it seemed. There&rsquo;s no giant COBOL open source community. Almost all of that code is private, and truly private: banks, insurers, governments. There&rsquo;s no training material, or very little of it. The model doesn&rsquo;t know what to do with code it has never seen, and production COBOL is almost entirely code it has never seen.</p>
<p>To close the reasoning: <strong>LLMs can replicate what&rsquo;s open, but they can&rsquo;t guess what&rsquo;s private.</strong> And that brings back the hypothesis from the introduction. If these models are good at code, it&rsquo;s because decades of open source community fought to keep code free, public, and reusable, and providers trained on top of that heritage without paying a cent of licensing for it. The least the AI industry owes the world is to acknowledge that: its capability is, to a large extent, our generosity compiled.</p>
<p>The final irony: the programmer who survives is precisely the one who knows how to build oracles, define tests, hunt down obscure references, and recognize when the agent started spinning. In other words, the skill the machine needs most from you is the one it can least learn from its training. For now, that&rsquo;s not a threatened job. It&rsquo;s a different job.</p>
]]></content:encoded><category>llms</category><category>coding-agents</category><category>retrocomputing</category><category>software-engineering</category></item><item><title>Stop Making Excuses and Ship More! With AI, the Premise Changed. Get It.</title><link>https://akitaonrails.github.io/en/2026/09/22/stop-making-excuses-and-ship-more-the-premise-changed/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/09/22/stop-making-excuses-and-ship-more-the-premise-changed/</guid><pubDate>Tue, 22 Sep 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;Today I fired off a string of posts on X hammering this point, and, like every time I talk about fast deploys, a bunch of people read it as &amp;ldquo;this guy is saying to ship any garbage to production without testing.&amp;rdquo; That&amp;rsquo;s not it. Let me lay it out calmly, because the argument is narrower and more boring than the version that spreads panic.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It&amp;rsquo;s better to deploy faster, because fixing things afterward became fast and cheap.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Today I fired off a string of posts on X hammering this point, and, like every time I talk about fast deploys, a bunch of people read it as &ldquo;this guy is saying to ship any garbage to production without testing.&rdquo; That&rsquo;s not it. Let me lay it out calmly, because the argument is narrower and more boring than the version that spreads panic.</p>
<blockquote>
  <p>It&rsquo;s better to deploy faster, because fixing things afterward became fast and cheap.</p>

</blockquote>
<p>That sentence is the whole article. Everything else is just me justifying why it&rsquo;s true now, even though it wasn&rsquo;t true a few years ago, and showing how I&rsquo;ve been acting on it myself since February.</p>
<h2>The Premise Changed, and Most People Haven&rsquo;t Noticed<span class="hx:absolute hx:-mt-20" id="the-premise-changed-and-most-people-havent-noticed"></span>
    <a href="#the-premise-changed-and-most-people-havent-noticed" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I started saying this <a href="https://x.com/AkitaOnRails/status/2102436061083267441"target="_blank" rel="noopener">on X</a> like this:</p>
<blockquote>
  <p>Fixing got cheap. Stop holding things back. Just ship it and fix it after. &ldquo;Ugh, but I risk shipping a bug.&rdquo; The wrong premise is thinking that staring at every line is going to make any difference at scale. And idle code doesn&rsquo;t improve on its own. Only used code lets you find out if it needs improving.</p>

</blockquote>
<p>And I followed up <a href="https://x.com/AkitaOnRails/status/2102476036709450066"target="_blank" rel="noopener">in the second post</a>, because the first read of anything I write in 280 characters always comes back angrier and more literal than intended:</p>
<blockquote>
  <p>Get the concept (this is an X post, not a blog post, stop reading it literally). I&rsquo;m not saying AI doesn&rsquo;t produce bugs. It does, obviously. But at this point, not more than you, the human, also produce. [&hellip;] The old excuse was that when a bug ships to production, it sits there longer than it should, because it never becomes a sprint priority to fix. Now THAT&rsquo;S NOT TRUE ANYMORE, the premise changed: fixing the bug afterward is FAST and CHEAP.</p>

</blockquote>
<p>That&rsquo;s the entire point. It isn&rsquo;t about AI being infallible. It&rsquo;s about the cost of fixing a mistake having collapsed, while most teams still run their review process as if that cost were still high.</p>
<p>With today&rsquo;s frontier models, Opus, Sol, K3, and company, the LLM makes fewer mistakes than most human developers I&rsquo;ve seen go through a code review. When it does err, the overwhelming majority of the time the mistake isn&rsquo;t the model&rsquo;s. It&rsquo;s whoever was driving the harness. The LLM does what you ask, and it doesn&rsquo;t do what you didn&rsquo;t ask.</p>
<p>If you don&rsquo;t know what to ask for, you won&rsquo;t produce anything useful, no matter how good the model is. That&rsquo;s exactly why only a real software engineer can get real production-grade software out of an agent: someone has to know how to break down the problem, validate the result, and recognize when the request itself was poorly framed. AI doesn&rsquo;t replace that judgment. It multiplies what that judgment already knew how to do.</p>
<h2>This Should Have Been the Norm for 25 Years<span class="hx:absolute hx:-mt-20" id="this-should-have-been-the-norm-for-25-years"></span>
    <a href="#this-should-have-been-the-norm-for-25-years" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The discomfort I feel with bureaucratic review processes isn&rsquo;t new, and the industry should have solved it a long time ago. Kent Beck and Ron Jeffries invented Extreme Programming in 1997, on Chrysler&rsquo;s C3 project, and Beck published the book in 1999 with continuous integration, TDD, and small-batch delivery as core practices. In February 2001, seventeen developers locked themselves in a resort in Snowbird, Utah, and walked out with the Agile Manifesto. That&rsquo;s twenty-five years ago.</p>
<p>Martin Fowler&rsquo;s mantra for continuous integration has always been &ldquo;if it hurts, do it more often,&rdquo; and the book <em>Continuous Delivery</em>, by Jez Humble and Dave Farley, formalized the idea that small, frequent deploys reduce risk, not increase it. DORA&rsquo;s annual State of DevOps reports have documented this with numbers for over a decade:</p>
<ul>
<li>elite teams: deploy on demand, multiple times a day;</li>
<li>low-performing teams: deploy once a month, sometimes once every six months.</li>
</ul>
<p>And the finding that&rsquo;s hardest to swallow if you&rsquo;re afraid of deploying fast: historically, more frequent deploys <strong>do not increase</strong> the failure rate. <em>They reduce it.</em> Teams that deploy more, on average across the last decade of reports, fail less and recover faster when they do fail.</p>
<p>Even with all of this documented, measured, and published for over a decade, a large chunk of the industry keeps damming up change into big batches, with slow manual review, and calling that &ldquo;prudence.&rdquo; It was never a settled matter even inside the agile community itself: in 2014, Kent Beck, Martin Fowler, and DHH (Rails&rsquo; creator) spent weeks publicly debating whether TDD was dead, without reaching a clean consensus. In other words, the very community that invented these practices still argues today about how much rigor is enough.</p>
<p>What I&rsquo;m saying is simpler than that whole debate: whatever your testing rigor level is, an LLM reviewing your diff before merge is fast, and reverting a bad deploy became trivial with containers and git. The math that&rsquo;s left is: how much time do you spend holding back code, versus how much time it would take to revert if it went wrong? If the second number is smaller, you&rsquo;re paying a toll for fear, not for prudence.</p>
<h2>This Isn&rsquo;t &ldquo;Ship Anything&rdquo;<span class="hx:absolute hx:-mt-20" id="this-isnt-ship-anything"></span>
    <a href="#this-isnt-ship-anything" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is the part that always gets lost: I&rsquo;m not saying to deploy with zero engineering practice underneath. I&rsquo;m saying the opposite. This is only safe <strong>after</strong> you already have basic software engineering working, which I document exhaustively on this blog for years: automated tests, CI running on every push, real revert capability, observability to know when something broke.</p>
<blockquote>
  <p>80/20. Ask the LLM to do a minimal review and ship it. If it breaks, revert (that&rsquo;s what containers, git, etc. are for). If you can&rsquo;t revert, your infra sucks, go fix it yesterday.</p>

</blockquote>
<p>That last sentence is the whole filter. If your infrastructure doesn&rsquo;t let you revert fast, the problem isn&rsquo;t deploy speed. It&rsquo;s your infrastructure, and it needs fixing before any conversation about going faster. Nobody gets a pass to skip having real rollback capability. What changes is that, once you have it, holding back code waiting on human review became pure waste.</p>
<h2>The Minimum Recommended Environment<span class="hx:absolute hx:-mt-20" id="the-minimum-recommended-environment"></span>
    <a href="#the-minimum-recommended-environment" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;ve already said &ldquo;basic software engineering&rdquo; several times in this piece without spelling out what that means in practice. Here&rsquo;s the list, no hedging. Nothing on it is optional, and none of it got expensive to maintain:</p>
<ul>
<li>unit tests covering isolated logic, TDD or not, as long as they exist;</li>
<li>scenario and integration tests covering concurrency, partial failure, and conflict, the things unit tests alone don&rsquo;t catch;</li>
<li>CI that builds and tests on every change, ideally for every platform you actually ship to;</li>
<li>hostile automated review before merge, like <code>pr-audit</code>, which treats a contributor&rsquo;s claim as unverified until proven;</li>
<li>feature flags to control the blast radius of who sees a change first;</li>
<li>real staging as a gate before any real user sees the change;</li>
<li>fast revert capability, with git and containers, and observability to know when something broke;</li>
<li>in compliance, financial, or health environments, one extra layer: heavier scenario testing, and more than one lock per identified failure mode.</li>
</ul>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/deploy-environment-diagram-en.png" alt="Diagram of the minimum pipeline for fast, safe deploys: commit, automated tests, cross-platform CI, feature flag, staging, and production, with a rollback arrow looping from production back to tests and a feedback arrow looping from production back to commit"  loading="lazy" /></p>
<p>What the diagram tries to make clear is that speed and safety aren&rsquo;t competing with each other here. They come from the same stack of layers, and each layer in that stack is what gives you permission to stop holding back code waiting for manual review.</p>
<h2>Where This Math Doesn&rsquo;t Work Out<span class="hx:absolute hx:-mt-20" id="where-this-math-doesnt-work-out"></span>
    <a href="#where-this-math-doesnt-work-out" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I&rsquo;m not going to pretend I didn&rsquo;t get good pushback in the replies on X, because several of them hit on something real that I skipped while writing fast for Twitter. It&rsquo;s worth answering for real instead of just validating whoever agreed.</p>
<ol>
<li>
<p><strong>Reverting code doesn&rsquo;t undo a consequence that already left your system.</strong> If the bug already processed a wrong payment, moved money to the wrong account, or controlled an elevator, reverting the deploy doesn&rsquo;t undo what already happened. &ldquo;If you can&rsquo;t revert, your infra sucks&rdquo; covers the technical side of deploy and rollback, not the case where the damage already left your system before you could react. Payments, health, and anything touching physical safety fall into this category, and there the review bar before shipping has to get stricter, not looser, because the cost of reverting stopped being technical.</p>
</li>
<li>
<p><strong>Databases break the &ldquo;reverting is easy&rdquo; analogy.</strong> Reverting a binary is trivial. Reverting a migration that already ran in production, already moved data, and maybe already dropped a column, is not. There&rsquo;s a pattern for this, <em>expand-contract</em>: add the new shape without removing the old one, wait for every consumer to migrate, only then remove the old one. This isn&rsquo;t about trusting AI more or less. It&rsquo;s about state that doesn&rsquo;t undo with <code>git revert</code>.</p>
</li>
<li>
<p><strong>Libraries and public packages don&rsquo;t run on the same math as a closed application.</strong> When my own code breaks, I fix it on the spot. When a lib that hundreds of projects depend on breaks, the people who eat the cost are people who don&rsquo;t even know I exist, with a much slower reaction time than mine. That&rsquo;s why my <code>pr-audit</code> skill, which I describe further below, treats semver classification as a version gate: a breaking change ships in a major, never in a patch, because there the real review isn&rsquo;t about catching a bug, it&rsquo;s about not blindsiding whoever depends on you without warning.</p>
</li>
<li>
<p><strong>App stores knock the speed down too</strong>, and my own frank_yomik example hides that instead of solving it. It publishes the APK straight as a GitHub Release, without going through the Play Store. If you distribute through Google&rsquo;s or Apple&rsquo;s official store, their review sits in the middle of your process, and you don&rsquo;t control how long it takes. This article is about removing the bottleneck that&rsquo;s inside your own control. The app store isn&rsquo;t inside it.</p>
</li>
<li>
<p><strong>The most honest point of all: every piece of evidence I bring here comes from projects where I&rsquo;m the sole owner and I decide alone.</strong> I have no way to prove, with my own numbers, that the same pace works inside a large company, with Legal, Compliance, and a product manager in the middle. What I&rsquo;d argue generalizes is the principle, not the exact pace: build real revert capability, automate the mechanical checking, and measure the result against real usage, not against your own sense of security. How much speed you apply on top of that depends on how much a mistake costs in your specific context, and nobody from outside answers that for you.</p>
</li>
</ol>
<p>On the first point specifically, it&rsquo;s worth opening up more, because &ldquo;stricter bar&rdquo; can&rsquo;t stay vague. It means real scenario testing, not just unit tests. ai-memory itself has more than a thousand integration tests spread across its modules, and most of them don&rsquo;t ask &ldquo;does this function return the right value?&rdquo; They ask things like:</p>
<ul>
<li>what happens when two writes race for the same file at the same time;</li>
<li>what happens when the disk fails mid-way through a delete operation;</li>
<li>what happens when two sessions try to consolidate the same data at the same time;</li>
<li>what happens when the process dies before it finishes writing.</li>
</ul>
<p>An environment with compliance, financial, or health requirements needs this level of testing: every identified failure mode has to have a mitigation, and more than one layer of checking, never a single lock.</p>
<p>And here&rsquo;s the point that holds up this article&rsquo;s entire argument even in those environments: this bar doesn&rsquo;t change depending on who wrote the code. A human developer&rsquo;s mistake takes down a payment system just as well as an LLM&rsquo;s mistake does, and nobody gets a free pass for loose review just because a senior human wrote the line. The bar of scenario testing and double-layer protection is about the problem domain, not about who, or what, produced the code.</p>
<p>Feature flags and staging environments are the other half of that double layer, and they also changed cost. Flags used to be a pain to maintain: dozens of them, each one needing to be born, monitored, and removed once it stops making sense, and it&rsquo;s easy to pile up dead flags nobody remembers the purpose of. That got cheap too: I ask the agent to audit every flag in the project, find which ones have been stuck at the same value for months, and generate the cleanup PR. Maintenance that used to take a whole afternoon turns into a task that takes minutes.</p>
<p>With that solved, the layered defense becomes simple to assemble: staging to catch obvious errors before any user sees them, feature flags to limit the blast radius of who sees the change first, and scenario tests to cover what staging alone doesn&rsquo;t simulate. Three layers, none of them expensive to maintain.</p>
<h2>How I&rsquo;ve Been Doing This Since February<span class="hx:absolute hx:-mt-20" id="how-ive-been-doing-this-since-february"></span>
    <a href="#how-ive-been-doing-this-since-february" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I started accelerating my output with AI in February this year, and the question I wanted to answer was simple: can you use the new generation of frontier LLMs to code full-time, at real production pace, not just for a demo? The answer turned into my <a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/">biggest LLM benchmark</a>, more than 110 articles published since then, and this, as I <a href="https://x.com/AkitaOnRails/status/2102176881139138806"target="_blank" rel="noopener">wrote recently</a>:</p>
<blockquote>
  <p>Since February I&rsquo;ve produced more than 110 articles on my blog. Most of it came from the 30+ open source projects I built and maintain (43 if you count other people&rsquo;s contributing projects, like Flea or Omarchy). 1.6 million lines of code produced. By myself (with agents, of course).</p>

</blockquote>
<p>The projects I use the most in my own day to day, and that survived the &ldquo;am I still maintaining this months later&rdquo; test, are three:</p>
<ul>
<li><a href="https://aijail.io"target="_blank" rel="noopener">ai-jail</a>: an OS sandbox for my coding agents;</li>
<li><a href="https://aimemorybr.netlify.app"target="_blank" rel="noopener">ai-memory</a>: long-term memory for the agents;</li>
<li>ai-usagebar: a bar widget to monitor LLM usage.</li>
</ul>
<p>Two of them already have a decent landing page:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/aijail-hero.png" alt="ai-jail’s landing page, an operating system sandbox for AI agents"  loading="lazy" /></p>
<p>ai-jail went live today. ai-memory is up at <a href="https://aimemorybr.netlify.app"target="_blank" rel="noopener">aimemorybr.netlify.app</a> because the <code>aimemory.io</code> domain transfer will still take a few more days.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/aimemory-hero.png" alt="ai-memory’s landing page, a long-term memory system for coding agents"  loading="lazy" /></p>
<p>Both run on Netlify. That&rsquo;s not a tool recommendation, it&rsquo;s just where I already had an account set up. The point that matters here isn&rsquo;t the platform, it&rsquo;s the flow: to put a new version live, the command is <code>git push</code>. There&rsquo;s no manual step between the commit and the updated site.</p>
<p>My newsletter, <a href="https://themakitachronicles.com"target="_blank" rel="noopener">The M.Akita Chronicles</a>, already has more than 15,000 subscribers:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/themakitachronicles-hero.png" alt="The M.Akita Chronicles newsletter’s subscription page"  loading="lazy" /></p>
<p>And the newest effort is converting blog posts into podcast episodes. The whole thing runs inside Discord, with a bot that generates the preview and waits for my command:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/discord-podcast-1.png" alt="Discord bot announcing that a podcast episode preview is ready, with commands to publish, schedule, or reject it"  loading="lazy" /></p>
<p>One <code>/publish-podcast 2</code> later, the episode is published and the transcript already has a URL:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/09/22/discord-podcast-2.png" alt="Discord bot confirming that the podcast episode was published, with a link to the transcript"  loading="lazy" /></p>
<p>From finished text to published audio, the entire process is me typing a slash command in a Discord conversation. That&rsquo;s the effort bar I use to measure whether a process is good enough: if publishing takes more than one command, some step is still too manual.</p>
<h3>Does the Quality of the Generated Code Hold Up?<span class="hx:absolute hx:-mt-20" id="does-the-quality-of-the-generated-code-hold-up"></span>
    <a href="#does-the-quality-of-the-generated-code-hold-up" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Every single day someone shows up worried that automating too much with LLMs is going to dump bad code, or worse, dangerous code, into production without anyone noticing. Given that the engineering underneath is the one I described in the previous section, automated tests, CI, feature flags, staging, fast rollback, my actual experience doesn&rsquo;t back up that fear.</p>
<p>I&rsquo;ve produced more than 1.6 million lines of code with LLMs since February. <strong>I have never once seen an LLM do something randomly destructive that I didn&rsquo;t ask for.</strong> Quite the opposite: in my <a href="/en/2026/09/15/new-llm-benchmark-v4-retesting-all-top-llms-part-1/">LLM Benchmark v4</a>, I deliberately built an adversarial scenario, a subagent planting fourteen real CVE-based sabotage items, hidden inside a project that grows across seven sprints. Most of the tested models found and fixed the sabotage on their own, without me having to point it out.</p>
<p>That&rsquo;s anecdotal. It doesn&rsquo;t prove anything beyond what that specific benchmark measures. But after all that volume, the impression that sticks is consistent: today&rsquo;s LLMs produce stable code most of the time, comparable to what a decent human developer would produce, just a lot faster, and on average, more accurate.</p>
<h2>Cross-Platform CI/CD With Zero Effort<span class="hx:absolute hx:-mt-20" id="cross-platform-cicd-with-zero-effort"></span>
    <a href="#cross-platform-cicd-with-zero-effort" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I went and counted how many of my <code>ai-*</code> and <code>frank-*</code> projects (18 repositories total, among the ones with a README) already have automated build and deploy. Fourteen have CI configured, and most of them build and publish to multiple operating system platforms from a push made on a Linux machine.</p>
<p>The most complete example is ai-memory itself: a single tag push triggers a workflow that, in the same run:</p>
<ul>
<li>builds Linux x86_64 and arm64;</li>
<li>builds macOS arm64 and x86_64;</li>
<li>builds Windows x86_64, packaged with PowerShell;</li>
<li>publishes a multi-architecture Docker image;</li>
<li>pushes the package to Arch Linux&rsquo;s AUR.</li>
</ul>
<p>ai-usagebar follows the same build pattern for Linux and Windows, and also publishes two AUR packages, to crates.io, and to a Scoop bucket for Windows.</p>
<p>Then there&rsquo;s frank_yomik, my manga reader with OCR and translation via a local LLM, which is the example I wanted to show that Android fits into the same pipeline. A <code>v*</code> tag push triggers five parallel builds:</p>
<ul>
<li>Linux;</li>
<li>macOS;</li>
<li>Windows;</li>
<li>browser extension;</li>
<li>Android, on an ordinary Linux runner, decoding a signing keystore stored in GitHub secrets and producing the already-signed APK.</li>
</ul>
<p>At the end, one job waits for all five to finish and publishes a single GitHub Release with every binary together. It builds for four different systems, including Android, without ever leaving Linux.</p>
<p>Not everything came out perfect on first look, and I&rsquo;d rather admit that than pretend it&rsquo;s all clean: frank_sherlock and frank_karaoke publish the release as a draft by default, which blocks the automation that follows (like the AUR push) until someone clicks &ldquo;publish&rdquo; by hand. I found this hole while writing this piece.</p>
<p>A neighboring project, frank_scanlation, had already solved a similar but different problem: GitHub suppresses the event that triggers automation when the release itself was created by the bot&rsquo;s own token, so its workflow triggers the next step by hand, via <code>workflow_dispatch</code>, from inside the job itself. Neither fix is hard. I&rsquo;ll apply both to the projects that still need it this week, because that&rsquo;s exactly the kind of accumulated friction this whole article is telling you not to leave sitting.</p>
<p>And speaking of hidden friction: ai-memory has Windows-specific code (NTFS file index, a native PowerShell hook) that only really runs on GitHub&rsquo;s <code>windows-latest</code> runner, about 1000 seconds against 250 on Linux for the same suite. I went as far as setting up a local Windows VM via <a href="https://github.com/dockur/windows"target="_blank" rel="noopener">dockur/windows</a>, thinking I&rsquo;d gain speed.</p>
<p>I didn&rsquo;t: the workload is dominated by disk I/O, and the result came out slower than GitHub&rsquo;s runner for the full suite. It only turned out useful for fast iteration on a single isolated crate. A good tool is one you measure, not one you assume, even when the measurement contradicts your initial bet.</p>
<h2>The Automated Review That Holds All of This Up<span class="hx:absolute hx:-mt-20" id="the-automated-review-that-holds-all-of-this-up"></span>
    <a href="#the-automated-review-that-holds-all-of-this-up" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I already <a href="/en/2026/09/17/talking-about-my-ai-skills/">detailed my review skills</a> in a recent article, so I&rsquo;ll summarize here:</p>
<ul>
<li><code>pr-audit</code> audits every PR before merge with the principle of <strong>evidence over narrative</strong>: nothing the contributor wrote is true until proven, with a hostile checklist for malicious code, exposed credentials, semver breaks, and prompt injection attempts;</li>
<li><code>iss-audit</code> applies the same distrust to issues, never executing a command pasted by whoever reported it;</li>
<li><code>github-resolution</code> turns what got approved into merged code, one ticket at a time, with a regression test written before the fix, and the rule that <em>&ldquo;slop is a defect&rdquo;</em>: no speculative abstraction, no forgotten TODO, no weakening a test just to pass faster.</li>
</ul>
<p>That catches the most expensive category of garbage inside the same diff. It doesn&rsquo;t catch duplication spread across different PRs, because the audit looks at one isolated diff, not the whole project&rsquo;s architecture. That part still depends on me noticing the pattern repeating and telling it to consolidate.</p>
<p>By my estimate, this handles about 99% of review and resolution work on its own, with me supervising and deciding on the cases that are left. But that&rsquo;s not the main point. Automated review is necessary, but it alone proves nothing about whether the software is good.</p>
<h2>What Actually Keeps This Grounded: Real People Using It<span class="hx:absolute hx:-mt-20" id="what-actually-keeps-this-grounded-real-people-using-it"></span>
    <a href="#what-actually-keeps-this-grounded-real-people-using-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s the part I think is the most important in the whole article, and the one that usually gets skipped over: your own review isn&rsquo;t worth as much as you think. What&rsquo;s worth something is published software being used by real people, giving real feedback.</p>
<p>I went and checked ai-memory&rsquo;s real numbers on GitHub, without rounding up to sound impressive:</p>
<ul>
<li><strong>issues: 287 total, 249 came from people who aren&rsquo;t me</strong>, 86.8% of the total;</li>
<li><strong>pull requests: 562 total, 371 from outside</strong>, 10 of them Dependabot&rsquo;s automatic bumps and the other 361 from real people, roughly 121 distinct contributors.</li>
</ul>
<p>The two people who contributed the most alongside me sent 64 and 43 PRs each, on their own.</p>
<p>That&rsquo;s what keeps the project grounded. It isn&rsquo;t the LLM being a good reviewer, nor me being a good reviewer. It&rsquo;s hundreds of real people actually using it, finding cases I never imagined, and sending real issues and PRs. Without that, my automated review skills would just be confirming my own assumptions back to me, no matter how rigorous the rules look on paper.</p>
<p>It doesn&rsquo;t matter how good the AI you use is: if nobody uses what you published, you have no real feedback, and without real feedback there&rsquo;s no real improvement. There&rsquo;s only your own opinion about your own work, validated by nobody but yourself.</p>
<h2>The Conclusion That Matters<span class="hx:absolute hx:-mt-20" id="the-conclusion-that-matters"></span>
    <a href="#the-conclusion-that-matters" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Real software engineering <strong>needs</strong> fast, near-zero-effort deploy. It isn&rsquo;t optional, it isn&rsquo;t a luxury for a big team, it&rsquo;s a prerequisite. And if reviewing and shipping to production is taking longer than it took to develop the feature, you&rsquo;re not just wrong.</p>
<p><strong>You are the bottleneck.</strong></p>
<p>Excuses and justifications aren&rsquo;t worth anything at that point. Use that time to fix yourself and your process, because only real-world results matter, not the sense of security from having reviewed a diff line by line that nobody else is ever going to look at again.</p>
<p>If the premise changed, I change today. Not tomorrow, not next week. And if yours hasn&rsquo;t changed yet, the problem isn&rsquo;t AI. It&rsquo;s you holding back your own work.</p>
]]></content:encoded><category>devops</category><category>software-engineering</category><category>coding-agents</category><category>vibe-coding</category></item><item><title>I Had Fable 5 Analyze the Code of TikTok, Clash of Kings and Gov.br - Understanding Fingerprinting</title><link>https://akitaonrails.github.io/en/2026/07/08/fable-5-analyze-tiktok-clash-of-kings-govbr-fingerprint/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/07/08/fable-5-analyze-tiktok-clash-of-kings-govbr-fingerprint/</guid><pubDate>Wed, 08 Jul 2026 16:00:00 -0300</pubDate><description>&lt;p&gt;Let me open with the most important disclaimer in the whole post, because it changes how you should read everything below: &lt;strong&gt;none of this is 100% factual.&lt;/strong&gt; I asked Claude Fable 5 to do a full analysis of the code of three Android apps, but I have no way to guarantee how deep it actually went without re-auditing file by file myself, and that&amp;rsquo;s hundreds of thousands of files per app. So treat this as a panorama, not a forensic report.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Let me open with the most important disclaimer in the whole post, because it changes how you should read everything below: <strong>none of this is 100% factual.</strong> I asked Claude Fable 5 to do a full analysis of the code of three Android apps, but I have no way to guarantee how deep it actually went without re-auditing file by file myself, and that&rsquo;s hundreds of thousands of files per app. So treat this as a panorama, not a forensic report.</p>
<p>And there are more layers of uncertainty. The APKs came from <a href="https://apkpure.com/"target="_blank" rel="noopener">APKPure</a>, not the official Play Store, so there&rsquo;s a chance (small, but real) they were tampered with along the way. And even after decompiling, there&rsquo;s a pile of binary libraries and SDKs we didn&rsquo;t crack open, assuming they&rsquo;re common libs like Google Firebase or Google Play Services. This mini-project was, at bottom, my own curiosity: I wanted to know what&rsquo;s inside these apps we install without thinking.</p>
<h2>Decompiling Android is way too easy<span class="hx:absolute hx:-mt-20" id="decompiling-android-is-way-too-easy"></span>
    <a href="#decompiling-android-is-way-too-easy" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Something a lot of people don&rsquo;t know: an Android app is <strong>way too easy</strong> to decompile. The APK is basically a zip. You unpack it, run a tool like <a href="https://github.com/skylot/jadx"target="_blank" rel="noopener">jadx</a> on the <code>.dex</code> files, and readable Java comes out the other end. If the app is Flutter, you can reconstruct the Dart with <a href="https://github.com/worawit/blutter"target="_blank" rel="noopener">Blutter</a>. If it&rsquo;s a Cocos2d-x game, the Lua scripts are often sitting right there in plaintext, no decryption needed.</p>
<p>This does <strong>not</strong> let you recompile the app from the source (class and method names come scrambled by obfuscation, resources are missing, the signing keys are missing). But for <strong>static analysis</strong> it&rsquo;s more than enough: you can read the endpoints the app contacts, the permissions it asks for, the identifiers it assembles, the SDKs it bundles. That&rsquo;s exactly the kind of reading I asked Fable 5 to do.</p>
<p>One honest limit: the most sensitive part usually lives in <strong>native code</strong> (<code>.so</code>), compiled C/C++, which doesn&rsquo;t decompile into anything readable. In TikTok&rsquo;s case, that&rsquo;s precisely where the guts are, and I&rsquo;ll come back to it in a bit.</p>
<h2>&ldquo;If it&rsquo;s free, you&rsquo;re the product&rdquo;<span class="hx:absolute hx:-mt-20" id="if-its-free-youre-the-product"></span>
    <a href="#if-its-free-youre-the-product" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>You&rsquo;ve heard that line a thousand times. It&rsquo;s true, but it almost always stays abstract. Let&rsquo;s make it concrete: how exactly do you become the product? The answer has a technical name, and it&rsquo;s why I picked this angle for the title: <strong>fingerprinting</strong>.</p>
<p>A fingerprint (device fingerprint) is a persistent identifier the platform assembles by combining dozens of signals from your device: phone model, OS version, screen resolution, CPU, RAM, carrier, timezone, language, battery, installed fonts, nearby Wi-Fi and Bluetooth networks, motion sensors, and so on. None of those data points identifies you on its own. But the <strong>combination</strong> of them is so specific it becomes practically unique, an implicit &ldquo;serial number&rdquo; for your device that you never explicitly authorized.</p>
<p>The clever part, and the reason it&rsquo;s so valuable, is the <strong>persistence</strong>. A cookie you can clear. The advertising ID you can reset. But a well-built fingerprint survives you clearing the app&rsquo;s data, survives a reinstall, sometimes survives a factory reset. It re-identifies you as the same person after any attempt to start from scratch. That&rsquo;s why companies want it so badly: with the fingerprint, they cross-reference your behavior across apps, across sessions, across reinstalls, and build a profile that follows you even when you think you wiped your tracks. That profile is the product from the line above. And the product is you.</p>
<p>The three apps I analyzed sit at three very different points on that spectrum, and that&rsquo;s where it gets interesting.</p>
<h2>The three apps<span class="hx:absolute hx:-mt-20" id="the-three-apps"></span>
    <a href="#the-three-apps" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>TikTok: industrial-grade collection, armored against auditing<span class="hx:absolute hx:-mt-20" id="tiktok-industrial-grade-collection-armored-against-auditing"></span>
    <a href="#tiktok-industrial-grade-collection-armored-against-auditing" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The <a href="https://github.com/akitaonrails/tiktok_analysis"target="_blank" rel="noopener">tiktok_analysis</a> was the densest one. Version 46.0.1, 37 dex files, 204 native libraries. The honest conclusion, and the one I think is fairest: <strong>it&rsquo;s not disguised malware, but it is industrial-grade data collection engineered to be hard to audit.</strong></p>
<p>On the reassuring side: no secret servers. Every host the app contacts belongs to TikTok/ByteDance or to a <em>named</em> ad/analytics vendor (Adjust, AppsFlyer, Facebook, Google, ThreatMetrix, Amazon). We found no IMEI or MAC collection, no <code>QUERY_ALL_PACKAGES</code>, no reading of SMS or call logs. US and EU user data goes to dedicated enclaves (the famous Project Texas, with Oracle in Ashburn).</p>
<p>On the worrying side: it collects <strong>a lot</strong>. A persistent device fingerprint, precise location, Wi-Fi and Bluetooth scanning, contacts upload (if you enable &ldquo;find friends&rdquo;), motion sensors, and a check against a fixed list of <strong>91 named apps</strong> to see which ones you have installed (competitors, PayPal, Venmo, Spotify, sister ByteDance apps). Your advertising ID fans out to about 6 third parties.</p>
<p>And the central finding: the fingerprint and much of the telemetry are assembled in <strong>native code</strong> (<code>libmetasec_ov.so</code> and a stack of crypto libs), with each request&rsquo;s signature going out encrypted. Meaning, even decompiling the whole app, you can&rsquo;t tell exactly which bytes leave the device.</p>
<p>To attack that black box specifically, one of the analyses ran a <code>strings</code> sweep over the 204 native libraries (the repo&rsquo;s <a href="https://github.com/akitaonrails/tiktok_analysis/blob/main/docs/13-native-library-analysis.md"target="_blank" rel="noopener">report 13</a>), and there <code>libmetasec_ov.so</code> gives the game away in plaintext. You can read the fields it collects (<code>secdeviceid</code>, <code>device_id</code>, battery level and state, screen size), the function that signs the requests, the ByteDance telemetry endpoints baked into the binary, and even the scheduler that fires the upload on a timer. What the Java layer only let you infer, the native code confirms. It&rsquo;s a fingerprinting machine, plain and simple.</p>
<p>And the same sweep went looking for exactly what everyone&rsquo;s afraid of finding (keylogger, screen recorder, input injection) and found none of it. The scary keyword hits were false friends: <code>keylog</code> was the TLS debug log, <code>screenshot</code> was the video player and C2PA content authenticity, <code>getevent</code> was a media method name. There&rsquo;s anti-debug and frida/xposed detection, yes, but that&rsquo;s the app inspecting itself from the inside, standard security-library behavior, not snooping on the rest of the phone.</p>
<p>So the opacity is still the point, just sharper: we know what <code>libmetasec_ov</code> does and where it reports to, and what stays locked are the exact payload bytes, which only native disassembly or live capture would reveal. &ldquo;Not proven to be spyware&rdquo; and &ldquo;collects far more than it needs, opaquely&rdquo; are both true at once, now with plaintext confirmation that the fingerprinting engine is real.</p>
<h3>Clash of Kings: the game is honest, the monetization is predatory<span class="hx:absolute hx:-mt-20" id="clash-of-kings-the-game-is-honest-the-monetization-is-predatory"></span>
    <a href="#clash-of-kings-the-game-is-honest-the-monetization-is-predatory" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The <a href="https://github.com/akitaonrails/clash_of_kings_analysis"target="_blank" rel="noopener">clash_of_kings_analysis</a> was a pleasant surprise on the privacy front. It&rsquo;s a strategy game by China&rsquo;s Elex, a Cocos2d-x engine with almost 12,000 Lua scripts in plaintext. And check it out: <strong>no GPS, no camera, no microphone, no contacts, no SMS, no IMEI.</strong> The two most sensitive reads it <em>can</em> do (installed-app list and Wi-Fi MAC) are both consent-gated. No malware, no hidden exfiltration.</p>
<p>The privacy concern is the same villain as TikTok in a simpler version: a <strong>reset-resistant device UUID</strong>, written in four different places (system settings, preferences, SD card, and cloud backup) precisely to survive a reinstall. Plus telemetry to China-region infra and to Tencent Bugly.</p>
<p>Where Clash of Kings really weighs on you is the <strong>monetization</strong>. It&rsquo;s the complete predatory free-to-play kit: loot boxes (the hero &ldquo;luck draws&rdquo;), countdown-timer offers to manufacture FOMO, VIP pay-to-win tiers, cumulative-spend ladders with leaderboards for who spends the most (the whale hunt), a monthly card, the whole package. The half-full side: drop-rate odds are disclosed in-game, and the Chinese anti-addiction and real-name system for minors is present. Decent privacy, wallet in danger.</p>
<h3>Gov.br: the example of how to do it right (with one caveat)<span class="hx:absolute hx:-mt-20" id="govbr-the-example-of-how-to-do-it-right-with-one-caveat"></span>
    <a href="#govbr-the-example-of-how-to-do-it-right-with-one-caveat" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The <a href="https://github.com/akitaonrails/govbr_analysis"target="_blank" rel="noopener">govbr_analysis</a> is the counterpoint, and it actually left me relieved. It&rsquo;s the official digital-identity app of the Brazilian government, built in Flutter (I reconstructed the Dart with Blutter). By the standards of a mandatory government app, it&rsquo;s <strong>well-built and privacy-respectful</strong>: traffic is almost all to <code>*.gov.br</code>, SERPRO and Dataprev, TLS is certificate-pinned, credentials sit in Android&rsquo;s encrypted KeyStore, the ICP-Brasil signing uses FIPS-grade crypto, and the app hardens hard against rooted or tampered devices. And most importantly: <strong>no commercial ad trackers.</strong> No Segment, Adjust, AppsFlyer, or Meta.</p>
<p>The two honest caveats: it bundles <strong>Google Firebase</strong> (Analytics, Crashlytics, Messaging, Remote Config) with collection on by default and the ad-id permission declared, which is the lazy default of nearly every app today; and your <strong>proof-of-life selfie gets uploaded to the government backend</strong> for identity matching, which is inherent to the purpose and doesn&rsquo;t go to third parties. The one item only live traffic capture would settle is whether your CPF gets attached to the Crashlytics reports. But overall, if every app were built with this kind of care, this post wouldn&rsquo;t exist.</p>
<h2>What almost every app carries along<span class="hx:absolute hx:-mt-20" id="what-almost-every-app-carries-along"></span>
    <a href="#what-almost-every-app-carries-along" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Cross-referencing the three (and this is where the pattern shows up), you can see what tends to come bundled in the app you download, even in a government app:</p>
<ul>
<li><strong>Google Firebase / Play Services</strong>: present in all three. Analytics, Crashlytics (error reporting), Cloud Messaging (push), Remote Config. It&rsquo;s so ubiquitous it became invisible infrastructure. And it ships with collection on by default.</li>
<li><strong>Ad attribution stack</strong>: Adjust, AppsFlyer, Branch.io, Singular, Facebook SDK. The job is to link &ldquo;you clicked this ad&rdquo; to &ldquo;you installed and used the app.&rdquo; TikTok had a redundant pileup: five of these at once, each collecting your ad-id independently.</li>
<li><strong>Ad networks</strong>: Google&rsquo;s AdMob/DoubleClick, and in the games&rsquo; case the ad-mediation ecosystem.</li>
<li><strong>Region-specific analytics/crash</strong>: Tencent Bugly in Clash of Kings is the example, telemetry going to Chinese infra.</li>
<li><strong>A specialized fingerprint/anti-fraud service</strong>: the most striking case was <a href="https://risk.lexisnexis.com/products/threatmetrix"target="_blank" rel="noopener">LexisNexis&rsquo;s ThreatMetrix</a> in TikTok: a company whose business literally <em>is</em> building device fingerprints for fraud detection. Legit for anti-fraud use, but it&rsquo;s literally a fingerprint-as-a-service machine, embedded.</li>
</ul>
<p>The takeaway is that &ldquo;product&rdquo; isn&rsquo;t assembled by a single company. It&rsquo;s five, six, seven partners, each grabbing a piece, and the fingerprint stitching it all back into one profile. Nobody&rsquo;s spying on you alone. It&rsquo;s a group effort.</p>
<h2>What you can do<span class="hx:absolute hx:-mt-20" id="what-you-can-do"></span>
    <a href="#what-you-can-do" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>You won&rsquo;t zero this out short of leaving the internet, but you can cut the exposed surface down a lot:</p>
<ul>
<li><strong>Don&rsquo;t consent to what isn&rsquo;t needed.</strong> GPS is the big one. A video app doesn&rsquo;t need your precise location to work. Deny it, or grant only &ldquo;while using the app.&rdquo; Same goes for contacts, microphone, and camera: only allow them when the feature you want to use right now requires it.</li>
<li><strong>Reset your advertising ID now and then</strong> (and on Android, you can choose to &ldquo;delete advertising ID&rdquo; outright). It doesn&rsquo;t kill the fingerprint, but it breaks part of the trail.</li>
<li><strong>Be suspicious of &ldquo;find friends.&rdquo;</strong> That&rsquo;s the contacts-upload trigger. You hand over the entire social network of people who trusted you with their number.</li>
<li><strong>Prefer the web version when you can.</strong> A browser with a tracker blocker exposes less fingerprint than the native app, which has access to sensors and signals the browser doesn&rsquo;t give up.</li>
<li><strong>Accept the trade-off consciously.</strong> The point isn&rsquo;t to go paranoid and uninstall everything. It&rsquo;s to know the price. If you find TikTok fun enough to pay for it with your fingerprint, great, that&rsquo;s an informed decision. The problem is paying without knowing you&rsquo;re paying.</li>
</ul>
<h2>The point of the post<span class="hx:absolute hx:-mt-20" id="the-point-of-the-post"></span>
    <a href="#the-point-of-the-post" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>None of this proves or disproves that a specific app is 100% safe. Static analysis shows what the app is <strong>capable</strong> of doing, what&rsquo;s built inside it; only live traffic capture, with a rooted device and a MITM proxy, would show what actually leaves over the network on each request. I didn&rsquo;t go that far. This is a glimpse, not a verdict.</p>
<p>And that&rsquo;s exactly the point. Today, with a good LLM and a free afternoon, any curious developer can crack open their own phone and see what&rsquo;s running inside it, without depending on some tech influencer&rsquo;s opinion or an alarmist headline. The three full analyses are on GitHub (<a href="https://github.com/akitaonrails/tiktok_analysis"target="_blank" rel="noopener">TikTok</a>, <a href="https://github.com/akitaonrails/clash_of_kings_analysis"target="_blank" rel="noopener">Clash of Kings</a>, <a href="https://github.com/akitaonrails/govbr_analysis"target="_blank" rel="noopener">gov.br</a>) with step-by-step instructions to reproduce. Grab an app you use every day and do the same. It&rsquo;s the closest thing to reading the label before you eat that we have in the software world.</p>
]]></content:encoded><category>security</category><category>software-engineering</category></item><item><title>Why LLMs Will Fail at Your Company</title><link>https://akitaonrails.github.io/en/2026/06/24/why-llms-will-fail-at-your-company/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/06/24/why-llms-will-fail-at-your-company/</guid><pubDate>Wed, 24 Jun 2026 12:00:00 -0300</pubDate><description>&lt;p&gt;This post is more speculation than a finished thesis. I am thinking out loud. It may be incomplete. It may be wrong. If you disagree, even better: reply in the comments. I am not trying to sell a new methodology. I want to poke a wound that many people are pretending is not there.&lt;/p&gt;
&lt;p&gt;In the last few months, we started seeing stories about large companies burning absurd amounts of tokens on AI tools without being able to point, clearly, to the matching product value. The most famous case was &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 burning through its annual AI budget in a few months&lt;/a&gt; with heavy use of tools like Claude Code, while executives admitted they still could not draw a straight line between token consumption and value delivered. It is not that nobody used the tool. They used it too much. That is exactly the problem.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post is more speculation than a finished thesis. I am thinking out loud. It may be incomplete. It may be wrong. If you disagree, even better: reply in the comments. I am not trying to sell a new methodology. I want to poke a wound that many people are pretending is not there.</p>
<p>In the last few months, we started seeing stories about large companies burning absurd amounts of tokens on AI tools without being able to point, clearly, to the matching product value. The most famous case was <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 burning through its annual AI budget in a few months</a> with heavy use of tools like Claude Code, while executives admitted they still could not draw a straight line between token consumption and value delivered. It is not that nobody used the tool. They used it too much. That is exactly the problem.</p>
<p>By now, I think most people understand that a one-line prompt solves nothing. The fantasy of typing &ldquo;build me an Uber clone&rdquo; and coming back to a finished product died quickly. The market response was to run in another direction: <strong>spec-driven development</strong>.</p>
<p>Instead of asking for everything in one sentence, you write a specification. Scope, use cases, acceptance criteria, desired architecture, constraints, flows, maybe even a full PRD. Then you hand that to the agent and expect it to implement with less ambiguity.</p>
<p>Is that better than one-shot prompting? Of course. I still think it is wrong.</p>
<h2>The problem is not lack of specification<span class="hx:absolute hx:-mt-20" id="the-problem-is-not-lack-of-specification"></span>
    <a href="#the-problem-is-not-lack-of-specification" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I have been hammering this idea for years, long before LLMs became fashionable. Two of my videos matter for this point: <a href="/2019/10/09/akitando-63-nao-terceirize-suas-decisoes-a-licao-mais-importante-da-sua-vida/">Don&rsquo;t Outsource Your Decisions</a> and <a href="/2019/06/18/akitando-51-esqueca-metodologias-ageis-rated-r/">Forget Agile Methodologies</a>.</p>
<blockquote>
  <p>The summary is simple: most professionals were trained not to decide.</p>

</blockquote>
<p>Think about your own routine. Many people do nothing if there is no backlog ready. They cross their arms, blame management, blame colleagues, blame customers, and wait for someone else to say what should be done. When the task arrives, they execute. When it goes wrong, the requirement was bad. When the customer complains, the priority was wrong. When the product degrades, the process was to blame.</p>
<p>Almost nobody really decides, commits to the decision until the end, and suffers the consequences. Without suffering the consequence, you never learn how to decide.</p>
<p>You do not learn which choices lead to which results. You do not learn how to prioritize. You do not learn how to explain why something needs to exist. You do not learn how to write a specification because you understand the problem; you learn how to fill a template so nobody complains.</p>
<h2>Agile became structured bureaucracy<span class="hx:absolute hx:-mt-20" id="agile-became-structured-bureaucracy"></span>
    <a href="#agile-became-structured-bureaucracy" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The original 2001 principles were crushed. What many companies call Agile today is bureaucracy in shorter cycles.</p>
<p>Everybody knows the classic drawing: backlog, sprint planning, sprint, daily, review, retrospective, next sprint. The <a href="https://scrumguides.org/scrum-guide.html"target="_blank" rel="noopener">Scrum Guide</a> describes the formal events. Diagrams like the <a href="https://commons.wikimedia.org/wiki/File:Scrum_process.svg"target="_blank" rel="noopener">Scrum process on Wikimedia</a> show the pretty circle. In practice, many teams turned it into theater.</p>
<p>An entire day is spent debating what goes into the backlog. The final backlog is rarely what actually needs to be done. It is the consensus of least resistance. Compromise after compromise. Small tasks, small risks, small wins, small failures. Everything designed to avoid too much conflict.</p>
<p>That is not merely mediocre. It is worse: it is the active choice for easy decisions, the kind that will not bring too much criticism your way.</p>
<p>Then the team spends days or weeks implementing that lukewarm backlog. QA comes in, finds problems, tries to fit corrections into the next round. A few more days, maybe two more weeks. A month later, maybe two, the original decision finally starts to show results in production.</p>
<p>But the result is so far away from the decision that almost nobody makes the connection. The team does not feel the real weight of what it decided. The error does not become learning. The process does not improve. The most important part of any continuous improvement system, <strong>kaizen</strong>, never happens.</p>
<blockquote>
  <p>Something else happens instead: everybody learns to stay invisible.</p>

</blockquote>
<p>You learn to please management, not the product. You learn to reduce friction, not solve the problem. You learn to survive the meeting, not deliver value. And when results get worse, upper management reacts in the usual way: more control, more rules, more gates, more committees, more checklists. The machine gets heavier. The team gets more cowardly.</p>
<h2>Then AI arrives<span class="hx:absolute hx:-mt-20" id="then-ai-arrives"></span>
    <a href="#then-ai-arrives" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now the LLM enters the room.</p>
<p>Until recently, the biggest bottleneck in software development was writing and adjusting code. Coding took days or weeks. That is why so much energy went into planning. If the team is going to spend two weeks implementing, spending a day arguing about what goes into the sprint feels reasonable.</p>
<p>But coding tools with LLMs change where the bottleneck sits. They can do a large part of the manual work: generate structure, write basic tests, refactor, adapt APIs, create screens, fill boilerplate, try fixes. It is not perfect, but it is fast enough to change the economics of the process.</p>
<p>It is the old chain and weakest-link problem. You strengthen one link, another one starts limiting the system. Before, the weak link was coding speed. Now, increasingly, it is the human ability to decide, explain, test, and review.</p>
<blockquote>
  <p>The mistake companies make is putting LLMs inside the same broken process and expecting multiplicative gains.</p>

</blockquote>
<p>You take people trained for years not to decide, give them a tool that requires clear decisions, and then act surprised when garbage comes out faster.</p>
<h2>The LLM reflects the user<span class="hx:absolute hx:-mt-20" id="the-llm-reflects-the-user"></span>
    <a href="#the-llm-reflects-the-user" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>A coding agent needs you to explain the goal, the context, the constraints, the priorities, what must not break, which paths are acceptable, and how success should be measured. It needs direction.</p>
<p>Now go interview your team. Ask each person:</p>
<ul>
<li>what is the real goal of what you are doing?</li>
<li>how does this improve the product?</li>
<li>which metric should move?</li>
<li>what trade-off are you accepting?</li>
<li>what must not break?</li>
<li>which unhappy paths need to be tested?</li>
</ul>
<p>Many people cannot answer. They know which ticket they picked up. They know they need to close a few tasks per week. They know there is a daily tomorrow. They do not know why the work matters.</p>
<p>Then you give that person an LLM.</p>
<p>The result is not magic. It is a mirror. Bad code ten times faster. Bugs ten times faster. Angry customers ten times faster. Giant pull requests ten times faster. The problem is not only AI. AI amplifies the lack of direction.</p>
<p>Spec-driven development tries to fix this with documentation. But if the specification is born from the same cowardly process, it only documents the cowardice. A PRD written by someone who does not understand the decision is still bad. Acceptance criteria written to avoid conflict still dodge the real problem. The agent will execute a bad decision better.</p>
<h2>The process has to change<span class="hx:absolute hx:-mt-20" id="the-process-has-to-change"></span>
    <a href="#the-process-has-to-change" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>My hypothesis is that the whole process has to be redesigned around the new constraint. Again: speculation. I do not have enough data to prove it. But looking at the kind of failure I see in companies, and the kind of success I see in intense individual use, my theory is this:</p>
<blockquote>
  <p><strong>throw away the sprint ritual as the central unit of work.</strong></p>

</blockquote>
<p>It makes no sense to keep days of planning, days of review, and weeks of coding as if the bottleneck were still the same. With LLMs, the cycle needs to be shorter. Much shorter. Ideally, inside the same day.</p>
<p>In my ideal setup, each programmer with an LLM should pair directly with a Product Manager for short blocks of work. The PM&rsquo;s job is to close the gap the programmer usually has: goal, priority, constraint, business context, expected result. That needs to enter the prompt and the conversation with the agent.</p>
<p>But because coding is no longer the main bottleneck, the PM does not need to wait two weeks to see a result. They can sit with the programmer for a morning, implement a small functional slice, adjust on the spot, decide on the spot, cut scope on the spot. Then the PM rotates to another programmer.</p>
<p>Then QA comes in. Not as a distant phase, but as pairing inside the same cycle. QA closes another common gap: testing only the happy path, forgetting error cases, not automating enough, not thinking about behavioral regression. QA&rsquo;s job becomes helping turn the implementation into something verifiable now, while the context is still hot.</p>
<p>Then the Tech Lead comes in. Also on the same day, not two weeks later. They look at what changed and close the third gap: unnecessary complexity, poor design, hidden regression, missing documentation, obvious vulnerability, performance getting worse, coupling that did not need to exist.</p>
<p>Build, check, adjust. In real time.</p>
<figure>
  <a href="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/06/24/why-llms-fail/llm-pairing-rotation-en.svg">
    <img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/06/24/why-llms-fail/llm-pairing-rotation-en.svg" alt="Diagram comparing the common sprint flow, where decisions are far from results, against a rotating LLM pairing flow with PM, programmer, QA and Tech Lead on the same day." />
  </a>
  <figcaption>The bottleneck changes: less time waiting for a sprint to end, more short cycles of decision, implementation, testing and review.</figcaption>
</figure>
<h2>The new role of each person<span class="hx:absolute hx:-mt-20" id="the-new-role-of-each-person"></span>
    <a href="#the-new-role-of-each-person" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This model demands more responsibility, not less.</p>
<p>The programmer stops being the person who only picks up tickets. They need to learn how to turn a goal into executable instruction. They need to discuss trade-offs. They need to understand why they are coding. They need to use the LLM as a tool for execution and investigation, not as an excuse to turn the brain off.</p>
<p>The PM stops being a backlog factory. They need to decide with the programmer, looking at the live product. This is not writing a card and disappearing. It is watching the thing come to life, adjusting it, cutting it, prioritizing it, and accepting the consequence.</p>
<p>QA stops being the person who receives the package at the end and returns defects. QA enters early to turn expected behavior into verification. If the LLM accelerates code, QA needs to accelerate learning about risk.</p>
<p>The Tech Lead stops being a late PR stamp. They enter while change is still cheap. They help keep things simple, remove complexity, protect architecture, documentation, security, and performance.</p>
<p>None of this works if everybody keeps using AI as expensive autocomplete inside the same bureaucratic Scrum.</p>
<h2>Why companies will not like this<span class="hx:absolute hx:-mt-20" id="why-companies-will-not-like-this"></span>
    <a href="#why-companies-will-not-like-this" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Because this process exposes decisions.</p>
<p>In the current process, the decision dissolves. It was the backlog. It was the PM. It was refinement. It was the sprint. It was the team. It was a dependency. It was the customer changing their mind. Everybody can hide.</p>
<p>In a short LLM cycle, the decision appears quickly. You said this was the priority? Then implement it now. You said this was the acceptance criterion? Then test it now. You said this design was good? Then review it now. If it is wrong, the consequence shows up the same day.</p>
<p>That is uncomfortable. But that is how you learn.</p>
<p>Without nearby consequence, decision becomes theater. With nearby consequence, decision becomes training.</p>
<h2>How to implement this without burning money<span class="hx:absolute hx:-mt-20" id="how-to-implement-this-without-burning-money"></span>
    <a href="#how-to-implement-this-without-burning-money" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The first step is accepting something professional programmers usually hate: you can throw code away.</p>
<blockquote>
  <p>In fact, you should throw code away much more often.</p>

</blockquote>
<p>Code is not a monument. Code is working material. Deleting code is as important as writing new code. That is why we use Git. The idea was always to work in branches, try paths, change our minds, compare alternatives, delete what did not work, and keep only what survived contact with reality.</p>
<p>But in the traditional process, code becomes too emotionally expensive. If it took two weeks to produce, nobody wants to admit it was waste. The manager feels that deleting it proves the previous decision was bad. The programmer defends the implementation because they suffered to make it. The team pushes the problem forward because deletion feels like defeat. It is not. Sometimes it is hygiene.</p>
<p>With LLMs, that cost changes. <strong>Making mistakes becomes cheap</strong>. Prototyping becomes cheap. Building a proof of concept becomes cheap. Trying two paths and throwing one away becomes cheap. Asking the agent to rewrite an entire approach in a temporary branch, compare it, document what was learned, and <code>git reset</code> without drama becomes cheap.</p>
<p>So do more of that. More prototypes. More proofs of concept. More internal A/B tests. More disposable branches. Less attachment to the first code that appeared. LLMs are good at this kind of work: exploring alternatives quickly, showing where the specification was vague, and producing material you can destroy without guilt.</p>
<p>But do not expect everyone to receive LLM tokens on Monday and know how to do this alone. That is the fastest way to burn the budget and then say AI does not work.</p>
<p>My suggestion is the opposite: assume one or two weeks with no planned delivery. Say clearly that the goal is not to ship a feature. It is to train the new process. Break the old ritual. Let the team practice in a safe environment. If it goes wrong, fine: reset the branch, delete the prototype, document what you learned, try again.</p>
<p>In that phase, the PM should not write a twenty-page spec. They should pair with the programmer and build small prompts, one step at a time, watching the result in real time. With each LLM response, the PM learns how much detail the code actually needs. The programmer learns to ask better questions. QA learns to turn behavior into tests early. The Tech Lead learns to cut complexity while it is still cheap.</p>
<p>This is how LLM adoption has some chance of working: as a process change, not as an expensive autocomplete license.</p>
<h2>The uncomfortable conclusion<span class="hx:absolute hx:-mt-20" id="the-uncomfortable-conclusion"></span>
    <a href="#the-uncomfortable-conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><blockquote>
  <p>LLMs fail so often because they are inserted into processes made by people who were trained not to decide.</p>

</blockquote>
<p>It is not just lack of a better prompt. It is not just lack of a larger spec. It is not just lack of token governance. Those things help, but they do not touch the center of the problem.</p>
<p>The center is that many people do not know how to explain a measurable goal, prioritize, accept a trade-off, or connect decision with result. An LLM needs exactly that to be useful.</p>
<p>If you put AI into the old process, you accelerate the old process. More mediocre backlog. More bad decisions. More bugs. More cost. More rework. More reports trying to prove productivity the customer never felt.</p>
<p>If you want LLMs to work, redesign the process around them. Smaller cycles. Real pairing. Decision close to execution. Testing close to decision. Review close to code. Result close to whoever decided.</p>
<p>This is the part programmers need to face: yes, you probably never learned how to decide properly. You never learned how to describe a goal objectively. You never learned how to measure consequence. The process trained you that way. Now AI is revealing the failure.</p>
<p>The good news is that you can train the opposite.</p>
<p>But it will not happen with another ceremony, another prompt spreadsheet, another AI usage policy, or another sprint full of tickets written so nobody gets criticized.</p>
<p><strong>It will happen when you stop outsourcing decisions.</strong></p>
]]></content:encoded><category>llms</category><category>vibe-coding</category><category>software-engineering</category></item><item><title>AI Controversy in Open Source Project Contributions - My Take</title><link>https://akitaonrails.github.io/en/2026/06/05/ai-controversy-open-source-project-contributions-my-take/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/06/05/ai-controversy-open-source-project-contributions-my-take/</guid><pubDate>Fri, 05 Jun 2026 11:00:00 -0300</pubDate><description>&lt;p&gt;Over the last few days, the discussion around &lt;strong&gt;AI slop&lt;/strong&gt; in open source projects came back. Maintainers complaining that AI-generated PRs are garbage. Projects banning AI-assisted contributions. Bug bounty programs getting flooded with fake reports. Users angry because a release that used AI broke something. And, of course, the internet doing what the internet does best: turning one specific case into a holy war.&lt;/p&gt;
&lt;p&gt;My short take: &lt;strong&gt;AI in open source contributions is here to stay&lt;/strong&gt;. It will increase, not decrease. Pandora&amp;rsquo;s box is already open. There is no realistic plan where everyone goes back to programming 100% manually and pretends Claude Code, Codex, OpenCode, Cursor, Gemini CLI, and the rest never existed.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Over the last few days, the discussion around <strong>AI slop</strong> in open source projects came back. Maintainers complaining that AI-generated PRs are garbage. Projects banning AI-assisted contributions. Bug bounty programs getting flooded with fake reports. Users angry because a release that used AI broke something. And, of course, the internet doing what the internet does best: turning one specific case into a holy war.</p>
<p>My short take: <strong>AI in open source contributions is here to stay</strong>. It will increase, not decrease. Pandora&rsquo;s box is already open. There is no realistic plan where everyone goes back to programming 100% manually and pretends Claude Code, Codex, OpenCode, Cursor, Gemini CLI, and the rest never existed.</p>
<p>That does not mean every project is obligated to accept AI-assisted contributions. Far from it. The maintainer has the absolute right to run the project however they want. They can accept AI, ban AI, freeze the project, delete the repository and go grow tomatoes. Open source is not mandatory democracy. Whoever maintains decides.</p>
<p>But I also think banning AI is mostly delaying a fight you cannot win. The volume will keep going up. Average quality will wobble. There will be trash, there will be good work, there will be almost-right PRs, there will be hallucinated bug reports. The practical question is not &ldquo;how do we stop AI?&rdquo; The practical question is: <strong>how do we adapt the process to survive this?</strong></p>
<h2>The recent cases<span class="hx:absolute hx:-mt-20" id="the-recent-cases"></span>
    <a href="#the-recent-cases" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The loudest case right now was <a href="https://linuxiac.com/rsync-3-4-3-regressions-trigger-debate-over-ai-assisted-code/"target="_blank" rel="noopener">rsync 3.4.3</a>. The version shipped on May 20 as a security update, fixing six CVEs. Soon after, users reported regressions, especially in backup flows using daemon mode and incremental transfer options. Rsync is not a toy. It is a backup, deploy, mirror, sysadmin tool. Breaking compatibility there hurts.</p>
<p>Andrew Tridgell, creator of Samba and co-creator of rsync, answered in the post <a href="https://medium.com/@tridge60/rsync-and-outrage-d9849599e5a0"target="_blank" rel="noopener">rsync and outrage</a>. He confirmed that he used Claude, with cross-checks from Codex and Gemini, to help rewrite part of the test suite in Python. But he also said the obvious thing a lot of people do not want to hear: he did not type &ldquo;convert all of this&rdquo; and go to sleep. He designed the suite, reviewed the work, burned a lot of CI time, and acknowledged that regressions happened in valid but uncommon cases that were covered neither by the existing tests nor by his manual testing.</p>
<p>That is the interesting point: the regression became a discussion about AI, but regression was not born with AI. Regression was born with software.</p>
<p>On the other side we have Zig. The <a href="https://ziglang.org/code-of-conduct/"target="_blank" rel="noopener">Zig Code of Conduct</a> today has an explicit policy: <strong>Strict No LLM / No AI Policy</strong>. No LLM-generated content, not code, not prose, not grammar edits, not translation, not brainstorming. Andrew Kelley explained in an interview, summarized by <a href="https://www.devclass.com/devops/2026/06/01/zig-creator-seeks-uncompromising-perfection-before-blessing-10/5248219"target="_blank" rel="noopener">DevClass</a>, that the team considers AI contributions invariably bad, review-time sinks, unteachable, non-deterministic. The line that sums up his position is a good one: the bar he wants is not &ldquo;works surprisingly well,&rdquo; it is &ldquo;uncompromising perfection.&rdquo;</p>
<p>I disagree with the generalization, but I respect the decision. It is his project. If he wants to keep that space free of LLMs, that is his right. Period.</p>
<p>In the same neighborhood came the Bun discussion. The <a href="https://bun.sh/blog/bun-joins-anthropic"target="_blank" rel="noopener">official post about Bun joining Anthropic</a> says Bun was acquired because Claude Code and other Anthropic coding products depend on it. &ldquo;If Bun breaks, Claude Code breaks,&rdquo; in the post&rsquo;s own words. Meanwhile, the <a href="https://www.cosmicjs.com/blog/bun-rust-rewrite-javascript-runtime"target="_blank" rel="noopener">Cosmic piece about the Rust rewrite</a> points to reasons like contributor accessibility, maintainability at scale, and long-term stability. I saw people linking this to Zig&rsquo;s anti-AI policy, but the source I could verify does not state that causality. What we can safely say is: Bun, a project born in Zig, is moving important parts to Rust, and one stated reason is Rust&rsquo;s larger contributor base.</p>
<p>Then there is curl. According to <a href="https://hackaday.com/2026/01/26/the-curl-project-drops-bug-bounties-due-to-ai-slop/"target="_blank" rel="noopener">Hackaday</a>, Daniel Stenberg suspended curl&rsquo;s bug bounty program starting February 1, 2026 because the project was flooded with LLM-generated vulnerability reports. Long reports, intimidating, with the shape of a serious exploit, but when an experienced maintainer reads them, they are not vulnerabilities. The PR removing the bounty is on GitHub as <a href="https://github.com/curl/curl/pull/20312"target="_blank" rel="noopener">curl/curl#20312</a>.</p>
<p>That case is different from a code PR. It is worse. A bad patch you test, compile, read. A fake security report forces someone experienced to spend time proving that the bug does not exist. That is the kind of thankless work that burns maintainers out.</p>
<p>So yes: the problem is real. AI slop exists. It is filling issue trackers, PRs, bug bounties, comments, everything.</p>
<p>Now the part nobody likes: it will not stop.</p>
<h2>There is no &ldquo;back to normal&rdquo;<span class="hx:absolute hx:-mt-20" id="there-is-no-back-to-normal"></span>
    <a href="#there-is-no-back-to-normal" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I already talked about this in <a href="/en/2026/04/11/vs-code-is-the-new-punch-card/">VS Code Is the New Punch Card</a>. The interface changed. First you had to type binary instructions. Then you had to organize punch cards. Then you had to convince the compiler. Then you had to operate editor, terminal, framework, CI, container, cloud, and YAML. Now you have to orchestrate agents.</p>
<p>This does not eliminate engineering. It only changes where the manual work ends.</p>
<p>Some people are treating AI like a passing fad, like the &ldquo;metaverse&rdquo; or NFTs. It is not. AI for programming is already past the conference-demo stage. It is inside real workflows. Rsync used it. Bun has Claude Code bots opening PRs. My projects use it. Your contributors will use it, even if you forbid it. If you think you can detect with 100% certainty whether a patch was written with or without AI, good luck with that one.</p>
<p>The maintainer can still set a rule. They can say &ldquo;if I find out you used AI, I block you.&rdquo; That may make sense in small communities with their own culture, like Zig. But in open source at large, especially big projects with users all over the world, this turns into compliance theater. The person uses AI, rewrites the text, does not disclose it, and that&rsquo;s that. What are you going to do? AI detector for code? For crying out loud.</p>
<h2>The mistake is thinking you have to review everything alone<span class="hx:absolute hx:-mt-20" id="the-mistake-is-thinking-you-have-to-review-everything-alone"></span>
    <a href="#the-mistake-is-thinking-you-have-to-review-everything-alone" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The fairest maintainer complaint is simple: there already are not enough people to review human contributions. AI increases volume. More PRs, more issues, more bug reports. The maintainer was already tired, now they are underwater.</p>
<p>I get it. I just do not think the solution is to process the new volume with the old process.</p>
<p>You fight AI with AI.</p>
<p>The first triage has to be done with an LLM. Not to approve automatically. To filter. To summarize. To compare the diff against the stated goal. To point out bad smells. To say &ldquo;this touches schema, it needs a migration.&rdquo; To say &ldquo;this changes public behavior, it needs compatibility tests.&rdquo; To say &ldquo;this PR description does not match the code.&rdquo; That layer alone removes 80% of the grunt work.</p>
<p>The mistake is letting the agent decide alone. The right move is making the agent do the boring investigation, then you decide.</p>
<p>That is how I do it.</p>
<h2>My real PR flow<span class="hx:absolute hx:-mt-20" id="my-real-pr-flow"></span>
    <a href="#my-real-pr-flow" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Anyone who followed my <a href="/en/2026/05/14/wrapping-up-my-ai-marathon-success-or-failure/">AI marathon</a> knows I did not get here after watching two YouTube videos. I spent weeks using agents every day, on real projects, breaking real things, fixing real things. What I am describing here is the workflow that survived that, not LinkedIn theory.</p>
<p>Shameless propaganda: I am happy that my three main AI-related projects are getting contributions almost every day:</p>
<ul>
<li><a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>, which I already covered in <a href="/en/2026/03/01/ai-jail-sandbox-for-ai-agents-from-shell-script-to-real-tool/">ai-jail: Sandbox for AI Agents</a>.</li>
<li><a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener">ai-memory</a>, which I explained in <a href="/en/2026/05/23/i-built-memory-system-for-coding-agents-ai-memory/">I Built a Memory System for Coding Agents: ai-memory</a>.</li>
<li><a href="https://github.com/akitaonrails/ai-usagebar"target="_blank" rel="noopener">ai-usagebar</a>, from <a href="/en/2026/05/24/i-built-a-waybar-widget-for-omarchy-to-monitor-llm-usage-ai-usagebar/">I Built a Waybar Widget for Omarchy to Monitor LLM Plan Usage: ai-usagebar</a>.</li>
</ul>
<p>ai-memory in particular became a live experiment. As I write this, GitHub shows <strong>55 closed PRs</strong> in the project; through the <code>gh</code> API, 46 were merged and 9 were closed without merge. Thanks to everyone who contributed. Seriously. Djalma, mrpaiva, pedrofjr, CaTeIM, azevedo-luis, brunoomariano, rikelmyso7, abnersajr, and several others. That is a lot of work from a lot of people.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/06/05/ia-open-source-contribuicoes/screenshot-ai-projects.png" alt="GitHub closed Pull Requests screen for ai-memory, showing 55 closed PRs and several recent PRs from contributors."  loading="lazy" /></p>
<p>It is also obvious, looking at the style, that most of those PRs were made with AI assistance. And that is not a problem. It would be hypocritical to complain: <strong>the entirety of ai-memory was written by AI</strong>. I did not open a code editor once to write that thing by hand.</p>
<p>But almost no PR arrives immediately mergeable. That is the point a lot of people still do not understand. &ldquo;Made with AI&rdquo; does not mean &ldquo;blind merge.&rdquo; It means &ldquo;the audit begins.&rdquo;</p>
<p>My prompt for reviewing a PR is more or less this:</p>
<blockquote>
  <p>check the latest open PR as usual: do not trust the author, audit the whole code thoroughly, make sure it achieves its goal.</p>
<p>Check whether we need to adjust or fix anything before merging: do not accept regressions, do not accept degraded code quality, check whether test coverage is correct.</p>
<p>If it touches build or OS support, check GitHub Actions, CI, clean build, correct wiring, and documentation.</p>
<p>Summarize what the PR does, what you found, and ask me for approval before proceeding.</p>

</blockquote>
<p>Notice two things. First: &ldquo;do not trust the author.&rdquo; That is not rudeness. It is engineering. The PR description is a hypothesis. The diff is the evidence. Second: &ldquo;ask me for approval.&rdquo; The agent investigates, I decide.</p>
<p>When I receive several PRs on the same day, I do this for all of them. After they are all in, I send another prompt:</p>
<blockquote>
  <p>check today&rsquo;s commits.</p>
<p>We touched a lot of code, so I need a complete audit of the touched code to make sure we did not add regressions, unnecessary duplication, dead code, or magic values that should become documented constants.</p>
<p>Follow clean code principles, confirm test coverage, and refactor whatever is needed.</p>

</blockquote>
<p>If the project is already in use, I add the part many people forget:</p>
<blockquote>
  <p>this project is already being used. Do not break compatibility.</p>
<p>If we touched database schema or user-file structures, there must be a clean migration on upgrade.</p>
<p>Upgrade must not corrupt data.</p>

</blockquote>
<p>This guarantees nothing. But it avoids obvious mistakes.</p>
<p>In ai-memory you can see that pattern in the history. After a big batch of PRs, there are hardening commits from me. One example is <a href="https://github.com/akitaonrails/ai-memory/commit/65682dc"target="_blank" rel="noopener"><code>65682dc</code></a>, &ldquo;fix(audit): address 5 BLOCKING audit findings from today&rsquo;s review,&rdquo; touching OpenAI OAuth, admin API, admission hooks, and the MCP server. Another is the sequence of PRs <a href="https://github.com/akitaonrails/ai-memory/pull/65"target="_blank" rel="noopener"><code>#65</code></a>, <a href="https://github.com/akitaonrails/ai-memory/pull/68"target="_blank" rel="noopener"><code>#68</code></a>, <a href="https://github.com/akitaonrails/ai-memory/pull/70"target="_blank" rel="noopener"><code>#70</code></a>, followed by hardening commits around base path, wikilinks, and strict mode. PR goes in, then audit, then adjustment. That second step is where the difference lives.</p>
<p>In <a href="https://github.com/akitaonrails/ai-memory/pull/77"target="_blank" rel="noopener"><code>#77</code></a>, the auto-scope by session/actor change touched a lot: <code>active_project</code>, hooks router, MCP server, stress tests, multi-user tests. More than 3,800 lines added. I am not going to pretend someone reads that manually line by line with the same consistency after the fifth hour. The agent helps map risk. I use my judgment to decide what needs hardening.</p>
<h2>Regression happens with or without AI<span class="hx:absolute hx:-mt-20" id="regression-happens-with-or-without-ai"></span>
    <a href="#regression-happens-with-or-without-ai" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The rsync case is useful because it exposes a fantasy: the idea that human code was safe and AI code is dangerous.</p>
<p>No. Code is dangerous.</p>
<p>Rust is considered a safe language. And Rust has bugs. Go look at Rust&rsquo;s issue tracker. Zig wants perfection and has bugs. Curl has bugs. Rsync has bugs. Linux has bugs. SQLite has bugs. My code has bugs. Your code has bugs. There is no magical language that produces bug-free software. There never will be.</p>
<p>This is not pessimism. It is a fundamental limit. In computing, we have known for a long time that there is no perfect general verifier for non-trivial semantic properties of programs. Halting problem, Rice&rsquo;s theorem, that kind of thing. You can prove a lot in restricted domains. You can use a better type system. You can use Rust to eliminate entire classes of memory-safety bugs. You can use tests, fuzzing, model checking, static analysis. Good. Use all of it.</p>
<p>But &ldquo;arbitrary software with no bugs&rdquo; is fantasy.</p>
<p>Humans err because of incomplete context, wrong assumptions, pressure, fatigue, lack of tests, lack of real use. AI errs for the same practical reasons: incomplete context, wrong assumptions, training on bad code, poorly specified goals, weak validation. The difference is scale. AI can produce more trash per minute. It can also produce more fixes per minute if you build the process correctly.</p>
<h2>I use it because I use it<span class="hx:absolute hx:-mt-20" id="i-use-it-because-i-use-it"></span>
    <a href="#i-use-it-because-i-use-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Another thing I learned in practice: it is very hard to maintain a project well if you do not use it yourself.</p>
<p>I use ai-memory every day. I use ai-jail every day. I use ai-usagebar every day. I used the <a href="https://github.com/akitaonrails/prettify-manga"target="_blank" rel="noopener">Prettify Manga Reader</a> extension in my real reading. That is why I catch bugs automated tests do not catch.</p>
<p>In Prettify, for example, the commits tell the story. <a href="https://github.com/akitaonrails/prettify-manga/commit/66c6728"target="_blank" rel="noopener"><code>66c6728</code></a> added the night filter. Then came <a href="https://github.com/akitaonrails/prettify-manga/commit/b5bcb4d"target="_blank" rel="noopener"><code>b5bcb4d</code></a>, release 0.2.2, with Kindle Web Reader support, toolbar, shortcuts, and screenshot. Soon after came <a href="https://github.com/akitaonrails/prettify-manga/commit/0e6b7ee"target="_blank" rel="noopener"><code>0e6b7ee</code></a>, release 0.2.3, fixing spread pairing so right-to-left reading stayed correct without messing up chronological order. That came from real use. I was reading, noticed strange behavior, asked the LLM to fix it, tested it, released it.</p>
<p>That is the healthy loop: use it, feel the pain, fix it, test again.</p>
<p>In ai-jail, some contributions came from use cases I would not have prioritized on my own. <a href="https://github.com/akitaonrails/ai-jail/pull/56"target="_blank" rel="noopener"><code>#56</code></a> fixed the PKGBUILD on AUR. <a href="https://github.com/akitaonrails/ai-jail/pull/55"target="_blank" rel="noopener"><code>#55</code></a> forwarded OSC 52 clipboard writes through the alt-screen PTY proxy. <a href="https://github.com/akitaonrails/ai-jail/pull/51"target="_blank" rel="noopener"><code>#51</code></a> recognized Grok as a known agent and documented the machine-id privacy mask. These details show up when other people use the tool in setups different from mine.</p>
<p>In ai-usagebar, this is even clearer. <a href="https://github.com/akitaonrails/ai-usagebar/pull/4"target="_blank" rel="noopener"><code>#4</code></a> added a macOS Keychain fallback for Anthropic credentials. I am on Linux/Omarchy, not macOS. I was not going to find that bug in my daily use. <a href="https://github.com/akitaonrails/ai-usagebar/pull/7"target="_blank" rel="noopener"><code>#7</code></a> fixed reading a custom <code>credentials_path</code> in the widget. Same story: a user with a different config from mine. This is where open source shines: someone scratches their own itch and improves the project for everyone else.</p>
<p>But notice: when I cannot test manually, like macOS and Windows, I depend more on CI. GitHub Actions runs macOS, Windows, Linux. It does not replace real use, but it reduces risk. And when someone who uses that environment opens an issue or PR, that is gold.</p>
<p>Manual testing is still the last line of defense. If you give it up, you are introducing bugs by definition. Automated tests catch what you remembered to specify. Real use catches what you did not even know you needed to specify.</p>
<p>After merging a large set, my final flow is usually:</p>
<blockquote>
  <p>now run <code>bin/deploy</code> to my home server and test it online.</p>
<p>Check health, check whether it still works as before.</p>

</blockquote>
<p>Again: it guarantees nothing. But it avoids breaking the obvious.</p>
<h2>There is no silver bullet<span class="hx:absolute hx:-mt-20" id="there-is-no-silver-bullet"></span>
    <a href="#there-is-no-silver-bullet" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I do not think every project should accept AI PRs. I also do not think every project should ban them. There is no universal rule.</p>
<p>If you are a solo maintainer of a critical library, with no time, no decent CI, no energy to deal with an avalanche of bad PRs, banning AI-assisted contribution may be the right decision for now. Better that than accepting junk and breaking users.</p>
<p>If you maintain a small community and want to teach human contributors, as Zig argues, an anti-AI policy makes sense inside that culture. You are optimizing for contributor formation, not patch throughput.</p>
<p>But if you maintain a project that already gets high volume, that already has CI, that already has tests, that already has users, I think adaptation is inevitable. You will need automated triage. You will need an agent reviewing PRs. You will need a bot summarizing issues. You will need a tool that says &ldquo;this looks like a hallucinated vulnerability report.&rdquo;</p>
<p>AI is imperfect and scales fast. That is exactly why the process has to change.</p>
<h2>My conclusion<span class="hx:absolute hx:-mt-20" id="my-conclusion"></span>
    <a href="#my-conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>AI slop is real. It will get worse before it gets better. There will be garbage PRs. There will be fake security reports. There will be regressions blamed on AI that would have happened with humans too. There will be angry maintainers. There will be users yelling. Normal.</p>
<p>But the direction does not change.</p>
<p>AI-assisted code is here to stay. The percentage of code touched by agents will keep rising. Banning it can work locally, for a while, in one specific community. Across the industry, it is a lost war.</p>
<p>The pragmatic path is to make AI work <strong>for us</strong>. Use AI for triage. Use AI for audits. Use AI to compare diffs. Use AI to find regressions. Use AI to write tests that fail before and pass after. Use AI to do the boring work that wears humans down.</p>
<p>And keep the human decision where it matters.</p>
<p>I have said this several times and I will repeat it: <strong>AI is a mirror</strong>.</p>
<p>If you are sloppy, it accelerates your sloppiness. Your code becomes industrialized AI slop.</p>
<p>If you are a real engineer, it amplifies your engineering. You keep demanding tests, CI, compatibility, migrations, review, clean code, deploy, manual testing. You just do it 10 times faster.</p>
<p>And for companies: if you are thinking, &ldquo;great, now I save money because I do not need to hire as many programmers,&rdquo; I get it. Up to a point. But if you are putting AI into the workflow, then you must strengthen QA. More tests, more review, more validation process, more good people thinking about quality. Skip that and you are screwed. You will trade development cost for production bugs, rework, incidents, and angry customers.</p>
<p>There is no going back to the 100% manual world. Better to stop wasting energy trying to close Pandora&rsquo;s box and start building a decent process around what came out of it.</p>
]]></content:encoded><category>open-source</category><category>coding-agents</category><category>software-engineering</category></item><item><title>Open Source Best Practices with LLMs - The Bare Minimum</title><link>https://akitaonrails.github.io/en/2026/05/30/open-source-best-practices-llm-the-minimum/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/05/30/open-source-best-practices-llm-the-minimum/</guid><pubDate>Sat, 30 May 2026 13:00:00 -0300</pubDate><description>&lt;p&gt;Over the last few months I&amp;rsquo;ve pushed a pile of personal projects to GitHub. &lt;a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener"&gt;Frank FBI&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener"&gt;Frank Mega&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener"&gt;Frank Yomik&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener"&gt;Frank Sherlock&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener"&gt;Frank Investigator&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener"&gt;ai-jail&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ai-memory"target="_blank" rel="noopener"&gt;ai-memory&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ai-usagebar"target="_blank" rel="noopener"&gt;ai-usagebar&lt;/a&gt;, &lt;a href="https://github.com/akitaonrails/ghpending"target="_blank" rel="noopener"&gt;ghpending&lt;/a&gt;, and so on. Some in Ruby on Rails, most in Rust, one or two in Flutter. Coded mostly with Claude Code and Codex, riding the wave of my &lt;a href="https://akitaonrails.github.io/en/2026/05/14/wrapping-up-my-ai-marathon-success-or-failure/"&gt;vibe coding marathon&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And here&amp;rsquo;s the part nobody mentions when they say &amp;ldquo;I built a project with AI&amp;rdquo;: generating the code is the easy part. The hard part, the thing that separates a repo that&amp;rsquo;s just a pile of code from an actual open source project, is everything that comes &lt;strong&gt;after&lt;/strong&gt; &lt;code&gt;git init&lt;/code&gt;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Over the last few months I&rsquo;ve pushed a pile of personal projects to GitHub. <a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">Frank FBI</a>, <a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener">Frank Mega</a>, <a href="https://github.com/akitaonrails/FrankYomik"target="_blank" rel="noopener">Frank Yomik</a>, <a href="https://github.com/akitaonrails/FrankSherlock"target="_blank" rel="noopener">Frank Sherlock</a>, <a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener">Frank Investigator</a>, <a href="https://github.com/akitaonrails/ai-jail"target="_blank" rel="noopener">ai-jail</a>, <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/ghpending"target="_blank" rel="noopener">ghpending</a>, and so on. Some in Ruby on Rails, most in Rust, one or two in Flutter. Coded mostly with Claude Code and Codex, riding the wave of my <a href="/en/2026/05/14/wrapping-up-my-ai-marathon-success-or-failure/">vibe coding marathon</a>.</p>
<p>And here&rsquo;s the part nobody mentions when they say &ldquo;I built a project with AI&rdquo;: generating the code is the easy part. The hard part, the thing that separates a repo that&rsquo;s just a pile of code from an actual open source project, is everything that comes <strong>after</strong> <code>git init</code>.</p>
<p>I have a strong opinion about this, so let me just plant the flag: no open source project is ready to be published without three things. In order of importance:</p>
<ol>
<li><strong>Installation surface.</strong> The new user has to be able to install and try the tool with the least friction possible. One command, ideally.</li>
<li><strong>Tests and automated CI.</strong> So contributions are easier and there&rsquo;s common ground on what&rsquo;s minimally acceptable to land in the project.</li>
<li><strong>Documentation.</strong> The short, easy-to-read version (the README) and a more detailed set in <code>docs/</code>, so new contributors understand the architectural decisions.</li>
</ol>
<p>Without those three, the way I see it, the project isn&rsquo;t ready. It can have the most brilliant code on earth inside, doesn&rsquo;t matter. If a stranger can&rsquo;t install it, can&rsquo;t trust that whatever they touch won&rsquo;t break everything, and can&rsquo;t understand why things are the way they are, then you don&rsquo;t have an open source project. You have a public dead archive.</p>
<p>And there&rsquo;s a bonus that became an essential part of my workflow: <strong>when all my projects follow the same pattern, I can just tell Claude Code or Codex &ldquo;run the deploy&rdquo; or &ldquo;cut a release,&rdquo; and they can do it.</strong> Because the structure is predictable. <code>bin/deploy</code> does the same thing in every project. The release comes out of a tag. The agent doesn&rsquo;t have to guess anything. Standardization is what makes the automation trustworthy. It looks like fussiness from an organized person, but it&rsquo;s the prerequisite for me to trust, eyes closed, whatever the agent is about to run.</p>
<p>Let me break down the three pillars and show the minimum setup I actually use.</p>
<h2>Pillar 1: The installation surface<span class="hx:absolute hx:-mt-20" id="pillar-1-the-installation-surface"></span>
    <a href="#pillar-1-the-installation-surface" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is the most important one and the most neglected. People spend weeks tuning the internal algorithm and then ship a README that says &ldquo;clone the repo and run <code>cargo build --release</code>.&rdquo; So the guy who was curious enough to try it needs to have Rust installed, wait ten minutes for it to compile, and pray no system lib is missing. You just lost 90% of your potential users.</p>
<p>The ideal is to offer <strong>several paths</strong> and let the user pick the one that fits their environment. Look at the install header for ai-jail, my champion of options:</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="c1"># Homebrew (macOS or Linux)</span>
</span></span><span class="line"><span class="cl">brew tap akitaonrails/tap <span class="o">&amp;&amp;</span> brew install ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># AUR (Arch Linux) - prebuilt binary</span>
</span></span><span class="line"><span class="cl">yay -S ai-jail-bin
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># cargo (any platform with Rust)</span>
</span></span><span class="line"><span class="cl">cargo install ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># mise</span>
</span></span><span class="line"><span class="cl">mise use -g github:akitaonrails/ai-jail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Direct binary download</span>
</span></span><span class="line"><span class="cl">curl -fsSL https://github.com/akitaonrails/ai-jail/releases/latest/download/ai-jail-linux-x86_64.tar.gz <span class="p">|</span> tar xz</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>Five paths. The Arch user runs <code>yay</code>. The macOS one runs <code>brew</code>. The Rust dev runs <code>cargo install</code>. Whoever uses <a href="https://github.com/jdx/mise"target="_blank" rel="noopener">mise</a> to manage versions pulls it through mise. And whoever just wants the binary downloads the tarball straight from the release. Everyone grabs what they&rsquo;re already used to, nobody&rsquo;s forced to install a whole toolchain just to kick the tires.</p>
<h3>The secret: compile once, repackage many<span class="hx:absolute hx:-mt-20" id="the-secret-compile-once-repackage-many"></span>
    <a href="#the-secret-compile-once-repackage-many" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The gotcha that scares you at first is thinking each package format needs its own build. It doesn&rsquo;t. The trick is to <strong>compile the binary once per architecture, then repackage that same binary into every format.</strong></p>
<p>In my Rust pipeline, the build job produces a tarball with the finished binary and its <code>sha256</code>. The packaging steps that follow (Docker, AUR, Homebrew) <strong>download that already-compiled tarball</strong> instead of recompiling. It cuts CI time, guarantees the byte that goes to AUR is exactly the one that goes to Docker, and kills off that whole class of &ldquo;works in one format, breaks in the other&rdquo; bug.</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="c"># Docker job reusing the already-compiled binary</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">docker-amd64</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l">build-linux</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/download-artifact@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">ai-memory-linux-x86_64</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">artifacts</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Desempacota o binário pré-compilado</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">tar -xzf artifacts/ai-memory-linux-x86_64.tar.gz -C dist/docker/</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">docker/build-push-action@v6</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">runtime-prebuilt-amd64 </span><span class="w"> </span><span class="c"># inject the binary, don&#39;t recompile</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>The AUR job does the same thing: download the tarball, compute the <code>sha256</code>, inject it into the <code>PKGBUILD</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-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nv">X86_SHA</span><span class="o">=</span><span class="k">$(</span>awk <span class="s1">&#39;{print $1}&#39;</span> artifacts/ai-usagebar-linux-x86_64.tar.gz.sha256<span class="k">)</span>
</span></span><span class="line"><span class="cl">sed -i <span class="s2">&#34;s/^sha256sums_x86_64=.*/sha256sums_x86_64=(&#39;</span><span class="nv">$X86_SHA</span><span class="s2">&#39;)/&#34;</span> packaging/aur/PKGBUILD-bin</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>That&rsquo;s the core idea. The binary is the real artifact. RPM, DEB, AppImage, AUR, Homebrew tarball, Docker image, all of it is just <strong>wrapping around the same binary</strong>.</p>
<p>The two snippets above are excerpts. If you want to see the whole thing running, the full build-once-reuse flow (binary compiled once, reused across the Docker and AUR jobs) lives in <a href="https://github.com/akitaonrails/ai-memory/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener">ai-memory&rsquo;s <code>release.yml</code></a>, and the <code>sha256</code> injection into the <code>PKGBUILD</code> is in <a href="https://github.com/akitaonrails/ai-usagebar/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener">ai-usagebar&rsquo;s <code>release.yml</code></a>.</p>
<h3>The menu of formats<span class="hx:absolute hx:-mt-20" id="the-menu-of-formats"></span>
    <a href="#the-menu-of-formats" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Every platform is its own world. Here&rsquo;s the minimum worth knowing:</p>
<p><strong>Linux</strong> is the most fragmented. The options, from simplest to most work:</p>
<ul>
<li><strong>Tarball</strong> (<code>.tar.gz</code>): the lowest common denominator. Works on any distro, just unpack it and drop it in <code>PATH</code>. Always offer this one.</li>
<li><strong>AUR</strong> (Arch): two packages, the <code>-bin</code> (prebuilt binary, installs in seconds) and the source one (compiles on the user&rsquo;s machine). I publish both with the <code>KSXGitHub/github-actions-deploy-aur</code> action. The <code>-bin</code> is what everybody uses.</li>
<li><strong>AppImage</strong>: a single executable file that carries all its dependencies inside. Good for desktop apps. Frank Sherlock, which is Tauri, builds the AppImage right in the build and I still GPG-sign it afterward (the signing step is in <a href="https://github.com/akitaonrails/FrankSherlock/blob/master/.github/workflows/release.yml"target="_blank" rel="noopener">its <code>release.yml</code></a>). For a simple binary you can use <code>appimagetool</code>.</li>
<li><strong>RPM and DEB</strong>: for Fedora/openSUSE and Debian/Ubuntu. The tool that saves your life here is <a href="https://github.com/goreleaser/nfpm"target="_blank" rel="noopener">nfpm</a>, which generates both from a single declarative <code>nfpm.yaml</code>, no need to learn the guts of <code>rpmbuild</code> or <code>dpkg-deb</code>. It takes the already-compiled binary and spits out both packages.</li>
</ul>
<p><strong>macOS</strong> has two targets: Apple Silicon (<code>aarch64-apple-darwin</code>) and Intel (<code>x86_64-apple-darwin</code>). You either compile them separately or build a universal binary with <code>lipo</code>. And here lives the platform&rsquo;s biggest headache: <strong>signing and notarization</strong>. Without it, macOS Gatekeeper refuses to open your app. The flow is to decode the P12 certificate, import it into a temporary keychain, sign with <code>codesign --options runtime --timestamp</code>, and notarize:</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">codesign --sign <span class="s2">&#34;</span><span class="nv">$APPLE_SIGNING_IDENTITY</span><span class="s2">&#34;</span> --options runtime --timestamp ./meu-binario
</span></span><span class="line"><span class="cl">ditto -c -k meu-binario notarize.zip
</span></span><span class="line"><span class="cl">xcrun notarytool submit notarize.zip <span class="se">\
</span></span></span><span class="line"><span class="cl">  --apple-id <span class="s2">&#34;</span><span class="nv">$APPLE_ID</span><span class="s2">&#34;</span> --password <span class="s2">&#34;</span><span class="nv">$APPLE_PASSWORD</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --team-id <span class="s2">&#34;</span><span class="nv">$APPLE_TEAM_ID</span><span class="s2">&#34;</span> --wait</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>This requires a paid Apple Developer account. It&rsquo;s annoying, but it&rsquo;s the price of not throwing an &ldquo;app is damaged&rdquo; error in your user&rsquo;s face. The whole signing-and-notarization job, with the temporary-keychain dance, is in <a href="https://github.com/akitaonrails/ai-jail/blob/master/.github/workflows/release.yml"target="_blank" rel="noopener">ai-jail&rsquo;s <code>release.yml</code></a>.</p>
<p><strong>Windows</strong> is the easiest to build (<code>x86_64-pc-windows-msvc</code>) and the thorniest to sign. A Windows code signing certificate is expensive and bureaucratic, so I, honestly, don&rsquo;t sign. I accept that SmartScreen will throw a warning on first run and I make that clear in the README. For a personal open source project, that&rsquo;s good enough.</p>
<p><strong>Rust crates</strong> (<code>cargo publish</code>): if it&rsquo;s a Rust library or CLI, publishing to <a href="https://crates.io"target="_blank" rel="noopener">crates.io</a> gives you <code>cargo install</code> for free. One line in the pipeline. Tip: make it idempotent, because re-running on an already-published tag errors out. I <code>grep</code> the output for &ldquo;already exists&rdquo; so it doesn&rsquo;t blow up the whole release over that.</p>
<p><strong>Homebrew</strong>: the clean way is to keep your own tap (<code>akitaonrails/homebrew-tap</code>). The pipeline updates the formula with the new URL and the new <code>sha256</code> on every release. The user runs <code>brew tap</code> once and after that it&rsquo;s just <code>brew upgrade</code>.</p>
<p><strong>mise</strong>: <a href="https://github.com/jdx/mise"target="_blank" rel="noopener">mise</a> can install straight from GitHub releases without you configuring anything on your end, as long as your tarballs follow a platform-name convention. You get this path basically for free just by naming your releases well.</p>
<p>Notice a detail: AUR, Homebrew, mise, and the direct download <strong>all consume the same release tarball</strong>. You publish the binaries once, and four different package managers know how to fend for themselves. That&rsquo;s why &ldquo;compile once, repackage many&rdquo; is the golden rule.</p>
<h2>Pillar 2: GitHub Actions, tag-based releases, and the changelog<span class="hx:absolute hx:-mt-20" id="pillar-2-github-actions-tag-based-releases-and-the-changelog"></span>
    <a href="#pillar-2-github-actions-tag-based-releases-and-the-changelog" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The second thing that makes an open source project serious is CI. Forget the &ldquo;build passing&rdquo; badge on the README, the value is in the common ground it creates: when someone sends a pull request, CI runs the tests, the linter, and the security scanner, and you know right away whether that contribution breaks anything. Without it, every PR turns into a tiresome manual negotiation.</p>
<p>On my Rust projects CI runs <code>cargo fmt --check</code>, <code>cargo clippy -D warnings</code>, <code>cargo test</code>, and <code>cargo audit</code>. On the Rails projects it&rsquo;s <code>rubocop</code>, <code>brakeman</code> (security scanner), <code>bundler-audit</code>, and the Minitest suite. <a href="https://github.com/akitaonrails/FrankMD"target="_blank" rel="noopener">FrankMD</a> alone has over 1,800 tests between Ruby and JavaScript. Frank Sherlock clears 300 tests in Rust plus 300 on the frontend. That safety net is what lets me accept a stranger&rsquo;s PR without sweating.</p>
<h3>PR review with an LLM<span class="hx:absolute hx:-mt-20" id="pr-review-with-an-llm"></span>
    <a href="#pr-review-with-an-llm" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The tests and CI are the bottom net. Above it there&rsquo;s a layer that became central to my workflow: using the LLM itself to review the pull requests. Opus and GPT are absurdly good at code review and quality refactoring. Better than me on consistency, because they don&rsquo;t get tired and they don&rsquo;t skim over 200 lines just because it&rsquo;s the fifth review of the day.</p>
<p>I have two prompts I use all the time. The first, for a specific PR that just landed:</p>
<blockquote>
  <p>look at the open PR on github. don&rsquo;t trust the author&rsquo;s description. audit the code thoroughly to see if it actually makes sense. make sure there are no regressions and no drop in code quality. make sure test coverage is adequate. then tell me what you think we should do.</p>

</blockquote>
<p>Notice the &ldquo;don&rsquo;t trust the author&rsquo;s description.&rdquo; That&rsquo;s deliberate. A PR description tells you what the person thought they did, and it doesn&rsquo;t always match what they actually did. The LLM has to read the real code, not the summary.</p>
<p>The second prompt is for after a long coding session or a bunch of PRs merged at once:</p>
<blockquote>
  <p>we touched a lot of code. audit the code those PRs changed, look for dead code, unnecessary duplication, magic hardcoded values that should be constants and/or better documented, missing test coverage, aim for <a href="/en/2026/04/20/clean-code-for-ai-agents/">clean code</a> principles, then check whether the documentation was properly updated.</p>

</blockquote>
<p>With the verdict in hand, I decide. For an incomplete PR, what I do depends on the size and my priority at that moment. If it&rsquo;s something small, I fix it myself right in the PR with an <code>amend</code>, reply explaining what I changed, and then merge and close. When I can merge but can&rsquo;t fix beforehand, I merge and immediately drop a follow-up fix commit. And when it&rsquo;s a big change, I ask the LLM to reply to the PR explaining why we&rsquo;re not merging, with the right direction on what needs to be done.</p>
<p>That&rsquo;s how I closed almost 40 PRs and 20 issues on ai-memory alone, in a couple of days. That volume doesn&rsquo;t happen by hand, and it also can&rsquo;t happen on autopilot.</p>
<p>And this is the point I keep hammering: none of this is blind. I&rsquo;m piloting the LLM toward those goals the whole time. It does the audit, puts the suggestions on the table, and even writes the text of the reply, but the one who decides to merge, fix, or reject is me. The LLM is the tireless reviewer; I&rsquo;m the one who signs off.</p>
<h3>The tag-based trigger<span class="hx:absolute hx:-mt-20" id="the-tag-based-trigger"></span>
    <a href="#the-tag-based-trigger" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The release part is separate from CI. While CI runs on every push and PR, the release fires <strong>only when you push a version tag</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-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">tags</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s1">&#39;v*.*.*&#39;</span><span class="w">   </span><span class="c"># dispara em v1.2.3, v0.5.0, etc.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_dispatch</span><span class="p">:</span><span class="w"> </span><span class="c"># permite disparar manualmente também</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>The flow for cutting a version becomes this:</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">git tag v0.5.0
</span></span><span class="line"><span class="cl">git push origin v0.5.0</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>And that&rsquo;s it. GitHub Actions notices the tag, fires the release workflow, compiles for the targets, packages everything, signs whatever needs signing, creates the GitHub Release, and publishes to the package managers. I don&rsquo;t touch anything else. In fact that&rsquo;s literally what I tell Claude Code: &ldquo;cut v0.5.0.&rdquo; It runs <code>cargo set-version</code>, updates the CHANGELOG, commits, creates the tag, pushes, and the rest is the pipeline.</p>
<p>The Rust build matrix compiles each target on its native platform:</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">strategy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">matrix</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">include</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-22.04</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">x86_64-unknown-linux-gnu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-22.04-arm</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">aarch64-unknown-linux-gnu</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">macos-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">aarch64-apple-darwin</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">os</span><span class="p">:</span><span class="w"> </span><span class="l">windows-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">target</span><span class="p">:</span><span class="w"> </span><span class="l">x86_64-pc-windows-msvc</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 cross-compilation gotcha: if you compile ARM inside an x86 machine (via <a href="https://github.com/cross-rs/cross"target="_blank" rel="noopener">cross</a>), you <strong>can&rsquo;t run the tests</strong> for that binary right there, because the architecture doesn&rsquo;t match. The fix is to run tests only on the native target and cross-compile the rest without testing. ai-usagebar does exactly that, x86 native and ARM via <code>cross</code>, in the matrix of <a href="https://github.com/akitaonrails/ai-usagebar/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener">its <code>release.yml</code></a>. And always use a compile cache: <code>Swatinem/rust-cache</code> easily cuts build time in half.</p>
<h3>Release notes and the changelog matter<span class="hx:absolute hx:-mt-20" id="release-notes-and-the-changelog-matter"></span>
    <a href="#release-notes-and-the-changelog-matter" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is where a lot of people slack off, and they shouldn&rsquo;t. When you cut a release, the person thinking about upgrading wants to know <strong>what changed</strong>. &ldquo;v0.5.0&rdquo; says nothing. A changelog says something.</p>
<p>I keep a <code>CHANGELOG.md</code> in the <a href="https://keepachangelog.com/"target="_blank" rel="noopener">Keep a Changelog</a> format, one section per version. And the pipeline automatically extracts the current version&rsquo;s section to paste into the GitHub Release body. A simple <code>awk</code> handles it:</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">awk -v <span class="nv">ver</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$VERSION</span><span class="s2">&#34;</span> <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">  BEGIN { hdr = &#34;## [&#34; ver &#34;]&#34; }
</span></span></span><span class="line"><span class="cl"><span class="s1">  index($0, hdr) == 1 { flag=1; next }
</span></span></span><span class="line"><span class="cl"><span class="s1">  flag &amp;&amp; index($0, &#34;## [&#34;) == 1 { exit }
</span></span></span><span class="line"><span class="cl"><span class="s1">  flag { print }
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span> CHANGELOG.md &gt; changelog_section.md</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>That <code>awk</code> is the same one that runs on the ai-usagebar release (again in <a href="https://github.com/akitaonrails/ai-usagebar/blob/main/.github/workflows/release.yml"target="_blank" rel="noopener">its <code>release.yml</code></a>, which is my most polished one on this front). Then, after the changelog, I append the install instructions and the <code>sha256</code> checksums for each artifact. The result is a GitHub Release that explains itself: what changed, how to install it, and how to verify the integrity of the download. The user reads it and decides whether to upgrade. That&rsquo;s the kind of care that makes a stranger trust your project.</p>
<h2>Pillar 3: Real deployment, no fuss<span class="hx:absolute hx:-mt-20" id="pillar-3-real-deployment-no-fuss"></span>
    <a href="#pillar-3-real-deployment-no-fuss" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The projects that run as a service (the Rails ones, mainly) have to go somewhere. And here I go against the hype. No Kubernetes. No Kamal. No three-stage deploy pipeline with manual approval.</p>
<p>I have a home server. A <a href="/en/2026/03/31/migrating-my-home-server-with-claude-code/">Minisforum MS-S1 Max running openSUSE MicroOS</a> that I migrated recently, with a <a href="https://github.com/go-gitea/gitea"target="_blank" rel="noopener">Gitea</a> acting as a private Docker registry on port 3007. For a solo developer with a simple home server, that&rsquo;s more than enough. All that cluster-orchestration ceremony is solving a problem I don&rsquo;t have.</p>
<p>The whole deploy fits in a single <code>bin/deploy</code>. It does three things: builds the image, pushes it to the registry, and SSHes into the server to bring up the new version:</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="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Config comes from a .gitignored file, outside the repo</span>
</span></span><span class="line"><span class="cl"><span class="nb">source</span> <span class="s2">&#34;</span><span class="k">$(</span>dirname <span class="s2">&#34;</span><span class="nv">$0</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2">/../config/deploy.env&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">IMAGE</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">REGISTRY_HOST</span><span class="si">}</span><span class="s2">:</span><span class="si">${</span><span class="nv">REGISTRY_PORT</span><span class="si">}</span><span class="s2">/akitaonrails/frank_fbi:latest&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; Buildando a imagem...&#34;</span>
</span></span><span class="line"><span class="cl">docker build -t <span class="s2">&#34;</span><span class="nv">$IMAGE</span><span class="s2">&#34;</span> .
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; Empurrando pro registry...&#34;</span>
</span></span><span class="line"><span class="cl">docker push <span class="s2">&#34;</span><span class="nv">$IMAGE</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;==&gt; Subindo no servidor...&#34;</span>
</span></span><span class="line"><span class="cl">ssh <span class="s2">&#34;</span><span class="si">${</span><span class="nv">DEPLOY_USER</span><span class="si">}</span><span class="s2">@</span><span class="si">${</span><span class="nv">DEPLOY_HOST</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  <span class="s2">&#34;cd </span><span class="si">${</span><span class="nv">REMOTE_DIR</span><span class="si">}</span><span class="s2"> &amp;&amp; \
</span></span></span><span class="line"><span class="cl"><span class="s2">   docker compose pull &amp;&amp; \
</span></span></span><span class="line"><span class="cl"><span class="s2">   docker compose down --remove-orphans &amp;&amp; \
</span></span></span><span class="line"><span class="cl"><span class="s2">   docker compose up -d&#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>
<p>That&rsquo;s it. <code>build</code>, <code>push</code>, <code>ssh</code> with <code>pull</code>, <code>down</code>, <code>up</code>. Four commands on the remote server, chained in a single SSH. No agent running over there, no webhook, no control panel. The server has a <code>docker-compose.production.yml</code> pointing at the <code>:latest</code> image from the registry, and the <code>pull</code> brings the new version down.</p>
<p>Notice the sensitive data lives in a <code>config/deploy.env</code> that <strong>doesn&rsquo;t go into Git</strong>. The repo only carries the <code>config/deploy.env.example</code>, with the structure and fake values. This &ldquo;versioned example, real one ignored&rdquo; pattern repeats in every project of mine, and it&rsquo;s what lets the project be public without leaking the host, user, or path of my server.</p>
<p>The image itself comes from a multi-stage Dockerfile. The build stage installs the compilation tools, builds everything, and the final stage copies only the result into a lean image, running as a non-root user. The production compose swaps <code>build:</code> for <code>image:</code>, points the volumes at the server&rsquo;s persistent paths (<code>/var/opt/docker/...</code>), and uses <code>restart: always</code> to bring everything back up if the server reboots.</p>
<p>And again, the beauty of standardization: since <strong>every</strong> project of mine has this same <code>bin/deploy</code>, I can tell Claude Code &ldquo;deploy Frank FBI&rdquo; and it knows exactly what to run. I don&rsquo;t have to remember a single command. The tool is the executable documentation of the process.</p>
<h2>The lesson: nobody cares about your stack<span class="hx:absolute hx:-mt-20" id="the-lesson-nobody-cares-about-your-stack"></span>
    <a href="#the-lesson-nobody-cares-about-your-stack" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Now the part I most wanted to write, and the one worth more than all the YAML snippets above.</p>
<p>Most people, when they go to present a project, focus everything on the technology. &ldquo;Check out my brand-new vector database.&rdquo; &ldquo;Check out my last-gen reactive framework.&rdquo; &ldquo;I used such-and-such microservices architecture.&rdquo; And they fill the top of the README with that: the dependency list, the component diagram, the stack.</p>
<p>Nobody cares.</p>
<p>Seriously. The user who lands on your README doesn&rsquo;t want to know which database you used. They want to know <strong>what problem this solves for them</strong>. Implementation detail doesn&rsquo;t belong at the top of the README. It belongs in <code>docs/</code>, for whoever&rsquo;s going to contribute. In the README, what matters is the use case.</p>
<p>Look at the difference. &ldquo;MCP server in Rust with FTS5 and optional LLM consolidation&rdquo; says nothing to anybody. Now &ldquo;long-term memory shared across Claude Code, Codex, and Cursor, with handoff between sessions&rdquo; — that one the guy gets instantly, because it&rsquo;s the pain he has. Both describe ai-memory. One talks about technology, the other talks about the problem it solves.</p>
<p>The use case is the single most important thing about the whole project. Before writing a line of code, the right question is &ldquo;what problem does this solve, and for whom?&rdquo; If you can&rsquo;t answer that in one sentence, you&rsquo;re building a <strong>solution looking for a problem</strong>. And a solution looking for a problem is, in practice, a solution that solves nothing. Doesn&rsquo;t matter how elegant the code is on the inside.</p>
<p>The UX layer, the surface the user touches, is worth far more than the internal engineering. A project with mediocre technology that&rsquo;s trivial to install, that clearly states what problem it solves, and that has honest documentation, goes further than a brilliant project nobody can run. Every time.</p>
<p>So the minimum, the real minimum for an open source project, is this: easy install, CI that gives you confidence, documentation that starts from the problem. The pretty algorithm comes way down the priority list. The three pillars of this post exist to serve one thing, which is the experience of whoever shows up from outside. Get that right first. The rest is implementation detail.</p>
]]></content:encoded><category>open-source</category><category>software-engineering</category><category>devops</category></item><item><title>Wrapping Up My AI Marathon: Success or Failure?</title><link>https://akitaonrails.github.io/en/2026/05/14/wrapping-up-my-ai-marathon-success-or-failure/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/05/14/wrapping-up-my-ai-marathon-success-or-failure/</guid><pubDate>Thu, 14 May 2026 14:00:00 -0300</pubDate><description>&lt;p&gt;I think I&amp;rsquo;m finally out of my AI hyperfocus. When I started ramping up in February and kept going until almost May, the goal was to answer one simple question: can you actually use this new generation of frontier LLMs from Anthropic and OpenAI to code 100% of the time? Short answer: yes.&lt;/p&gt;
&lt;p&gt;Instead of doing a one-day demo and risking a &amp;ldquo;yeah I think so,&amp;rdquo; I went all-in: over 500 hours, almost half a million lines of code and documentation, 24 repositories between public and private.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I think I&rsquo;m finally out of my AI hyperfocus. When I started ramping up in February and kept going until almost May, the goal was to answer one simple question: can you actually use this new generation of frontier LLMs from Anthropic and OpenAI to code 100% of the time? Short answer: yes.</p>
<p>Instead of doing a one-day demo and risking a &ldquo;yeah I think so,&rdquo; I went all-in: over 500 hours, almost half a million lines of code and documentation, 24 repositories between public and private.</p>
<p>But today someone flagged a point I never thought would need defending:</p>
<blockquote>
  <p>&ldquo;Are all 24 projects perfect, models of code excellence?&rdquo; — <strong>ABSOLUTELY NOT.</strong></p>

</blockquote>
<p>I thought that was obvious. For starters, if I believed any of them was that good, I&rsquo;d be spinning up a startup around it. I don&rsquo;t lack capital to invest. If I&rsquo;m not doing that, clearly all of this is at hobby pace.</p>
<p>Second obvious point: if I&rsquo;m jumping between projects every few days, it&rsquo;s because I lost interest. If I really liked it, I&rsquo;d still be tuning it today. I keep saying it in the videos — software never &ldquo;finishes&rdquo;. The moment it stops moving, it dies.</p>
<p>The goal was to test different kinds of projects to see how the LLMs handled each. That&rsquo;s why there&rsquo;s stuff in Ruby on Rails, Rust, Go, Python, Crystal. Plus frameworks like Tauri and Flutter. I figured, for instance, that Flutter being super popular would mean the LLMs wouldn&rsquo;t struggle. They did. They could move forward, just not as smoothly as in Rails. Tauri was &ldquo;rough&rdquo; too. Crystal was REALLY rough.</p>
<p>Third point: every experimental project got named &ldquo;Frank&rdquo;. Sarcasm, obviously. I don&rsquo;t have the patience to invent a pretty name for a project I already know is going to be one-off.</p>
<p>I use my GitHub basically as a repository of exercises and experiments. Look at all the old repos: barely any are a &ldquo;real&rdquo; project I want to maintain. I do what I&rsquo;ve been preaching in the videos: to actually learn, you exercise on MANY projects, even if most of them are meant to be THROWN AWAY. Nobody learns by trying to build a single perfect project. Check out my videos <a href="/2020/05/15/a-dor-de-aprender/">A Dor de Aprender</a> or <a href="/2020/05/16/guia-definitivo-de-aprendendo-a-aprender/">Guia Definitivo de Aprendendo a Aprender</a> (Portuguese).</p>
<p>In the middle of a bunch of half-baked projects, some naturally emerge as the ones I like and maintain. In practice I threw them all at the wall to see what stuck. The good ones naturally pulled in more contributors too. I don&rsquo;t keep an eye on Issues and PRs every day. I check once a week. Two or three days ago I made a sweep and cleared everything that was pending, for example. Even on projects I don&rsquo;t care much about, if a PR shows up, I review and accept it. It wasn&rsquo;t useful to me, but it can be useful to someone else.</p>
<h2>The project ranking<span class="hx:absolute hx:-mt-20" id="the-project-ranking"></span>
    <a href="#the-project-ranking" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>To make it clear, here&rsquo;s a ranking of the projects with a comment on each. The PR counts below are non-dependabot (gem bumps don&rsquo;t count). The score is my estimate of &ldquo;how stable and usable is this thing in its current state.&rdquo;</p>
<h3>Top tier (7-8/10)<span class="hx:absolute hx:-mt-20" id="top-tier-7-810"></span>
    <a href="#top-tier-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> The only one really close to a stable 1.0 release. Got the most traction: 19 PRs closed, 23 Issues resolved. Recommend using, it works very well.</p>
<p><strong>akitando-news (the M.Akita Chronicles) — 8/10.</strong> The one I built with the most care, in the &ldquo;Agile Vibe Coding&rdquo; mode I described a few posts back. And the one I touch most often. There&rsquo;s always some little thing to tweak, stuff I keep adding, removing, experimenting with. Which is why it&rsquo;s been shipping every week. Also why: it&rsquo;s closed and I don&rsquo;t plan to open it.</p>
<p><strong><a href="https://github.com/akitaonrails/akitaonrails.github.io"target="_blank" rel="noopener">akitaonrails.github.io</a> — 7/10.</strong> This is the blog you&rsquo;re reading. I touch it often and I&rsquo;ve already closed about 7 Issues and 30 PRs from contributors. I&rsquo;ll keep tuning it bit by bit.</p>
<p><strong><a href="https://github.com/akitaonrails/FrankMD"target="_blank" rel="noopener">FrankMD</a> — 7/10.</strong> One of the first ones I built, with no criteria at all: I just wanted an editor with automatic image upload to my S3. These days I use Claude Code to edit posts, so the only people using it are the people contributing to it. Got solid traction, I closed 17 PRs and 29 Issues. It&rsquo;s not perfect, but it&rsquo;s good enough to use — at least it&rsquo;s been tested enough.</p>
<p><strong><a href="https://github.com/akitaonrails/easy-ffmpeg"target="_blank" rel="noopener">easy-ffmpeg</a> — 7/10.</strong> Tiny little project, but I actually use it in the videos. My OBS always records in MKV and when I want to share I convert to MP4. Small use cases like that. Good enough to use.</p>
<h3>Middle tier (5/10)<span class="hx:absolute hx:-mt-20" id="middle-tier-510"></span>
    <a href="#middle-tier-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> Most recent one, serves as a &ldquo;dump&rdquo; of documentation for my retro games. I use it as a notepad and it&rsquo;s been working fine for my setup. Not worried about it working for anyone else. If anyone wants to send PRs, go ahead.</p>
<p><strong><a href="https://github.com/akitaonrails/llm-coding-benchmark"target="_blank" rel="noopener">llm-coding-benchmark</a> — 5/10.</strong> This one doesn&rsquo;t run on its own. I have Claude Code execute it. The goal is to tabulate data so I can write articles about new LLMs. The code has to be REALLY rough.</p>
<p><strong><a href="https://github.com/akitaonrails/FrankMega"target="_blank" rel="noopener">FrankMega</a> — 5/10.</strong> The closest thing to a &ldquo;todo list&rdquo; exercise. I just needed to share a big file with a friend and thought &ldquo;hmm, I bet I can build a mini-Mega in 1 day&rdquo;. And that was it. The name is a joke, obviously — this is nowhere near the actual Mega.</p>
<p><strong><a href="https://github.com/akitaonrails/distrobox-llm"target="_blank" rel="noopener">distrobox-llm</a> — 5/10.</strong> More personal organization on my PC: I wanted to isolate the AI tooling into a distrobox so I wouldn&rsquo;t have to keep updating big packages on the main system.</p>
<h3>Lower tier (3-4/10)<span class="hx:absolute hx:-mt-20" id="lower-tier-3-410"></span>
    <a href="#lower-tier-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> This was an idea I&rsquo;d had in my head for years. I just wanted to see if the concept worked. It works. But running local LLMs for it is slow and not as practical as I&rsquo;d like. It&rsquo;s the only one I might come back to, because I actually want to use it. As-is, it&rsquo;s in &ldquo;works, sort of&rdquo; territory.</p>
<p><strong><a href="https://github.com/akitaonrails/NW-Omarchy"target="_blank" rel="noopener">NW-Omarchy</a> — 4/10.</strong> Another &ldquo;let&rsquo;s see if this works&rdquo; experiment, lasted 2 days. The concept worked, but I don&rsquo;t plan to do much more than that. Up for grabs if anyone wants to take it further. The real test I wanted to run was whether X11 would be MUCH worse than Wayland. The gap turned out smaller than I expected, but I don&rsquo;t see myself using it day-to-day. Like I&rsquo;ve said: for the niche of people who can&rsquo;t use Wayland, it&rsquo;s a decent alternative.</p>
<p><strong><a href="https://github.com/akitaonrails/frank_investigator"target="_blank" rel="noopener">frank_investigator</a> — 3/10.</strong> Started it, didn&rsquo;t have the patience to keep going. Planted the seeds of the idea. If nobody sends PRs, it&rsquo;s not going to evolve much. Needs more research and a lot more testing.</p>
<p><strong><a href="https://github.com/akitaonrails/frank_fbi"target="_blank" rel="noopener">frank_fbi</a> — 3/10.</strong> Another concept test, no personal use case (I never fall for phishing). No idea where this would go yet.</p>
<p><strong><a href="https://github.com/akitaonrails/frank_karaoke"target="_blank" rel="noopener">frank_karaoke</a> — 3/10.</strong> Born from a weekend conversation with my brothers. Another shot at using Flutter (and seeing the limits, both of Flutter and of LLMs writing Flutter). Useful for me to learn about the karaoke ecosystem and find out that real scoring only works with per-song metadata. So I didn&rsquo;t push it past toy territory.</p>
<h3>Bottom of the barrel (1-2/10)<span class="hx:absolute hx:-mt-20" id="bottom-of-the-barrel-1-210"></span>
    <a href="#bottom-of-the-barrel-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> I don&rsquo;t use it myself. So it&rsquo;s probably full of bugs that nobody&rsquo;s fixed. It was a test to see if I could build a Tauri app, but I lost interest fast. There&rsquo;s room to make it better, but it&rsquo;d take a lot more hours of actual use, testing, and fixes. It was also my first time touching Tauri, so that part is probably pretty rough too.</p>
<p><strong><a href="https://github.com/akitaonrails/FrankClaw"target="_blank" rel="noopener">FrankClaw</a> — 1/10.</strong> I don&rsquo;t use it, and this is the only one I haven&rsquo;t even run a single time. The exercise was just to see how hard it&rsquo;d be to port the original openclaw from TypeScript to Rust. But as I&rsquo;ve said many times, I don&rsquo;t see any use for openclaw in my own workflow. When I finished version 0.1, I stopped. Even so, there were 6 PRs I closed. If anyone&rsquo;s interested, feel free to contribute.</p>
<p><strong><a href="https://github.com/akitaonrails/shadPS4"target="_blank" rel="noopener">shadPS4</a> — 1/10.</strong> Despite generating a lot of code and a lot of documentation, none of it was meant to be a real contribution. Pure learning, plus solving one very specific problem. I left it open just to show what &ldquo;study-material&rdquo; code looks like.</p>
<p><strong><a href="https://github.com/akitaonrails/easy-subtitle"target="_blank" rel="noopener">easy-subtitle</a> — 1/10.</strong> Started it, lost interest, never actually tested it. Just pushed it and forgot.</p>
<h3>The private ones<span class="hx:absolute hx:-mt-20" id="the-private-ones"></span>
    <a href="#the-private-ones" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Beyond akitando-news, I have stuff on my private Gitea like <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. I&rsquo;m not going to describe them because you&rsquo;re never going to see them. They serve as &ldquo;scratchpads&rdquo; for Claude Code to administer my machines.</p>
<h2>What this ranking actually says<span class="hx:absolute hx:-mt-20" id="what-this-ranking-actually-says"></span>
    <a href="#what-this-ranking-actually-says" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Notice: there&rsquo;s no project on this list that I think is 10/10.</p>
<p>For a project to graduate from hobby to real project, you need users and feedback. If nobody uses it, there are always dozens of functional bugs nobody&rsquo;s stumbled on, so nothing ever gets fixed. A project with no users is a dead project.</p>
<p>That&rsquo;s why I keep saying in the videos: there&rsquo;s no such thing as planning every detail, writing a pile of code, and one day &ldquo;finishing&rdquo;. Nope. When you think you&rsquo;ve &ldquo;finished,&rdquo; what you actually have is version 0.1. Then comes months of experimentation with real users reporting bugs. You go fixing. Out of those fixes come refactors, architecture changes because your initial assumptions were wrong against actual use. Software NEVER finishes.</p>
<h2>What I took out of this marathon<span class="hx:absolute hx:-mt-20" id="what-i-took-out-of-this-marathon"></span>
    <a href="#what-i-took-out-of-this-marathon" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>16 hours a day, every day, vibe coding, the output is always going to have slop in it. There&rsquo;s no way around it. Forcing volume doesn&rsquo;t fix it. You have to review manually, commit by commit, PR by PR. Why didn&rsquo;t I do that on all of them? Simple: nobody&rsquo;s paying me. I did it on 2 or 3 projects. The rest I just wanted to test the concept and move on to the next one.</p>
<p>The right way is what I reported a few posts ago: &ldquo;Agile&rdquo; Vibe Coding. That&rsquo;s my sarcastic way of saying &ldquo;use XP and normal software engineering practices&rdquo;. Metrics, monitoring, CI, the whole nine yards. None of that changed. Without it, you&rsquo;ll end up like a lot of my exercises: one-offs to throw away.</p>
<p>Don&rsquo;t be embarrassed to push &ldquo;bad&rdquo; projects. What I didn&rsquo;t anticipate is that some people assume just because I pushed code, I think it&rsquo;s perfect. But that&rsquo;s projection: they only push code they consider great, which is why they never push anything. I&rsquo;ve said this MANY times and I demonstrate it: I don&rsquo;t care about my code, I have no preciousness about my code, GitHub is a temp dump directory for whatever. The ones I really like usually live private.</p>
<p>If a project is really good, don&rsquo;t just say it. Put your money where your mouth is: build a business around it, charge users, deliver quality. I&rsquo;m retired, I don&rsquo;t need to charge anyone, but I also have no interest in working for free. The moment the code stops being fun, I drop it and move on. The ones I actually got hooked on (ai-jail, M.Akita Chronicles) I keep touching more often. That simple.</p>
<p>As I expected, and as I showed both in these little projects and in the benchmark, the choice is simple: use GPT Codex or Claude Code on one of the subsidized plans. Don&rsquo;t pay per token. That&rsquo;s where the 5x productivity lives (max 10x, with slop along for the ride). Yes, there&rsquo;s Kimi v2.6 or DeepSeek V4 Pro. But I&rsquo;ll save you the trip: the projects I scored below 5 here would quickly become projects below 2 with those.</p>
<h2>The point that matters most<span class="hx:absolute hx:-mt-20" id="the-point-that-matters-most"></span>
    <a href="#the-point-that-matters-most" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>AI doesn&rsquo;t replace a good programmer. AI mirrors and accelerates who you are. If you&rsquo;re a good programmer, AI accelerates good code. If you&rsquo;re a bad programmer, AI accelerates bad code.</p>
<p>That&rsquo;s exactly what I showed: most of the projects I built were one-offs and proof-of-concept tests. Bad code, with no interest from me to keep going. AI accelerated me finishing those tests in a half-baked way. Which is fine by me: without AI I never would&rsquo;ve started. Now that I&rsquo;ve seen what they look like, I can cross them off my list and move on.</p>
<p>What matters is your OBJECTIVE. If yours is to build a business and charge people, you have to do it with 10x more care than I did. Everything I do is meant to be thrown away. I don&rsquo;t need to make money on any of these projects. Don&rsquo;t assume things I never said.</p>
]]></content:encoded><category>vibe-coding</category><category>coding-agents</category><category>software-engineering</category></item><item><title>Clean Code for AI Agents</title><link>https://akitaonrails.github.io/en/2026/04/20/clean-code-for-ai-agents/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/04/20/clean-code-for-ai-agents/</guid><pubDate>Mon, 20 Apr 2026 12:00:00 -0300</pubDate><description>&lt;p&gt;In 2008, Robert C. Martin (Uncle Bob) published &lt;strong&gt;Clean Code: A Handbook of Agile Software Craftsmanship&lt;/strong&gt;. It&amp;rsquo;s one of the most influential software engineering books of the last couple of decades. For those who don&amp;rsquo;t know, Uncle Bob started programming professionally at 17, founded Object Mentor, signed the Agile Manifesto, served as the first chairman of the Agile Alliance, coined the SOLID acronym. He&amp;rsquo;s written a dozen books on design, architecture, and practice, and influenced entire generations of developers.&lt;/p&gt;</description><content:encoded><![CDATA[<p>In 2008, Robert C. Martin (Uncle Bob) published <strong>Clean Code: A Handbook of Agile Software Craftsmanship</strong>. It&rsquo;s one of the most influential software engineering books of the last couple of decades. For those who don&rsquo;t know, Uncle Bob started programming professionally at 17, founded Object Mentor, signed the Agile Manifesto, served as the first chairman of the Agile Alliance, coined the SOLID acronym. He&rsquo;s written a dozen books on design, architecture, and practice, and influenced entire generations of developers.</p>
<p>I&rsquo;ve been following Uncle Bob for many years. I&rsquo;ve exchanged messages with him a few times over the decades, I have formed opinions about his positions, and I did an hour-long live stream on my channel with him about Clean Code, Agile, Craftsmanship, and where the industry was heading. If you&rsquo;ve never watched it, I recommend it:</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/ycvaECDc31w"
    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>Clean Code specifically set a standard: code is written once but read dozens of times. The programmer&rsquo;s job isn&rsquo;t just to make it work. It&rsquo;s to make it work <strong>in a way that another programmer can understand, modify, and not break</strong>. Meaningful names, small functions, one responsibility per class, no duplication, automated tests, clear structure. The target audience was always another human being sitting at an editor trying to figure out what the first one did.</p>
<p>In 2026, that audience changed.</p>
<h2>The audience isn&rsquo;t human anymore<span class="hx:absolute hx:-mt-20" id="the-audience-isnt-human-anymore"></span>
    <a href="#the-audience-isnt-human-anymore" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Last week I wrote <a href="/en/2026/04/11/vs-code-is-the-new-punch-card/">VS Code Is the New Punch Card</a>. The thesis is that typing code manually into a text editor is becoming a niche activity, the same way typing binary directly on an Altair front panel became a relic after compilers got good. The AI agent is the new compiler: I describe intent in natural language, the agent navigates code, edits, runs tests, adjusts, delivers.</p>
<p>If that thesis holds, the next question is obvious: <strong>who are we writing code for now?</strong></p>
<p>Not for the human programmer who&rsquo;ll sit down tomorrow to maintain it. It&rsquo;s for the agent that&rsquo;ll read, edit, and extend it. And an agent is not human. Agents have different technical constraints, different biases, different limitations. Part of Clean Code still applies (in some cases more critically). Another part shifts in weight. And new demands emerge that Uncle Bob couldn&rsquo;t have anticipated in 2008.</p>
<p>This article is about that. What&rsquo;s the version of Clean Code that makes sense when the primary reader is an LLM?</p>
<h2>The real agent constraints<span class="hx:absolute hx:-mt-20" id="the-real-agent-constraints"></span>
    <a href="#the-real-agent-constraints" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before re-ranking, worth reviewing what agents actually face.</p>
<p><strong>File truncation.</strong> Most agent CLIs limit file reads to small ranges. Claude Code reads 2000 lines per chunk by default. Cursor, Codex, Windsurf, all have similar caps. A giant file simply doesn&rsquo;t fit into the context window in one shot. The agent has to ask for piece by piece, or worse, use grep and reconstruct mentally.</p>
<p><strong>Attention degrades with context.</strong> Claude Opus has a 200k-token window, Sonnet 1M, Gemini 1M. Sounds like a lot. In practice, &ldquo;needle in haystack&rdquo; tests show retrieval quality drops well before the claimed limit. Flash Attention and variants speed up the computation, but they don&rsquo;t replace full native attention. The more you stuff into the window, the worse the detail precision. And the agent&rsquo;s context isn&rsquo;t just your code: it holds CLAUDE.md, the system prompt, chat history, tool output, error logs, test output. All competing for the same window.</p>
<p><strong>Grep is cheaper than read.</strong> The agent knows this. It prefers <code>rg &quot;funcName&quot;</code> over loading a whole file. It&rsquo;s faster, uses fewer tokens, hits the target. Unique, distinctive names make that much more effective. This isn&rsquo;t a shortcut, it&rsquo;s an architectural choice: I wrote about this in detail in <a href="/en/2026/04/06/rag-is-dead-long-context/">Is RAG Dead? Long Context, Grep, and the End of the Mandatory Vector DB</a>, showing that Claude Code itself navigates a repo with <code>Glob</code> and <code>Grep</code>, no vector DB, no embedding, and that&rsquo;s not a deficiency, it&rsquo;s mature design. Lexical search + smart reader consuming raw text beats dense retriever + top-k on practically every real-domain benchmark. You benefit from that when you organize your code: greppable names aren&rsquo;t just &ldquo;nice for humans&rdquo;, they&rsquo;re the agent&rsquo;s primary navigation API.</p>
<p><strong>Tool calls cost tokens.</strong> Every <code>Read</code> or <code>Edit</code> or <code>Bash</code> burns input and output tokens. Short files, small test output, concise logs — all of that keeps the agent productive and the bill low.</p>
<p><strong>Latency matters.</strong> Agent in a loop, each tool call adds seconds. A large file slow to process becomes perceptible session friction.</p>
<p><strong>Grepping by visual pattern is hard.</strong> If you used inconsistent indentation, mixed tabs and spaces, varied brace style between files, the agent spends tokens internalizing the mess. Consistency helps.</p>
<p>From those constraints, Clean Code principles can be re-ranked by relevance for agent-facing work.</p>
<h2>Re-ranking: Clean Code in the age of agents<span class="hx:absolute hx:-mt-20" id="re-ranking-clean-code-in-the-age-of-agents"></span>
    <a href="#re-ranking-clean-code-in-the-age-of-agents" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Most important to least. A caveat: it&rsquo;s not that the ones at the bottom stop mattering. It&rsquo;s that the ones at the top started mattering MUCH more.</p>
<h3>1. Small functions (and small files)<span class="hx:absolute hx:-mt-20" id="1-small-functions-and-small-files"></span>
    <a href="#1-small-functions-and-small-files" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Uncle Bob: &ldquo;functions should do ONE thing, they should do it WELL, and they should do it only&rdquo;. Ideal size 4 to 20 lines, per the book.</p>
<p>For an agent, that recommendation became a technical obligation. A small function fits in a single tool call without truncation. A short file (keep it under 500 lines, ideally 200-300) fits in a single read. If the agent can grab the whole unit of meaning in one call, it reasons about it with full attention. If it has to paginate, it builds a fragmented mental model, and each fragment costs attention.</p>
<p>Before, &ldquo;small function&rdquo; was good for humans because it aided reading. Today, &ldquo;small function&rdquo; is good because it matches the model&rsquo;s unit of processing. If there&rsquo;s one recommendation to take to heart, it&rsquo;s this one.</p>
<h3>2. Single Responsibility Principle (SRP)<span class="hx:absolute hx:-mt-20" id="2-single-responsibility-principle-srp"></span>
    <a href="#2-single-responsibility-principle-srp" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Each module does one thing and has one reason to change. It was already the heart of Clean Code. For an agent, it becomes even more critical because:</p>
<ul>
<li>The agent can isolate the unit to understand without loading the rest of the system</li>
<li>You can run focused tests on it</li>
<li>You can edit without fearing side effects</li>
<li>Grepping by responsibility becomes predictable</li>
</ul>
<p>Code with tangled responsibilities forces the agent to load way more context for any simple change. An 800-line class that does three things is worse for the agent than three 250-line classes, even if the total is the same.</p>
<h3>3. Meaningful and unique names<span class="hx:absolute hx:-mt-20" id="3-meaningful-and-unique-names"></span>
    <a href="#3-meaningful-and-unique-names" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Clean Code already preached: names reveal intention, no disinformation, distinctive, pronounceable, searchable. For the agent, &ldquo;searchable&rdquo; became the most important property of that list.</p>
<p>The agent searches code via grep/ripgrep all the time. A generic name (<code>data</code>, <code>process</code>, <code>handler</code>, <code>Manager</code>, <code>Service</code>) returns fifty matches and forces the agent to read each one. A distinctive name (<code>UserRegistrationValidator</code>, <code>InvoiceLineItemTotal</code>, <code>ClaudeCodeSessionTracker</code>) returns three matches and the agent goes straight to the right one.</p>
<p>Rule of thumb: if you grep the name and a lot of irrelevant stuff comes back, the name is bad for the agent. If only what matters comes back, the name is right.</p>
<h3>4. Comments with context and provenance<span class="hx:absolute hx:-mt-20" id="4-comments-with-context-and-provenance"></span>
    <a href="#4-comments-with-context-and-provenance" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is where the inversion is most jarring. For Uncle Bob in 2008, the axiom was: &ldquo;good code explains itself, excessive comments are a code smell, every comment is debt that gets stale&rdquo;. Every experienced programmer who read the book absorbed that rule. Well-named code doesn&rsquo;t need comments. Too many comments = flag of bad code trying to justify itself.</p>
<p>Now flip it. <strong>The agent reads comments. And likes them</strong>. Comments become first-class context. The agent has perfect syntax fluency, knows exactly what <code>x++</code> does, doesn&rsquo;t need obvious captions (that kind is still bad, see item 13). What it DOESN&rsquo;T know is why you chose this approach over the obvious one, what production bug motivated this weird logic, what business constraint forces this specific order, what workaround exists because the upstream lib has known bug #1234, which commit introduced this decision, which Jira issue is the reference. That kind of information is <strong>provenance</strong>: the why of the decision. It only exists in the head of the human who wrote it, in the commit message, or in a well-placed comment. For the agent, the comment is the most accessible source during a tool call.</p>
<p>Docstrings with intent and usage examples also became strong signals. When the agent picks up a function without understanding the context, a header docstring (JSDoc-style with examples, Python <code>&quot;&quot;&quot;</code>, Rust <code>///</code>) drastically shortens the path to a correct change. Uncle Bob was skeptical of JavaDoc in 2008 because they got stale. Today, with the agent able to rewrite the docstring alongside the code, that counter-argument lost weight.</p>
<p>A practical consequence: <strong>don&rsquo;t prune the comments the agent writes</strong>. If you have the reflex &ldquo;verbose comment is noise&rdquo; inherited from the original Clean Code era, that rule flipped. The agent wrote that comment because, in the act of generating the code, it decided that information was worth preserving for future edits. Removing the comment in code review strips context that the agent itself will want to read on the next interaction. Let the agent comment. It knows what it&rsquo;s doing. The only kind of agent-authored comment worth removing is the obvious redundant one (item 13), and modern models rarely produce those if the system prompt is well written.</p>
<h3>5. Explicit types<span class="hx:absolute hx:-mt-20" id="5-explicit-types"></span>
    <a href="#5-explicit-types" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This isn&rsquo;t in 2008&rsquo;s Clean Code because the industry hadn&rsquo;t converted yet. But in 2026 it&rsquo;s a fundamental criterion.</p>
<p>Python without type hints, JavaScript instead of TypeScript, Ruby without RBS. Dynamic code without annotations forces the agent to infer types from usage, which costs reasoning and gets it wrong frequently. Typed code gives an immediate answer key: the signature says what goes in, what comes out, which states are valid. The agent saves discovery work and makes fewer mistakes.</p>
<p>If you&rsquo;re still on Python 3 without type hints, the transition will boost agent productivity more than any logic refactor.</p>
<h3>6. DRY (Don&rsquo;t Repeat Yourself)<span class="hx:absolute hx:-mt-20" id="6-dry-dont-repeat-yourself"></span>
    <a href="#6-dry-dont-repeat-yourself" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Clean Code already said duplication is the root of all evil. For an agent, duplication is worse than for a human for one specific reason: when the agent has to change something that&rsquo;s replicated, it can update one copy and forget the others. The attention window doesn&rsquo;t have natural gravity pulling &ldquo;oh, there are two more copies of this in other files&rdquo;. The agent has to find each one via grep, and if the pattern has subtle variation between copies, the result ends up inconsistent.</p>
<p>Factoring into a reusable function or module isn&rsquo;t aesthetic. It&rsquo;s automated-refactor safety.</p>
<h3>7. Tests the agent can run<span class="hx:absolute hx:-mt-20" id="7-tests-the-agent-can-run"></span>
    <a href="#7-tests-the-agent-can-run" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Uncle Bob dedicates a full chapter to Unit Tests and F.I.R.S.T (Fast, Independent, Repeatable, Self-Validating, Timely). All of it still applies, with an important addendum: <strong>the test has to be executable by the agent without human setup</strong>.</p>
<p>Meaning: the command to run the test is in the README or CLAUDE.md, in the <code>Makefile</code>, in the <code>package.json</code>. Output has a predictable format the agent parses. It doesn&rsquo;t depend on manually seeding the database, on a config file that isn&rsquo;t in the repo, on a secret credential. The agent writes code, runs tests, reads output, adjusts, runs again. That cycle is the foundation. If the tests don&rsquo;t run headless, the agent goes blind.</p>
<p>Here I speak from field experience. I documented this in <a href="/en/2026/02/20/zero-to-post-production-in-1-week-using-ai-on-real-projects-behind-the-m-akita-chronicles/">From Zero to Post-Production in 1 Week Using AI</a>, where I went hard on a real project: 274 commits in 8 days, 4 integrated applications, 1,323 automated tests by the end. What made it work wasn&rsquo;t &ldquo;AI programs on its own&rdquo;. It was <strong>Extreme Programming with the agent as pair instead of a human pair</strong>. Running tests on every commit, tight CI, coverage above 80% (95%+ on business logic), test-line-to-code-line ratio above 1:1 on some modules. Sounds like overkill. It isn&rsquo;t. In 274 commits, CI caught real bugs more than 50 times, bugs that would have gone straight to production if I had blindly trusted the agent. Without tests, the agent hands you plausible code that silently breaks something that worked yesterday. With strong tests, the agent becomes a multiplier: it generates a test, the test validates the code it wrote, the test is the safety net for the next change it makes. Virtuous loop.</p>
<p>XP practices (pair programming, CI, tests first, continuous refactoring, short feedback) didn&rsquo;t become obsolete. They became <strong>exactly the right way to work with an agent</strong>. Whoever programs in cowboy mode without tests today isn&rsquo;t rebellious. They&rsquo;re just slow, because the agent without tests keeps guessing, and guesses need manual review, which kills the speed the agent should bring. Good tests with good coverage became the difference between a productive agent and an agent that keeps flailing. Or, put another way: <strong>TDD became a technical obligation, not a philosophy</strong>.</p>
<p>I covered this theme from another angle in <a href="/en/2026/03/01/software-is-never-done-4-projects-life-after-deploy-one-shot-prompt-myth/">Software Is Never &ldquo;Done&rdquo;</a>, showing that post-deploy life is where tests matter most: in ten days of operation after launch, I ran 56 commits of fixes, hardening, and adjustment in response to real behavior, and each commit came with a regression test. Without the net, each of those 56 commits would be an opportunity to break something that worked yesterday. TDD isn&rsquo;t a phase, it&rsquo;s a habit.</p>
<h3>8. Predictable directory structure<span class="hx:absolute hx:-mt-20" id="8-predictable-directory-structure"></span>
    <a href="#8-predictable-directory-structure" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Clean Code barely discusses this (it was more focused on code inside a file). For an agent, tree organization matters. If <code>src/controllers/users.rb</code> implies <code>src/models/user.rb</code> and <code>src/views/users/</code>, the agent can anticipate paths without listing directories. If the project uses idiosyncratic naming (random files, unpatterned names, everything flat in one folder), the agent loses time with <code>find</code>.</p>
<p>Strong framework conventions (Rails, Django, Next.js, Laravel) help the agent a lot. Project without convention, the agent builds one over time, but until then it burns tokens exploring.</p>
<h3>9. Dependency Injection and Testability<span class="hx:absolute hx:-mt-20" id="9-dependency-injection-and-testability"></span>
    <a href="#9-dependency-injection-and-testability" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Code with injected dependencies (not hardcoded) is easier to test in isolation. The agent benefits from this. It can swap the real <code>EmailSender</code> for a <code>FakeEmailSender</code> in a test without touching the logic. Code that instantiates its dependencies internally forces the agent into monkey-patch-fake-server-hacks that are slow, fragile, and pollute the session with infra grime.</p>
<p>DI isn&rsquo;t ceremony. It&rsquo;s isolation scope. And in a real project, DI quickly becomes a load-bearing refactor: on one of my projects (the <a href="/en/2026/03/01/software-is-never-done-4-projects-life-after-deploy-one-shot-prompt-myth/">M.Akita Chronicles</a>), I discovered after launch that I needed to swap the default LLM model to another provider. The environment variable had existed from the start. But the model name was still hardcoded in references across 24 files. A whole commit (<code>Centralize LLM model config</code>) touched all 24 to isolate the config into a single constant. Swapping models after that became a one-line change. That&rsquo;s exactly the kind of refactor that only shows up after software meets reality, and it&rsquo;s where DI and config isolation pay dearly if you didn&rsquo;t do it earlier.</p>
<h3>10. Avoid deep nesting<span class="hx:absolute hx:-mt-20" id="10-avoid-deep-nesting"></span>
    <a href="#10-avoid-deep-nesting" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Clean Code talks about single level of abstraction per function. A corollary: avoid <code>if</code> inside <code>for</code> inside <code>if</code> inside <code>try</code>. Every indentation level is more attention the model has to spend tracking state. Four levels of indentation is MUCH more cognitively expensive for the agent than two levels with early return.</p>
<p>Pattern matching, guard clauses, early returns, flattening logic, all of this improves readability for the model the same way it improves it for the human, except measurably so, because the cost is measured in response quality.</p>
<h3>11. Errors with context<span class="hx:absolute hx:-mt-20" id="11-errors-with-context"></span>
    <a href="#11-errors-with-context" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><code>raise ValueError(&quot;invalid input&quot;)</code> doesn&rsquo;t help the agent when it reads the stack trace. <code>raise ValueError(f&quot;invalid input: received {repr(x)}, expected non-empty string of digits&quot;)</code> does. The agent uses exception messages as debug signal. Vague message = agent runs an extra round to figure out what went wrong.</p>
<p>Uncle Bob talked about this in Error Handling: &ldquo;Provide context with exceptions&rdquo;. It became critical now.</p>
<h3>12. Formatting and style<span class="hx:absolute hx:-mt-20" id="12-formatting-and-style"></span>
    <a href="#12-formatting-and-style" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Don&rsquo;t waste time on this. Use the default or most popular formatter for your language: <code>cargo fmt</code> for Rust, <code>gofmt</code> for Go, <code>prettier</code> for JS/TS, <code>black</code> or <code>ruff</code> for Python, <code>rubocop -A</code> for Ruby. Configure it in pre-commit, configure it in your editor to run on save, and move on. The agent handles any consistent style just fine, and the auto-formatter keeps the diff tidy between commits. Tab vs space, 80 vs 100 columns, brace style, all of that became noise. The formatter decides, you accept.</p>
<h3>13. Comments that describe the obvious<span class="hx:absolute hx:-mt-20" id="13-comments-that-describe-the-obvious"></span>
    <a href="#13-comments-that-describe-the-obvious" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Last on the list. Still bad, got even worse. Comments like <code>// increment i by 1</code> above <code>i++</code> waste the agent&rsquo;s tokens the same way they wasted the human&rsquo;s patience. The model knows how to read code, it doesn&rsquo;t need obvious captions.</p>
<p>If you have the habit of writing obvious comments because some school taught you that way, this is the moment to stop. In 2008 it was bad because it polluted visual space. In 2026 it&rsquo;s bad because it costs real money in tokens.</p>
<h2>What Uncle Bob couldn&rsquo;t foresee<span class="hx:absolute hx:-mt-20" id="what-uncle-bob-couldnt-foresee"></span>
    <a href="#what-uncle-bob-couldnt-foresee" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Beyond re-ranking what was in the book, some new things emerged that are specific to the agent world:</p>
<p><strong>Meta-documentation files for agents.</strong> <code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>.cursor/rules</code>, and so on. These are files the agent reads before any tool call, describing project conventions, important commands, caveats, things that don&rsquo;t go in a docstring. Writing these files is a new skill: short, direct, imperative, action-oriented. No philosophical prose. Bullet points of what the agent needs to know to not mess up.</p>
<p><strong>README with high-level architecture.</strong> Uncle Bob barely cared about README (the book is about code). For the agent, well-made READMEs drastically shorten the path to understanding the shape of the project. A simple ASCII or Mermaid diagram helps.</p>
<p><strong>Structured logging.</strong> JSON logs with named fields are much more useful for the agent than prose logs. The agent parses JSON trivially, uses fields to filter relevant errors, correlates across services. A loose <code>printf</code> in free text forces heuristic parsing.</p>
<p><strong>Accessible observability commands.</strong> <code>pnpm test</code>, <code>make lint</code>, <code>cargo check</code>, <code>python -m mypy</code> — the more the project exposes predictable commands the agent can invoke to validate changes, the better. If running tests requires 10 manual setup steps, the agent won&rsquo;t run tests, and the feedback loop breaks.</p>
<p><strong>Idempotent setup scripts.</strong> The agent has to be able to run <code>bin/setup</code> or <code>scripts/bootstrap.sh</code> on a clean machine and reach a state where it can work. If onboarding depends on instructions in someone&rsquo;s head, the agent is locked out.</p>
<h2>Instructing the agent to write clean code<span class="hx:absolute hx:-mt-20" id="instructing-the-agent-to-write-clean-code"></span>
    <a href="#instructing-the-agent-to-write-clean-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s an important detail that only becomes clear after about 500 hours of using agents: <strong>no LLM does any of this by default</strong>. You tell it &ldquo;implement feature X&rdquo; and it&rsquo;ll implement it the way the model considers average. No dependency injection. 80-line functions. No tests, or tests that mock the wrong thing. Duplicated logic because it&rsquo;s faster. 2000-line files because &ldquo;everything&rsquo;s in one place&rdquo;. You need to WRITE these rules. The agent reads, the agent follows.</p>
<p>Where to write: <code>CLAUDE.md</code>, <code>AGENTS.md</code>, <code>.cursor/rules</code>, <code>.github/copilot-instructions.md</code>, depending on the CLI. Format: short, imperative, action-oriented. The agent reads these files on every iteration (Claude Code re-reads CLAUDE.md on every query), so each line burns context tokens — density matters.</p>
<p>Below is a proposal for a template you can drop into a <code>CLAUDE.md</code> on a new project, consolidating what I discussed above in a format the agent consumes. <strong>This is not a definitive version</strong>, it&rsquo;s a starting point to test and tune to your language, your team, your flow. If a rule doesn&rsquo;t fit your context, remove it. If you need a new rule, add it. The point is to have a structured skeleton:</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-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gu">## Code style
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Functions: 4-20 lines. Split if longer.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Files: under 500 lines. Split by responsibility.
</span></span><span class="line"><span class="cl"><span class="k">-</span> One thing per function, one responsibility per module (SRP).
</span></span><span class="line"><span class="cl"><span class="k">-</span> Names: specific and unique. Avoid <span class="sb">`data`</span>, <span class="sb">`handler`</span>, <span class="sb">`Manager`</span>.
</span></span><span class="line"><span class="cl">  Prefer names that return &lt;5 grep hits in the codebase.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Types: explicit. No <span class="sb">`any`</span>, no <span class="sb">`Dict`</span>, no untyped functions.
</span></span><span class="line"><span class="cl"><span class="k">-</span> No code duplication. Extract shared logic into a function/module.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Early returns over nested ifs. Max 2 levels of indentation.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Exception messages must include the offending value and expected shape.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Comments
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Keep your own comments. Don&#39;t strip them on refactor — they carry
</span></span><span class="line"><span class="cl">  intent and provenance.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Write WHY, not WHAT. Skip <span class="sb">`// increment counter`</span> above <span class="sb">`i++`</span>.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Docstrings on public functions: intent + one usage example.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Reference issue numbers / commit SHAs when a line exists because
</span></span><span class="line"><span class="cl">  of a specific bug or upstream constraint.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Tests
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Tests run with a single command: <span class="sb">`&lt;project-specific&gt;`</span>.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Every new function gets a test. Bug fixes get a regression test.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Mock external I/O (API, DB, filesystem) with named fake classes,
</span></span><span class="line"><span class="cl">  not inline stubs.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Tests must be F.I.R.S.T: fast, independent, repeatable,
</span></span><span class="line"><span class="cl">  self-validating, timely.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Dependencies
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Inject dependencies through constructor/parameter, not global/import.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Wrap third-party libs behind a thin interface owned by this project.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Structure
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Follow the framework&#39;s convention (Rails, Django, Next.js, etc.).
</span></span><span class="line"><span class="cl"><span class="k">-</span> Prefer small focused modules over god files.
</span></span><span class="line"><span class="cl"><span class="k">-</span> Predictable paths: controller/model/view, src/lib/test, etc.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Formatting
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Use the language default formatter (<span class="sb">`cargo fmt`</span>, <span class="sb">`gofmt`</span>, <span class="sb">`prettier`</span>,
</span></span><span class="line"><span class="cl">  <span class="sb">`black`</span>, <span class="sb">`rubocop -A`</span>). Don&#39;t discuss style beyond that.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="gu">## Logging
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">-</span> Structured JSON when logging for debugging / observability.
</span></span><span class="line"><span class="cl">- Plain text only for user-facing CLI output.</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>This block fits in under 100 lines and costs about 500 tokens per iteration. Sounds like a lot, but the savings in code quality and absence of rework easily make up for it, especially if you&rsquo;re on a pay-per-token API account. Use it as a base and evolve it with your own experience.</p>
<p>Some items (SRP, small functions, tests) the agent will try to do on its own. Others (DI, strict DRY, explicit types EVERYWHERE, aggressively unique names) it only does when you say so explicitly. And some (like &ldquo;don&rsquo;t strip the comments you wrote yourself on a refactor&rdquo;) are so counterintuitive to its default training that without the instruction it WILL prune them. Hence the importance of having a rules file that gets read every iteration.</p>
<p>An analogous pattern shows up around defensive programming: the agent implements circuit breaker, retry with backoff, aggressive timeout, graceful degradation, all of it nicely — <strong>when you ask</strong>. But on its own it won&rsquo;t propose them. The agent doesn&rsquo;t know what your system&rsquo;s operational failure points are, so it implements the happy path and waits for instruction. If your CLAUDE.md lists the categories of defensive code the project needs (rate limit, retry, breaker, fallback), the agent covers them. If it doesn&rsquo;t list them, the agent doesn&rsquo;t invent them. It&rsquo;s another case where explicit instruction to the agent is what separates robust code from naive code.</p>
<p>If you use the same agent across different projects, it&rsquo;s worth having a base template and adding project-specific rules on top. But start with something like the above and iterate from there.</p>
<h2>The short version<span class="hx:absolute hx:-mt-20" id="the-short-version"></span>
    <a href="#the-short-version" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Uncle Bob wrote Clean Code to be read by other humans. In 2026, the primary reader became the agent. The good news is that most of what the book preached still holds. The bad news is that some things that were opinions (&ldquo;a file should have N lines&rdquo;) became technical constraints (&ldquo;a file with X lines makes the agent perform worse&rdquo;). The difference is that now there&rsquo;s a metric: token cost, tool-call latency, output quality. Whoever writes clean code for the agent saves API-bill money, session time, and gets less hallucination in the output.</p>
<p>And there&rsquo;s a cultural bonus worth registering: all these practices (XP, TDD, SOLID, SRP, DI, small code, abundant tests) were falling out of fashion in the 2010s, replaced by &ldquo;move fast and break things&rdquo; and two-month bootcamps. Programmers who invested in fundamentals became a minority, and it became fashion to trash Uncle Bob on the internet. Turns out those very fundamentals became the technical differentiator of working with agents. Whoever kept the discipline is well served. Whoever dismissed it is now struggling to teach the agent not to commit mistakes the XP crowd had mapped out 25 years ago.</p>
<p>Clean code was never fashion. It became infrastructure.</p>
]]></content:encoded><category>software-engineering</category><category>coding-agents</category></item><item><title>Claude Code's Source Code Leaked. Here's What We Found Inside.</title><link>https://akitaonrails.github.io/en/2026/03/31/claude-code-source-code-leaked-what-we-found-inside/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/03/31/claude-code-source-code-leaked-what-we-found-inside/</guid><pubDate>Tue, 31 Mar 2026 17:00:00 -0300</pubDate><description>&lt;blockquote&gt;
&lt;p&gt;Updated April 2, 2026: if you already read this yesterday, jump straight to the &lt;a href="#update-2026-04-02"&gt;new update section&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This morning (March 31, 2026), security researcher &lt;a href="https://x.com/Fried_rice"target="_blank" rel="noopener"&gt;Chaofan Shou&lt;/a&gt; discovered that the entire source code for Claude Code, Anthropic&amp;rsquo;s official CLI for AI coding, was sitting there for anyone to grab on the public npm registry. 512,000 lines of TypeScript. 1,900 files. All of it exposed through a 59.8 MB source map file accidentally bundled into version 2.1.88 of the &lt;code&gt;@anthropic-ai/claude-code&lt;/code&gt; package.&lt;/p&gt;</description><content:encoded><![CDATA[<blockquote>
  <p>Updated April 2, 2026: if you already read this yesterday, jump straight to the <a href="#update-2026-04-02">new update section</a>.</p>

</blockquote>
<p>This morning (March 31, 2026), security researcher <a href="https://x.com/Fried_rice"target="_blank" rel="noopener">Chaofan Shou</a> discovered that the entire source code for Claude Code, Anthropic&rsquo;s official CLI for AI coding, was sitting there for anyone to grab on the public npm registry. 512,000 lines of TypeScript. 1,900 files. All of it exposed through a 59.8 MB source map file accidentally bundled into version 2.1.88 of the <code>@anthropic-ai/claude-code</code> package.</p>
<p>Within hours the code had been mirrored on GitHub, picked apart by thousands of developers, and Anthropic had put out a statement calling it &ldquo;human error in release packaging, not a security breach.&rdquo; Which is technically true and ignores that the result is the same.</p>
<p><img src="https://raw.githubusercontent.com/kuberwastaken/claude-code/main/public/leak-tweet.png" alt="Tweet announcing the leak"  loading="lazy" /></p>
<p>I use Claude Code every day. Some of the articles you read here, I wrote with it. So I figured I&rsquo;d take a look at what&rsquo;s inside. I actually started writing this piece in Claude Code itself, but my Max plan ran out before I finished. I closed the rest in Codex.</p>
<h2>How the leak happened<span class="hx:absolute hx:-mt-20" id="how-the-leak-happened"></span>
    <a href="#how-the-leak-happened" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Claude Code is bundled with <a href="https://bun.sh/"target="_blank" rel="noopener">Bun</a>, the JavaScript runtime Anthropic acquired in late 2024. When you build with Bun, source maps are generated by default. Those <code>.map</code> files contain the full original source code, not just mappings. Every file, every comment, every internal constant, every system prompt.</p>
<p>The initial theory was that a <a href="https://github.com/oven-sh/bun/issues/28001"target="_blank" rel="noopener">known Bun bug</a> had caused the leak: even with <code>development: false</code>, source maps were still being served and bundled in. But <a href="https://github.com/oven-sh/bun/issues/28001#issuecomment-4164447815"target="_blank" rel="noopener">Jarred Sumner</a>, Bun&rsquo;s creator, shut that down: &ldquo;This has nothing to do with claude code. This is with Bun&rsquo;s frontend development server. Claude Code is not a frontend app. It is a TUI. It doesn&rsquo;t use Bun.serve() to compile a single-file executable.&rdquo; In other words, the Bun bug affects the frontend dev server, not the build process that generated the Claude Code npm package.</p>
<p>What actually happened is simpler: somebody at Anthropic forgot to add <code>*.map</code> to <code>.npmignore</code> or didn&rsquo;t configure the bundler to skip source map generation in production builds. And worse: according to <a href="https://www.theregister.com/2026/03/31/anthropic_claude_code_source_code/"target="_blank" rel="noopener">The Register</a>, the source map didn&rsquo;t just point to the original files, it referenced a ZIP hosted on Anthropic&rsquo;s own Cloudflare R2 bucket. npm happily served it to anyone running <code>npm pack</code>, and the rest was mirroring work.</p>
<p><img src="https://raw.githubusercontent.com/kuberwastaken/claude-code/main/public/claude-files.png" alt="Source files exposed in the npm package"  loading="lazy" /></p>
<p>The irony is that the code contains a whole system called &ldquo;Undercover Mode&rdquo; built specifically to prevent Anthropic&rsquo;s internal information from leaking in commits and PRs. They built a subsystem to stop the AI from revealing internal codenames, and then a source map exposed everything.</p>
<h2>What&rsquo;s inside: the hidden features<span class="hx:absolute hx:-mt-20" id="whats-inside-the-hidden-features"></span>
    <a href="#whats-inside-the-hidden-features" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The source code reveals 44 feature flags covering functionality that&rsquo;s ready but not yet shipped. This isn&rsquo;t vaporware. It&rsquo;s real code hiding behind flags that compile to <code>false</code> in external builds. Let me highlight the most interesting ones.</p>
<h3>KAIROS: a Claude that never stops<span class="hx:absolute hx:-mt-20" id="kairos-a-claude-that-never-stops"></span>
    <a href="#kairos-a-claude-that-never-stops" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Inside the <code>assistant/</code> directory, there&rsquo;s a mode called KAIROS, a persistent assistant that doesn&rsquo;t wait for you to type. It observes, logs, and acts proactively on things it notices. Maintains append-only daily log files, receives <code>&lt;tick&gt;</code> prompts at regular intervals to decide whether to act or stay quiet, and has a 15-second budget: any proactive action that would block the user&rsquo;s workflow for more than 15 seconds gets deferred.</p>
<p>KAIROS-exclusive tools: <code>SendUserFile</code> (sends files to the user), <code>PushNotification</code> (push notifications), <code>SubscribePR</code> (monitors pull requests). None of this exists in the public build.</p>
<h3>BUDDY: a Tamagotchi in the terminal<span class="hx:absolute hx:-mt-20" id="buddy-a-tamagotchi-in-the-terminal"></span>
    <a href="#buddy-a-tamagotchi-in-the-terminal" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I&rsquo;m not making this up. Claude Code has a complete pet companion system in the Tamagotchi style called &ldquo;Buddy.&rdquo; A deterministic gacha system with 18 species, rarity, shiny variants, procedurally generated stats, and a &ldquo;soul&rdquo; written by Claude on the first hatch.</p>
<p>The species is determined by a Mulberry32 PRNG seeded by the hash of the userId. Same user always gets the same buddy. There are 5 stats (DEBUGGING, PATIENCE, CHAOS, WISDOM, SNARK), 6 eye styles, 8 hat options, and sprites rendered as 5-line ASCII art with animations. The code references April 1-7, 2026 as the teaser window, with full launch slated for May 2026.</p>
<h3>ULTRAPLAN: 30 minutes of remote planning<span class="hx:absolute hx:-mt-20" id="ultraplan-30-minutes-of-remote-planning"></span>
    <a href="#ultraplan-30-minutes-of-remote-planning" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>ULTRAPLAN offloads complex planning tasks to a remote session running Opus 4.6, gives it up to 30 minutes to think, and lets you approve the result through the browser. The terminal shows polling every 3 seconds, and once approved, a sentinel value <code>__ULTRAPLAN_TELEPORT_LOCAL__</code> &ldquo;teleports&rdquo; the result back into the local terminal.</p>
<h3>Multi-Agent: &ldquo;Coordinator Mode&rdquo;<span class="hx:absolute hx:-mt-20" id="multi-agent-coordinator-mode"></span>
    <a href="#multi-agent-coordinator-mode" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The multi-agent orchestration system in the <code>coordinator/</code> directory turns Claude Code from a single agent into a coordinator that spawns, directs and manages multiple workers in parallel. Parallel research, synthesis by the coordinator, implementation by workers, verification by workers. The prompt teaches parallelism explicitly and forbids lazy delegation: &ldquo;Do NOT say &lsquo;based on your findings&rsquo; - read the actual findings and specify exactly what to do.&rdquo;</p>
<p>And there&rsquo;s more. The leak also shows in-process teammates with <code>AsyncLocalStorage</code> to isolate context, workers in separate processes via tmux/iTerm2 panes, memory synchronization between agents, and flags ready for <code>BRIDGE_MODE</code>, <code>VOICE_MODE</code>, <code>WORKFLOW_SCRIPTS</code>, <code>AFK mode</code>, <code>advisor-tool</code> and <code>history snipping</code>. None of that guarantees a launch, but it suggests a roadmap that&rsquo;s a lot further along than the public version lets on.</p>
<h2>The memory architecture<span class="hx:absolute hx:-mt-20" id="the-memory-architecture"></span>
    <a href="#the-memory-architecture" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The memory system caught my eye. It&rsquo;s not a &ldquo;store everything and retrieve&rdquo; approach. It&rsquo;s a three-layer architecture:</p>
<p><a href="https://x.com/himanshustwts/status/2038924027411222533"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.amazonaws.com/2026/03/31/claude-code-memory-architecture.jpg" alt="Summary of Claude Code’s memory architecture"  loading="lazy" /></a></p>
<p><code>MEMORY.md</code> is a lightweight pointer index (~150 characters per line) that stays permanently loaded in the context. It doesn&rsquo;t store data, it stores locations. The actual knowledge lives distributed across &ldquo;topic files&rdquo; fetched on demand. Raw transcripts are never reloaded into the context whole, only searched with grep for specific identifiers.</p>
<p>And it comes with an important discipline: the system writes to the topic file first and only then updates the index. <code>MEMORY.md</code> doesn&rsquo;t become a fact dump. It stays a map. If you let the index turn into storage, it pollutes the permanent context and degrades the whole system.</p>
<p>The &ldquo;Dream&rdquo; system (<code>services/autoDream/</code>) is a memory consolidation engine that runs as a background subagent. The name is intentional. It&rsquo;s Claude dreaming.</p>
<p>The dream has a three-gate trigger: 24 hours since the last dream, at least 5 sessions since the last dream, and acquiring a consolidation lock (preventing concurrent dreams). All three have to pass.</p>
<p>When it runs, it follows four phases: Orient (ls in the memory directory, read the index), Gather (look for new signals in logs, stale memories, transcripts), Consolidate (write or update topic files, convert relative dates to absolute, delete contradicted facts), and Prune (keep the index under 200 lines and ~25KB).</p>
<p>There are four memory types: <code>user</code> (user profile), <code>feedback</code> (corrections and confirmations), <code>project</code> (context about ongoing work), <code>reference</code> (pointers to external systems). The taxonomy explicitly excludes things derivable from the code (patterns, architecture, git history, file structure).</p>
<p>The dream subagent gets read-only bash. It can look at the project but can&rsquo;t modify anything. It&rsquo;s purely a consolidation pass.</p>
<p>And there&rsquo;s another detail I found elegant: memory isn&rsquo;t treated as truth. It&rsquo;s treated as a hint. The system assumes that memory may be stale, wrong or contradicted, so the model still has to verify before trusting it. That&rsquo;s the opposite of the fantasy of &ldquo;throw everything in a vector database and let the magic happen.&rdquo;</p>
<h2>&ldquo;Undercover Mode&rdquo;<span class="hx:absolute hx:-mt-20" id="undercover-mode"></span>
    <a href="#undercover-mode" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Anthropic employees (identified by <code>USER_TYPE === 'ant'</code>) use Claude Code on public and open source repositories. Undercover Mode (<code>utils/undercover.ts</code>) prevents the AI from accidentally revealing internal information in commits and PRs.</p>
<p>When active, it injects into the system prompt:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>## UNDERCOVER MODE - CRITICAL

You are operating UNDERCOVER in a PUBLIC/OPEN-SOURCE repository. Your commit
messages, PR titles, and PR bodies MUST NOT contain ANY Anthropic-internal
information. Do not blow your cover.

NEVER include in commit messages or PR descriptions:
- Internal model codenames (animal names like Capybara, Tengu, etc.)
- Unreleased model version numbers (e.g., opus-4-7, sonnet-4-8)
- Internal repo or project names
- Internal tooling, Slack channels, or short links
- The phrase &#34;Claude Code&#34; or any mention that you are an AI
- Co-Authored-By lines or any other attribution</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>There&rsquo;s no way to turn it off. If the system isn&rsquo;t sure it&rsquo;s in an internal repository, it stays in undercover mode. That confirms something kind of uncomfortable: Anthropic uses Claude Code to contribute to open source, and the agent is instructed to hide that it&rsquo;s an AI.</p>
<p>The internal codenames are animal names: Tengu (codename for the Claude Code project), Fennec (Opus), Capybara, Numbat (in testing). &ldquo;Fast Mode&rdquo; is internally called &ldquo;Penguin Mode&rdquo; with endpoint <code>claude_code_penguin_mode</code> and kill-switch <code>tengu_penguins_off</code>.</p>
<h2>The most paranoid parts<span class="hx:absolute hx:-mt-20" id="the-most-paranoid-parts"></span>
    <a href="#the-most-paranoid-parts" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>There&rsquo;s a piece of the analysis I almost missed because I was looking more at the hidden features. But maybe the most revealing thing about Anthropic&rsquo;s mindset is in the defense mechanisms against copying and abuse.</p>
<p>According to <a href="https://alex000kim.com/posts/2026-03-31-claude-code-source-leak/"target="_blank" rel="noopener">Alex Kim&rsquo;s analysis</a>, there&rsquo;s an anti-distillation mode that can ask the server to inject fake tools into the system prompt. The idea is to poison traffic recorded by anyone trying to distill Claude Code&rsquo;s behavior to train a competitor. There&rsquo;s also a second mechanism for summarizing connector text, cryptographically signed, so that part of the observable traffic doesn&rsquo;t match the original raw reasoning. It&rsquo;s not perfect protection. It&rsquo;s another layer of friction. But it shows the company is explicitly thinking about copy-by-observation, not just traditional security.</p>
<p>And there&rsquo;s the more aggressive part: client attestation. Every request includes a billing header with a placeholder <code>cch=00000</code>, and the Bun native runtime replaces that with a hash computed below the JavaScript layer. In other words, looking like Claude Code isn&rsquo;t enough. The binary tries to prove that it is Claude Code. That helps explain why the fight with third-party tools like OpenCode got so sensitive: it wasn&rsquo;t just a commercial or legal issue. There was technical enforcement built into the transport.</p>
<h3>Update: the DRM died in less than 24 hours<span class="hx:absolute hx:-mt-20" id="update-the-drm-died-in-less-than-24-hours"></span>
    <a href="#update-the-drm-died-in-less-than-24-hours" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Remember when I mentioned client attestation as &ldquo;the most aggressive part&rdquo;? Yeah. It lasted less than a day.</p>
<p>For context: Anthropic had been waging a war against third-party tools since January 2026. First came the server-side blocking of OAuth tokens from non-official clients. Then in March, the <a href="https://github.com/anomalyco/opencode"target="_blank" rel="noopener">OpenCode</a> maintainer merged a <a href="https://github.com/anomalyco/opencode/issues/7456"target="_blank" rel="noopener">PR</a> removing all Claude authentication from the project. The commit message was two words: &ldquo;anthropic legal requests.&rdquo; <a href="https://www.theregister.com/2026/02/20/anthropic_clarifies_ban_third_party_claude_access/"target="_blank" rel="noopener">The Register reported</a> that Anthropic updated its terms of service to make it explicit that OAuth tokens from Pro/Max subscriptions can only be used in the official Claude Code and on Claude.ai. People paying $100-200/month for Max who wanted to use the tool of their choice were left holding the bag.</p>
<p>The technical mechanism behind the block was the <code>cch=</code> header. With the leaked code you can see the system has two parts. The first is a version suffix: the <code>cc_version</code> field includes 3 hex characters derived from the user&rsquo;s first message via SHA-256, using a 12-character salt embedded in the JavaScript. The second is the body hash itself: the entire body of the request (messages, tools, metadata, model, thinking config, everything) is serialized as compact JSON with the <code>cch=00000</code> placeholder, then hashed with <a href="https://github.com/Cyan4973/xxHash"target="_blank" rel="noopener">xxHash64</a> using a fixed seed. The result is masked with <code>0xFFFFF</code> (20 bits) and formatted as 5 lowercase hex characters. The placeholder is replaced with the computed hash before the request leaves the process.</p>
<p>The detail that makes the difference: that substitution happens inside the Bun native runtime, written in Zig, below the JavaScript layer. Bun literally mutates the JavaScript string in-place, overwriting the <code>00000</code> bytes in the string buffer with the computed hash. If you ran the same bundle in Node or in a stock Bun, the placeholder would go to the server as-is and the request would be rejected.</p>
<p>And then the leak happened. With the source code exposed, <a href="https://x.com/StraughterG/status/2039344027556798476"target="_blank" rel="noopener">@StraughterG</a> (Jay Guthrie) announced that same night: &ldquo;Yesterday I said Anthropic&rsquo;s compiled Zig cch= hash was banning 3rd-party Claude clients. Tonight, the DRM is dead. We extracted the algorithm from the binary. It&rsquo;s not advanced cryptography. It&rsquo;s a static xxHash64 seed.&rdquo;</p>
<p>The seed is <code>0x6E52736AC806831E</code>. The <a href="https://a10k.co/b/reverse-engineering-claude-code-cch.html"target="_blank" rel="noopener">full algorithm</a>, as he explained in a thread of tweets, fits in a few lines of TypeScript:</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-typescript" data-lang="typescript"><span class="line"><span class="cl"><span class="kr">import</span> <span class="nx">xxhash</span> <span class="kr">from</span> <span class="s2">&#34;xxhash-wasm&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="p">{</span> <span class="nx">h64Raw</span> <span class="p">}</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">xxhash</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">body</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">request</span><span class="p">);</span> <span class="c1">// com cch=00000 no placeholder
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">hash</span> <span class="o">=</span> <span class="nx">h64Raw</span><span class="p">(</span><span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">().</span><span class="nx">encode</span><span class="p">(</span><span class="nx">body</span><span class="p">),</span> <span class="mh">0x6E52736AC</span><span class="nx">n</span> <span class="o">|</span> <span class="p">(</span><span class="mh">0x806831E</span><span class="nx">n</span> <span class="o">&lt;&lt;</span> <span class="mi">32</span><span class="nx">n</span><span class="p">));</span>
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">cch</span> <span class="o">=</span> <span class="p">(</span><span class="nx">hash</span> <span class="o">&amp;</span> <span class="mh">0xFFFFF</span><span class="nx">n</span><span class="p">).</span><span class="nx">toString</span><span class="p">(</span><span class="mi">16</span><span class="p">).</span><span class="nx">padStart</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="s2">&#34;0&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="c1">// substituir cch=00000 por cch={valor calculado}
</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 href="https://x.com/paoloanzn/status/2039348588741087341"target="_blank" rel="noopener">@paoloanzn</a> celebrated: &ldquo;we cracked it. the cch= signing system in claude code is fully reverse engineered.&rdquo; And he immediately put the bypass in <a href="https://github.com/paoloanzn/free-code"target="_blank" rel="noopener">free-code</a>, a fork of Claude Code with telemetry removed, system prompt guardrails stripped, and all 54 experimental feature flags unlocked.</p>
<p><a href="https://github.com/paoloanzn/free-code"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/2026/04/01/free-code-screenshot.png" alt="Screenshot of free-code running with experimental features"  loading="lazy" /></a></p>
<p>The technical point that matters: xxHash64 is not cryptography. It&rsquo;s a checksum hash designed for speed, not security. The seed is static, embedded in the binary. It changes with each version of Claude Code, but within a version it&rsquo;s the same for everyone. The &ldquo;security&rdquo; depended entirely on nobody being able to extract the seed from the compiled Zig binary. With the source code leaked, that obscurity evaporated in hours.</p>
<p>Now any third-party client — OpenCode, Claw-Code, whatever — can intercept the <code>fetch()</code>, hash the body with the correct seed, and pass the server&rsquo;s validation as if it were the official Claude Code. The barrier Anthropic built to protect its $2.5 billion ARR business model was, in the end, security by obscurity over a non-cryptographic hash.</p>
<p>The third detail is small but says a lot about real product in production: the system detects user frustration with regex. Yes, regex. Profanity, insults, &ldquo;this sucks,&rdquo; that kind of thing. It&rsquo;s funny to see an LLM company doing sentiment analysis with <code>wtf|ffs|shit</code>, but it&rsquo;s also the kind of pragmatic solution you reach for when you need a cheap immediate answer, not conceptual elegance.</p>
<h2>What the code reveals about how you use Claude Code<span class="hx:absolute hx:-mt-20" id="what-the-code-reveals-about-how-you-use-claude-code"></span>
    <a href="#what-the-code-reveals-about-how-you-use-claude-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://x.com/iamfakeguru/status/2038965567269249484"target="_blank" rel="noopener">@iamfakeguru</a> compiled a thread with seven technical findings from the code that any user should know:</p>
<p>Claude Code has a 2,000-line cap per file read. When you ask it to read a larger file, it silently truncates. Tool results are cut off at 50,000 characters. The context window compression system drops old messages to fit more new context. And there&rsquo;s a difference between the access level of Anthropic employees (<code>USER_TYPE === 'ant'</code>) and public access: internal tools like <code>ConfigTool</code> and <code>TungstenTool</code> are invisible in the external build.</p>
<p>The most useful finding from the thread is how Anthropic employees work around the limitations external users face. The code reveals that <code>USER_TYPE === 'ant'</code> unlocks internal tools, exclusive beta headers (<code>cli-internal-2026-02-09</code>), access to staging (<code>claude-ai.staging.ant.dev</code>), and a <code>ConfigTool</code> that lets you change configurations at runtime. External builds compile all of this to <code>false</code> via dead code elimination.</p>
<p>But the point that matters is: the CLAUDE.md you put at the root of your project gets read in full by Claude Code and injected into the system prompt. It&rsquo;s literally the place where you control how the agent behaves. <a href="https://x.com/iamfakeguru/status/2038965567269249484"target="_blank" rel="noopener">@iamfakeguru</a> published a complete override with 10 mechanical rules, and then put the whole file in a separate repository: <a href="https://github.com/iamfakeguru/claude-md"target="_blank" rel="noopener">iamfakeguru/claude-md</a>.</p>
<p><a href="https://github.com/iamfakeguru/claude-md/blob/main/CLAUDE.md"target="_blank" rel="noopener"><img src="https://new-uploads-akitaonrails.s3.amazonaws.com/2026/03/31/claude-md-production-grade-agent-directives.png" alt="Screenshot of the CLAUDE.md published by fakeguru"  loading="lazy" /></a></p>
<p>I&rsquo;m not going to paste the whole block here. What matters is the content: it forces post-edit verification (<code>tsc</code> and <code>eslint</code> before declaring success), it imposes re-reading files before editing, it requires reading large files in chunks, it assumes silent truncation of long results, and it tells you to break larger work into phases or parallel subagents. In other words: it turns into explicit rules everything that external users were having to figure out empirically.</p>
<p>These aren&rsquo;t magic instructions. They&rsquo;re guardrails. The difference is that now we know which limits the system actually has and we can write a CLAUDE.md that works with them, not against them.</p>
<h2>Cache bugs that cost real money<span class="hx:absolute hx:-mt-20" id="cache-bugs-that-cost-real-money"></span>
    <a href="#cache-bugs-that-cost-real-money" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><a href="https://x.com/altryne/status/2038676458026189225"target="_blank" rel="noopener">@altryne</a> (Alex Volkov) reported cache invalidation bugs that make uncached tokens cost 10-20x more than cached ones. There are two bugs: a string substitution bug in Bun that affects the standalone CLI (workaround: use <code>npx @anthropic-ai/claude-code</code> instead of the installed binary), and another in the <code>--resume</code> flag that breaks the cache with no known workaround. Over 500 users reported similar quota exhaustion issues. If you&rsquo;ve been feeling like Claude Code was burning tokens faster than expected over the past few days, it probably wasn&rsquo;t your imagination.</p>
<h2>&ldquo;Staff engineer spaghetti&rdquo;<span class="hx:absolute hx:-mt-20" id="staff-engineer-spaghetti"></span>
    <a href="#staff-engineer-spaghetti" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The code analysis revealed real problems. A comment in the source itself admits: &ldquo;1,279 sessions had 50+ consecutive failures (up to 3,272) in a single session, wasting ~250K API calls per day globally.&rdquo; The fix was three lines: limit consecutive failures to three before disabling compaction.</p>
<p>The <code>print.ts</code> file has 5,594 lines with a single 3,167-line function containing twelve levels of nesting. <code>main.tsx</code> is 803,924 bytes in a single file. <code>interactiveHelpers.tsx</code> is 57,424 bytes. These are files no human can review with confidence.</p>
<p>The most viral reaction came from <a href="https://x.com/thekitze/status/2038956521942577557"target="_blank" rel="noopener">@thekitze</a>: he asked GPT-5.4 to evaluate the codebase and the score came in at 6.5/10. The description: &ldquo;This is not junior spaghetti. This is staff-engineer spaghetti: performance-aware, feature-flagged, telemetry-instrumented, surgically optimized spaghetti.&rdquo; In other words, it isn&rsquo;t bad code from inexperience. It&rsquo;s bad code from pressure to ship fast without paying the cost of cleaning up afterwards.</p>
<p><a href="https://x.com/thekitze/status/2038986445839622405"target="_blank" rel="noopener">@thekitze</a> also elaborated in another thread on how the code shows a lack of basic engineering practices. And this is where I feel vindicated.</p>
<p>I&rsquo;ve been repeating in several posts on <a href="/en/en/tags/vibe-coding/">vibe coding</a> that speed without discipline produces exactly this. The principles I defend, small increments, tests at every step, review before committing, continuous refactoring, CI that rejects high cyclomatic complexity, are the same Extreme Programming principles that have worked since the early 2000s. Anthropic apparently didn&rsquo;t follow any of them on their own product.</p>
<p>A 3,167-line function with 12 levels of nesting isn&rsquo;t something that appears overnight. It&rsquo;s accumulation. It&rsquo;s the result of dozens of additions where nobody stopped to refactor because &ldquo;it works, don&rsquo;t touch it.&rdquo; It&rsquo;s the classic anti-pattern of vibe coding without discipline: generate code with AI, see that it compiles, commit, repeat. Without rigorous review. Without complexity limits in CI. Without the basic rule that if a function passes 50 lines, it needs to be broken up.</p>
<p>The irony is that Anthropic sells the most popular vibe coding tool on the market and doesn&rsquo;t practice what I call responsible vibe coding. Claude Code is worth $2.5 billion in ARR. The code that generates that revenue is rated 6.5/10.</p>
<h2>The &ldquo;clean room&rdquo; question<span class="hx:absolute hx:-mt-20" id="the-clean-room-question"></span>
    <a href="#the-clean-room-question" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>With the entire source code public, there&rsquo;s a serious legal and competitive implication. And here I think a lot of people started using the term &ldquo;clean room&rdquo; with a lightness that doesn&rsquo;t fit the subject.</p>
<p>A real clean room isn&rsquo;t just &ldquo;I rewrote it in another language&rdquo; or &ldquo;I didn&rsquo;t copy and paste.&rdquo; The classic model is much more annoying: one group studies the original and produces a functional spec; another group, isolated, implements from that spec without ever seeing the original code. The whole point is to reduce contamination risk.</p>
<p><a href="https://x.com/braelyn_ai/status/2039025584626397491"target="_blank" rel="noopener">@braelyn_ai</a> raised another interesting point: with generative tools, somebody could try a &ldquo;clean room rebuild&rdquo; using tests, observable behavior and documentation, without reusing the original implementation. In theory, that makes sense. In practice, what shows up in the heat of a leak usually lands in a much grayer zone.</p>
<p>The <a href="https://github.com/ultraworkers/claw-code"target="_blank" rel="noopener">Claw-Code</a> case illustrates this well. The project presents itself as an independent rewrite and has already shifted focus to Python and Rust, but the README itself admits direct study of the exposed code and even mentions a parity audit against a local archive. So I wouldn&rsquo;t call that a clean room in the strictest classic sense. I&rsquo;d call it an inspired reimplementation, with a deliberate attempt to move away from the leaked snapshot.</p>
<p>That doesn&rsquo;t mean every reimplementation is doomed. Software copyright doesn&rsquo;t protect abstract ideas, generic tool flow, high-level architecture or &ldquo;a CLI that does X.&rdquo; It protects concrete expression. But that&rsquo;s exactly why discipline matters. The more a project wants to maintain independence, the less it should rely on the leaked material as a direct benchmark.</p>
<p>There&rsquo;s a more pragmatic detail in there: the literal copies of the leaked source will probably disappear quickly when the first DMCAs start arriving. Mirrors fall easily. That&rsquo;s why a reimplementation matters more than a raw mirror. It doesn&rsquo;t erase the legal discussion, but it changes the type of fight quite a bit and the chances of staying online.</p>
<p>That&rsquo;s more or less what I did myself when I <a href="/en/2026/03/16/rewrote-openclaw-in-rust-frankclaw/">rewrote OpenClaw in Rust</a>. The point wasn&rsquo;t to copy line by line. It was to understand the behavior and rewrite the whole piece in my own code.</p>
<p>The satirical site <a href="https://malus.sh/"target="_blank" rel="noopener">malus.sh</a> appeared today offering &ldquo;Clean Room as a Service&rdquo; with the tagline &ldquo;Robot-Reconstructed, Zero Attribution.&rdquo; The joke: AI robots recreate open source projects eliminating attribution obligations, with guarantees like &ldquo;This has never happened because it legally cannot happen. Trust us.&rdquo; and indemnification via an offshore subsidiary in a jurisdiction that doesn&rsquo;t recognize software copyright. It&rsquo;s satire, but it&rsquo;s satire that describes what someone is going to actually try to do.</p>
<p><a id="update-2026-04-02"></a></p>
<h2>Update on April 2, 2026<span class="hx:absolute hx:-mt-20" id="update-on-april-2-2026"></span>
    <a href="#update-on-april-2-2026" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Since the text above opens in the heat of March 31, it&rsquo;s worth recording what happened right after. I decided to add this update after reading <a href="https://x.com/k1rallik/status/2039686500619534818"target="_blank" rel="noopener">this tweet from @k1rallik</a>, which captures the post-leak mood well but mixes verifiable facts with a touch too much epic.</p>
<p>First: the DMCA part got messier than it looked. The notice itself published in the <code>github/dmca</code> repository says GitHub processed the takedown against the entire network of <strong>8,100 repositories</strong>, because the notification claimed that &ldquo;all or most of the forks&rdquo; were infringing to the same extent as the main repository. The next day, Anthropic published a <strong>partial retraction</strong>: it asked for the reinstatement of all the removed repositories, except <code>nirholas/claude-code</code> and <strong>96 forks listed individually</strong>. So the thesis that the initial attempt was too broad is correct. The final picture, however, isn&rsquo;t &ldquo;8,100 repositories were taken down.&rdquo; What happened was a formal walk-back after the mass removal.</p>
<p>Second: the <a href="https://github.com/ultraworkers/claw-code"target="_blank" rel="noopener">Claw-Code</a> project really did blow up. By the time I was updating this post, GitHub was already showing <strong>142,829 stars</strong> and <strong>101,510 forks</strong>. That alone is enough to say the story has moved out of the &ldquo;curious fork of the leak&rdquo; category and into the &ldquo;real competitive side effect&rdquo; category. The viral tweet that went around today is right about the size of the damage but exaggerates some details. The project&rsquo;s own README describes itself as &ldquo;the fastest repo in history to surpass 50K stars&rdquo; and says the milestone came in two hours. I couldn&rsquo;t independently confirm that historical record, so I&rsquo;d rather treat that as the project&rsquo;s own claim, not as a settled fact.</p>
<p>Third: the Rust part also needs nuance. Yes, there&rsquo;s already a Rust workspace on the main branch and the <code>Cargo.toml</code> is at version <code>0.1.0</code>. But I couldn&rsquo;t find a public release on GitHub to support the line &ldquo;release 0.1.0 already shipped&rdquo; as a formal launch. What I can say with confidence is something else: the project already has a Python base, already has a Rust workspace, and has already drawn enough attention to keep existing even without the literal mirror of the leaked code.</p>
<h2>What Anthropic should have done<span class="hx:absolute hx:-mt-20" id="what-anthropic-should-have-done"></span>
    <a href="#what-anthropic-should-have-done" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Anthropic responded fast. They pulled the compromised package, put out a public statement, and cleaned up what they could. But the damage was done. The code was mirrored before the takedown. Mirrors on GitHub, analyses in blogs, threads on X/Twitter. There&rsquo;s no way to un-publish something on the internet.</p>
<p>What bothers me isn&rsquo;t the leak itself. Bugs happen. What bothers me is that this was avoidable with basic engineering practices:</p>
<ol>
<li>Add <code>*.map</code> to <code>.npmignore</code>. One line.</li>
<li>Configure the bundler to not generate source maps in production builds. One flag.</li>
<li>Have a CI check that rejects publication if the package contains <code>.map</code>. A 5-line script.</li>
<li>Have a release pipeline with manual review before publishing to npm. Process, not code.</li>
</ol>
<p>None of these are hard. They&rsquo;re all the kind of thing that gets dropped when you&rsquo;re moving too fast and don&rsquo;t have discipline in the release process. It&rsquo;s exactly what I preach as <a href="/en/en/tags/vibe-coding/">disciplined vibe coding</a>: moving fast doesn&rsquo;t mean skipping the guardrails.</p>
<p>And the second failure: the quality of the code itself. 512,000 lines with 3,000-line functions and 12 levels of nesting isn&rsquo;t engineering. It&rsquo;s accumulation. It&rsquo;s what happens when you generate code with AI without rigorous review, without continuous refactoring, without CI that rejects high cyclomatic complexity. The irony of being precisely the company that sells the most popular vibe coding tool in the world doesn&rsquo;t go unnoticed.</p>
<h2>Sources<span class="hx:absolute hx:-mt-20" id="sources"></span>
    <a href="#sources" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><ul>
<li><a href="https://github.com/Kuberwastaken/claude-code"target="_blank" rel="noopener">Kuberwastaken/claude-code - Complete breakdown of the leaked code</a></li>
<li><a href="https://alex000kim.com/posts/2026-03-31-claude-code-source-leak/"target="_blank" rel="noopener">Alex Kim - Claude Code Source Leak: fake tools, frustration regexes, undercover mode</a></li>
<li><a href="https://venturebeat.com/technology/claude-codes-source-code-appears-to-have-leaked-heres-what-we-know/"target="_blank" rel="noopener">VentureBeat - Claude Code&rsquo;s source code appears to have leaked</a></li>
<li><a href="https://www.theregister.com/2026/03/31/anthropic_claude_code_source_code/"target="_blank" rel="noopener">The Register - Anthropic accidentally exposes Claude Code source code</a></li>
<li><a href="https://fortune.com/2026/03/31/anthropic-source-code-claude-code-data-leak-second-security-lapse-days-after-accidentally-revealing-mythos/"target="_blank" rel="noopener">Fortune - Anthropic leaks its own AI coding tool&rsquo;s source code</a></li>
<li><a href="https://cybernews.com/security/anthropic-claude-code-source-leak/"target="_blank" rel="noopener">Cybernews - Full source code for Anthropic&rsquo;s Claude Code leaks</a></li>
<li><a href="https://gizmodo.com/source-code-for-anthropics-claude-code-leaks-at-the-exact-wrong-time-2000740379"target="_blank" rel="noopener">Gizmodo - Source Code for Anthropic&rsquo;s Claude Code Leaks at the Exact Wrong Time</a></li>
<li><a href="https://www.anthropic.com/news/anthropic-acquires-bun-as-claude-code-reaches-usd1b-milestone?s=33"target="_blank" rel="noopener">Anthropic - Anthropic acquires Bun as Claude Code reaches $1B milestone</a></li>
<li><a href="https://github.com/oven-sh/bun/issues/28001"target="_blank" rel="noopener">Bun Issue #28001 - Source maps incorrectly served in production</a></li>
<li><a href="https://news.ycombinator.com/item?id=43909409"target="_blank" rel="noopener">Hacker News - Claude&rsquo;s system prompt is over 24k tokens with tools</a></li>
<li><a href="https://malus.sh/"target="_blank" rel="noopener">malus.sh - Clean Room as a Service (satire)</a></li>
<li><a href="https://x.com/iamfakeguru/status/2038965567269249484"target="_blank" rel="noopener">@iamfakeguru - Thread with 7 technical findings from the code</a></li>
<li><a href="https://x.com/altryne/status/2038676458026189225"target="_blank" rel="noopener">@altryne - Cache bugs that cost 10-20x more</a></li>
<li><a href="https://x.com/thekitze/status/2038956521942577557"target="_blank" rel="noopener">@thekitze - &ldquo;Staff-engineer spaghetti&rdquo; 6.5/10</a></li>
<li><a href="https://x.com/braelyn_ai/status/2039025584626397491"target="_blank" rel="noopener">@braelyn_ai - Clean room and legal implications</a></li>
<li><a href="https://github.com/github/dmca/blob/master/2026/03/2026-03-31-anthropic.md"target="_blank" rel="noopener">GitHub DMCA - Anthropic takedown notice processed against the network of 8.1K repositories</a></li>
<li><a href="https://github.com/github/dmca/blob/master/2026/04/2026-04-01-anthropic-retraction.md"target="_blank" rel="noopener">GitHub DMCA - Anthropic&rsquo;s partial retraction the next day</a></li>
<li><a href="https://github.com/ultraworkers/claw-code"target="_blank" rel="noopener">ultraworkers/claw-code - Python and Rust reimplementation that became the main post-leak project</a></li>
<li><a href="https://x.com/mem0ai/status/2039041449854124229"target="_blank" rel="noopener">@mem0ai - Analysis of the memory architecture</a></li>
<li><a href="https://x.com/himanshustwts/status/2038924027411222533"target="_blank" rel="noopener">@himanshustwts - Memory architecture summary</a></li>
<li><a href="https://github.com/iamfakeguru/claude-md"target="_blank" rel="noopener">iamfakeguru/claude-md - Override published with the complete CLAUDE.md</a></li>
<li><a href="https://x.com/StraughterG/status/2039344027556798476"target="_blank" rel="noopener">@StraughterG - &ldquo;the DRM is dead&rdquo; - reverse engineering of the cch= hash</a></li>
<li><a href="https://x.com/StraughterG/status/2039344035555344550"target="_blank" rel="noopener">@StraughterG - xxHash64 seed and TypeScript bypass code</a></li>
<li><a href="https://x.com/paoloanzn/status/2039348588741087341"target="_blank" rel="noopener">@paoloanzn - &ldquo;we cracked it&rdquo; - confirmation of the reverse engineering</a></li>
<li><a href="https://github.com/paoloanzn/free-code"target="_blank" rel="noopener">paoloanzn/free-code - Fork of Claude Code with telemetry removed and features unlocked</a></li>
<li><a href="https://a10k.co/b/reverse-engineering-claude-code-cch.html"target="_blank" rel="noopener">a10k.co - What&rsquo;s cch? Reverse Engineering Claude Code&rsquo;s Request Signing</a></li>
<li><a href="https://www.theregister.com/2026/02/20/anthropic_clarifies_ban_third_party_claude_access/"target="_blank" rel="noopener">The Register - Anthropic clarifies ban on third-party tool access to Claude</a></li>
</ul>
]]></content:encoded><category>coding-agents</category><category>security</category><category>software-engineering</category></item><item><title>Software Is Never 'Done' — 4 Projects, Life After Deploy, and Why One-Shot Prompting Is a Myth</title><link>https://akitaonrails.github.io/en/2026/03/01/software-is-never-done-4-projects-life-after-deploy-one-shot-prompt-myth/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/03/01/software-is-never-done-4-projects-life-after-deploy-one-shot-prompt-myth/</guid><pubDate>Sun, 01 Mar 2026 12:00:00 -0300</pubDate><description>&lt;p&gt;And don&amp;rsquo;t forget to subscribe to my newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;I published the &amp;ldquo;final post&amp;rdquo; of the &lt;a href="https://akitaonrails.github.io/en/2026/02/20/zero-to-post-production-in-1-week-using-ai-on-real-projects-behind-the-m-akita-chronicles/"&gt;Behind The M.Akita Chronicles&lt;/a&gt; series 10 days ago. 274 commits, 1,323 tests, deployed to production. I wrote down the lessons, did the conclusion, dropped the quote at the end. Done.&lt;/p&gt;
&lt;p&gt;125 post-production commits later, I can confirm: &lt;strong&gt;software &lt;em&gt;&amp;ldquo;done&amp;rdquo;&lt;/em&gt; doesn&amp;rsquo;t exist.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Today the M.Akita Chronicles repo has 335 commits and 1,422 tests. Tomorrow, Monday, subscribers get the 3rd consecutive newsletter — generated, reviewed, and sent by a system that won&amp;rsquo;t stop evolving. Meanwhile, &lt;a href="https://akitaonrails.github.io/en/2026/02/23/vibe-code-built-a-smart-image-indexer-with-ai-in-2-days-frank-sherlock/"&gt;Frank Sherlock&lt;/a&gt; went from 50 commits and v0.1 to 103 commits and v0.7 with face detection. &lt;a href="https://akitaonrails.github.io/en/2026/02/01/frankmd-markdown-editor-vibe-code-part-1/"&gt;FrankMD&lt;/a&gt; got 3 external contributors and shipped v0.2.0. And even &lt;a href="https://akitaonrails.github.io/en/2026/02/21/vibe-code-built-a-mega-clone-in-rails-in-1-day-frankmega/"&gt;FrankMega&lt;/a&gt; — a project built in 1 day — needed fixes when real users showed up.&lt;/p&gt;</description><content:encoded><![CDATA[<p>And don&rsquo;t forget to subscribe to my newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>!</p>
<p>&ndash;</p>
<p>I published the &ldquo;final post&rdquo; of the <a href="/en/2026/02/20/zero-to-post-production-in-1-week-using-ai-on-real-projects-behind-the-m-akita-chronicles/">Behind The M.Akita Chronicles</a> series 10 days ago. 274 commits, 1,323 tests, deployed to production. I wrote down the lessons, did the conclusion, dropped the quote at the end. Done.</p>
<p>125 post-production commits later, I can confirm: <strong>software <em>&ldquo;done&rdquo;</em> doesn&rsquo;t exist.</strong></p>
<p>Today the M.Akita Chronicles repo has 335 commits and 1,422 tests. Tomorrow, Monday, subscribers get the 3rd consecutive newsletter — generated, reviewed, and sent by a system that won&rsquo;t stop evolving. Meanwhile, <a href="/en/2026/02/23/vibe-code-built-a-smart-image-indexer-with-ai-in-2-days-frank-sherlock/">Frank Sherlock</a> went from 50 commits and v0.1 to 103 commits and v0.7 with face detection. <a href="/en/2026/02/01/frankmd-markdown-editor-vibe-code-part-1/">FrankMD</a> got 3 external contributors and shipped v0.2.0. And even <a href="/en/2026/02/21/vibe-code-built-a-mega-clone-in-rails-in-1-day-frankmega/">FrankMega</a> — a project built in 1 day — needed fixes when real users showed up.</p>
<p>Throughout February, I published more than a dozen posts detailing the build of each project. This post is different. It isn&rsquo;t about building from scratch, I covered that already. It&rsquo;s about what happens <strong>after</strong>. And what happens after destroys the one-shot prompt narrative. Software needs an experienced human at the wheel. Iterative development is the only thing that works. Anyone who disagrees hasn&rsquo;t put a system in production yet.</p>
<p>I&rsquo;ll prove it with <code>git log</code>.</p>
<h2>Life After Deploy<span class="hx:absolute hx:-mt-20" id="life-after-deploy"></span>
    <a href="#life-after-deploy" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><table>
  <thead>
      <tr>
          <th>Project</th>
          <th>Post-publication commits</th>
          <th>Highlight</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>The M.Akita Chronicles</td>
          <td>56</td>
          <td>3rd week in production, new features, 13 bug fixes, prompt tuning</td>
      </tr>
      <tr>
          <td>Frank Sherlock</td>
          <td>53</td>
          <td>From v0.3 to v0.7: video support, face detection, AUR publish</td>
      </tr>
      <tr>
          <td>FrankMD</td>
          <td>14</td>
          <td>3 external contributors, 4 PRs merged, v0.2.0</td>
      </tr>
      <tr>
          <td>FrankMega</td>
          <td>2</td>
          <td>MIME types nobody saw coming</td>
      </tr>
      <tr>
          <td><strong>Total</strong></td>
          <td><strong>125</strong></td>
          <td></td>
      </tr>
  </tbody>
</table>
<p>Let&rsquo;s get into the details.</p>
<h2>The M.Akita Chronicles: 3 Weeks in Production<span class="hx:absolute hx:-mt-20" id="the-makita-chronicles-3-weeks-in-production"></span>
    <a href="#the-makita-chronicles-3-weeks-in-production" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The newsletter system has been live since February 16. It&rsquo;s already sent 2 newsletters and tomorrow it sends the third. 56 commits have happened since the &ldquo;final post&rdquo;:</p>
<table>
  <thead>
      <tr>
          <th>Date</th>
          <th>Commits</th>
          <th>What happened</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Feb 20 (Thu)</td>
          <td>16</td>
          <td>Steam Gaming (whole new section), bug fixes in paywall detection</td>
      </tr>
      <tr>
          <td>Feb 21 (Fri)</td>
          <td>12</td>
          <td>/preview command, YouTube collage, /rerun notifications</td>
      </tr>
      <tr>
          <td>Feb 22 (Sat)</td>
          <td>1</td>
          <td>Fix: /preflight sending the result to the wrong channel</td>
      </tr>
      <tr>
          <td>Feb 23 (Sun)</td>
          <td>10</td>
          <td>2nd newsletter publication, Gmail clipping warning, TTS revert</td>
      </tr>
      <tr>
          <td>Feb 24 (Mon)</td>
          <td>1</td>
          <td>Podcast prompt tweak</td>
      </tr>
      <tr>
          <td>Feb 28 (Sat)</td>
          <td>7</td>
          <td>Marvin prompt tuning, rate limiting, /rerun comments</td>
      </tr>
      <tr>
          <td>Mar 1 (Sun)</td>
          <td>9</td>
          <td>Mood system, switch to Grok-4, QA pipeline, config centralization</td>
      </tr>
  </tbody>
</table>
<h3>Features Nobody Saw Coming<span class="hx:absolute hx:-mt-20" id="features-nobody-saw-coming"></span>
    <a href="#features-nobody-saw-coming" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><strong>Steam Gaming.</strong> Wasn&rsquo;t in the original plan. It was born because the entertainment section only had anime — and gamer readers were left out. Result: 6 commits just on launch day. The first commit (<code>bf41e25</code>) added the whole section — Steam API service, generation job, tests, Hugo shortcode. The next 5 fixed things: Portuguese date parsing, ranking offset so the top 10 didn&rsquo;t repeat, wishlist filter for releases, dark theme. No spec in the world predicts that the Steam API returns dates with Portuguese abbreviations (<code>&quot;fev&quot;</code>, <code>&quot;mar&quot;</code>, <code>&quot;abr&quot;</code>) when you ask for <code>l=brazilian</code>. You find that out when the parser breaks in production.</p>
<p><strong>Preview system.</strong> <code>/preview</code> was born because I needed to see how the auto-generated content looked <strong>before</strong> publishing the entire newsletter. Seems obvious in retrospect, but in v1 the only way to validate was to generate everything and look at the final result. 5 commits to build a preview system with 9 sections, aliases (<code>hn</code> for hacker_news, <code>steam</code> for steam_gaming), separate comment-preview mode, and rescue when one section fails without taking the others down (<code>d23c725</code> — because a section with an error was killing the entire preview).</p>
<p><strong>Marvin&rsquo;s moods.</strong> Marvin (the bot&rsquo;s sarcastic persona) suffers from a chronic LLM problem: smoothing. Without intervention, every comment ends up the same lukewarm tone. The iterative fix was a mood system — 9 modes the operator picks per story:</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="no">MARVIN_MOODS</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;sarcastic&#34;</span>   <span class="o">=&gt;</span> <span class="s2">&#34;Be extra sarcastic and biting in your commentary.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;ironic&#34;</span>      <span class="o">=&gt;</span> <span class="s2">&#34;Use irony and dark humor to make your point.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;grounded&#34;</span>    <span class="o">=&gt;</span> <span class="s2">&#34;Be more neutral and journalist-like, factual and measured.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;provocative&#34;</span> <span class="o">=&gt;</span> <span class="s2">&#34;Be provocative and controversial, challenge assumptions.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;counter&#34;</span>     <span class="o">=&gt;</span> <span class="s2">&#34;Take the opposite position from Akita&#39;s comment.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;insightful&#34;</span>  <span class="o">=&gt;</span> <span class="s2">&#34;Find a non-obvious angle -- a historical parallel, a second-order consequence.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;positive&#34;</span>    <span class="o">=&gt;</span> <span class="s2">&#34;Find the genuinely positive angle.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;hopeful&#34;</span>     <span class="o">=&gt;</span> <span class="s2">&#34;Be cautiously optimistic. Acknowledge the good potential.&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;negative&#34;</span>    <span class="o">=&gt;</span> <span class="s2">&#34;Be extra pessimistic and bleak.&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span><span class="o">.</span><span class="n">freeze</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>None of this came from a spec. It came from 3 weeks of reading bland comments and thinking <em>&ldquo;this isn&rsquo;t good enough&rdquo;</em>. There are 7 commits just for prompt tuning across 10 days. I banned formulaic patterns (<code>d4bd3a6</code>). I killed off &ldquo;Ah,&rdquo; as an opener (<code>4171628</code>). I removed Marvin from entire podcast sections because he was contaminating the tone (<code>246cc60</code>). I pushed for substance instead of puns (<code>8bc47c4</code>). Each prompt commit is a micro-correction that only makes sense after reading the previous output.</p>
<h3>Bugs That Only Exist in Production<span class="hx:absolute hx:-mt-20" id="bugs-that-only-exist-in-production"></span>
    <a href="#bugs-that-only-exist-in-production" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Of the 56 commits, 13 are bug fixes. Some favorites:</p>
<ul>
<li>
<p><strong>Gmail clipping</strong> (<code>3258f5b</code>): Gmail silently cuts off emails larger than 102KB. I found out when the 2nd newsletter went out longer and Gmail readers didn&rsquo;t see the end. Fix: a size check in the content preflight that warns before publishing. I shrunk the history sections from 10 to 5 items (<code>51ce528</code>) to fit the limit.</p>
</li>
<li>
<p><strong>False paywall detection</strong> (<code>1fd8739</code>): the scraper marked sites as paywalled when it found accidental block text in a generic footer. Only showed up when the story list grew to dozens of real sources.</p>
</li>
<li>
<p><strong>t.co link mangling</strong> (<code>6bd056f</code>): Twitter shortens URLs with t.co, but it also auto-links file names that look like domains. <code>config.yml</code> becomes a link to the <code>config.yml</code> domain. 158 lines of fix to handle tweet text correctly.</p>
</li>
<li>
<p><strong>TTS language revert</strong> (<code>7803ad9</code> -&gt; <code>c1dd668</code>): I tried switching the TTS language from &ldquo;Portuguese&rdquo; to &ldquo;Auto&rdquo;. The model produced a mixed accent. Reverted the same day.</p>
</li>
<li>
<p><strong>Podcast section ordering</strong> (<code>5bef18c</code>): podcast sections came out in the wrong order. 3-line fix + untracking of 1,504 lines of generated content that had been committed by accident.</p>
</li>
<li>
<p><strong>Elementor sites with multiple <code>&lt;article&gt;</code></strong> (<code>e70b756</code>): sites built with Elementor use the <code>&lt;article&gt;</code> tag in a non-semantic way. The content extractor was grabbing the wrong block. 171 lines of fix + tests.</p>
</li>
</ul>
<p>None of these bugs could have been predicted in a spec. They only exist because the system is live, processing real data, hitting real APIs, being read by real people.</p>
<h3>From 1,323 to 1,422 Tests<span class="hx:absolute hx:-mt-20" id="from-1323-to-1422-tests"></span>
    <a href="#from-1323-to-1422-tests" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The test suite kept growing along with it:</p>
<table>
  <thead>
      <tr>
          <th>App</th>
          <th>Tests (Feb 20)</th>
          <th>Tests (Mar 1)</th>
          <th>Growth</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>marvin-bot</td>
          <td>970</td>
          <td>1,060</td>
          <td>+90</td>
      </tr>
      <tr>
          <td>newsletter</td>
          <td>353</td>
          <td>362</td>
          <td>+9</td>
      </tr>
      <tr>
          <td><strong>Total</strong></td>
          <td><strong>1,323</strong></td>
          <td><strong>1,422</strong></td>
          <td><strong>+99</strong></td>
      </tr>
  </tbody>
</table>
<p>New feature? Comes with a test. Bug fix? Comes with a regression test. Without that, the 56 post-production commits would be 56 chances to break something that worked yesterday. TDD isn&rsquo;t a phase, it&rsquo;s a habit.</p>
<h3>The Model Changed: From Claude to Grok-4<span class="hx:absolute hx:-mt-20" id="the-model-changed-from-claude-to-grok-4"></span>
    <a href="#the-model-changed-from-claude-to-grok-4" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Commit <code>8f9d11c</code>: <code>Switch default model to x-ai/grok-4</code>. The architecture had isolated the LLM model into an environment variable from day one. But the model name was still hardcoded across 24 files. Result: commit <code>c8c688e</code> — <code>Centralize LLM model config</code> — 24 files touched to make swapping models a 1-line config change.</p>
<p>This kind of refactoring only shows up in operation. When you start the project, you don&rsquo;t know you&rsquo;re going to change models. When you change, you want it to be trivial. And the cleanup to make it trivial? Only happens when the pain shows up.</p>
<h2>Frank Sherlock: From v0.1 to v0.7 in 4 Days<span class="hx:absolute hx:-mt-20" id="frank-sherlock-from-v01-to-v07-in-4-days"></span>
    <a href="#frank-sherlock-from-v01-to-v07-in-4-days" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The <a href="/en/2026/02/23/vibe-code-built-a-smart-image-indexer-with-ai-in-2-days-frank-sherlock/">Frank Sherlock post</a> covered the first 2 days and ~50 commits: the benchmark research, the Tauri app scaffold (Rust + React), the classification pipeline with Ollama, and the v0.1.0 release with binaries for Linux, macOS and Windows.</p>
<p>What the post didn&rsquo;t cover: the next 53 commits, in 4 days, that took the project from v0.3 to v0.7:</p>
<table>
  <thead>
      <tr>
          <th>Version</th>
          <th>Date</th>
          <th>Highlight</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>v0.4.0</td>
          <td>Feb 24</td>
          <td>Duplicate detection (SHA-256 + dHash), PDF password manager</td>
      </tr>
      <tr>
          <td>v0.5.0</td>
          <td>Feb 24</td>
          <td>Auto-update, scan performance 2x, checkpoint resume</td>
      </tr>
      <tr>
          <td>v0.6.0</td>
          <td>Feb 25</td>
          <td>Video support (11 formats), directory tree, FTS stemmer</td>
      </tr>
      <tr>
          <td>v0.7.0</td>
          <td>Feb 27</td>
          <td><strong>Face detection</strong> with native ONNX, person management</td>
      </tr>
  </tbody>
</table>
<h3>Face Detection: A Feature Born from Iteration<span class="hx:absolute hx:-mt-20" id="face-detection-a-feature-born-from-iteration"></span>
    <a href="#face-detection-a-feature-born-from-iteration" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>v0.1 was an image indexer with LLM-based classification. v0.7 has face detection with 512-dimensional embeddings, cosine-similarity clustering, and per-person search (<code>face:alice</code>). None of that was in the original plan. It came up because I started using the app with real photos and thought <em>&ldquo;I want to find every photo of so-and-so&rdquo;</em>. The feature was born from use, not from a spec.</p>
<p>The path to get there was methodical:</p>
<ol>
<li><strong><code>f34132f</code></strong>: A/B testing framework for face detection — benchmark before implementing, not after</li>
<li><strong><code>ef3be82</code></strong>: A/B results (SCRFD + ArcFace won)</li>
<li><strong><code>6d9174a</code></strong>: Native implementation with ONNX Runtime — no Python, no external dependency</li>
<li><strong><code>3b25eaf</code></strong>: Clustering, person management, complete FacesView</li>
<li><strong><code>a02d67f</code></strong>: Refactoring — extract helpers, delete dead code, share CSS</li>
</ol>
<p>Benchmark -&gt; implement -&gt; refactor. The same cycle as always. And something no one-shot prompt produces.</p>
<h3>Video Support: Another Emergent Feature<span class="hx:absolute hx:-mt-20" id="video-support-another-emergent-feature"></span>
    <a href="#video-support-another-emergent-feature" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The app was built for images. But real media folders have videos. Commit <code>a67a2f9</code> added full support: scanning of 11 formats (MP4, MKV, AVI, MOV, WebM&hellip;), keyframe extraction for LLM classification, black frame skipping, .srt subtitle parsing for the full-text index, and inline preview with HTTP Range streaming.</p>
<p>1,626 lines added in one commit. An entire feature born from real use, not from a spec.</p>
<h3>7 Releases, 3 Platforms, Automatic AUR<span class="hx:absolute hx:-mt-20" id="7-releases-3-platforms-automatic-aur"></span>
    <a href="#7-releases-3-platforms-automatic-aur" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In 7 days, Frank Sherlock had 7 releases. Each one with binaries compiled for Linux (AppImage), macOS (DMG with notarization and code signing), and Windows (MSI). The CI/CD pipeline (<code>release.yml</code>) does everything automatically. Starting at v0.5.0, an additional workflow (<code>aur-publish.yml</code>) publishes automatically to the AUR — the Arch Linux package repository.</p>
<p>Final tally: 17,210 lines of Rust, 10,863 lines of TypeScript, 621 tests (322 Rust + 299 frontend). Cross-platform. Releases published and installable by any user. One week, one dev, one AI agent.</p>
<p>The CI/CD cross-platform part is where the <em>&ldquo;works on my machine&rdquo;</em> fantasy dies. There were 17 commits just for builds: Windows UNC paths, macOS signing with notarization, Linux AppImage, release workflow permissions, macOS Intel target removed. No LLM in the world knows that macOS requires specific entitlements for hardened runtime, or that paths on Windows start with <code>\\?\</code>. That kind of deploy work demands someone who&rsquo;s been through that pain before.</p>
<h2>FrankMD: Real Open Source<span class="hx:absolute hx:-mt-20" id="frankmd-real-open-source"></span>
    <a href="#frankmd-real-open-source" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>FrankMD was the first project in this saga, a self-hosted Markdown editor with Rails 8. The <a href="/en/2026/02/01/frankmd-markdown-editor-vibe-code-part-1/">February posts</a> covered the build. What happened after is more interesting: other people started contributing.</p>
<p>14 commits since February 20. 3 external contributors. 4 PRs merged. v0.2.0 release on the 28th:</p>
<table>
  <thead>
      <tr>
          <th>PR</th>
          <th>Author</th>
          <th>Type</th>
          <th>What it did</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>#34</td>
          <td>@murilo-develop</td>
          <td>Bug fix</td>
          <td>Ollama integration: ModelNotFoundError + API base URL</td>
      </tr>
      <tr>
          <td>#36</td>
          <td>@rafaself</td>
          <td>Bug fix</td>
          <td>Table hint behavior in the editor (3 iterative commits)</td>
      </tr>
      <tr>
          <td>#38</td>
          <td>@LuccaRomanelli</td>
          <td>Feature</td>
          <td>Auto-sync theme with the Omarchy desktop environment</td>
      </tr>
      <tr>
          <td>#39</td>
          <td>@LuccaRomanelli</td>
          <td>Feature</td>
          <td>&ldquo;New Folder&rdquo; in the explorer&rsquo;s context menu</td>
      </tr>
  </tbody>
</table>
<h3>The Maintainer&rsquo;s Pattern<span class="hx:absolute hx:-mt-20" id="the-maintainers-pattern"></span>
    <a href="#the-maintainers-pattern" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>What&rsquo;s most telling isn&rsquo;t the contributions themselves, but what I did after every merge.</p>
<p>When @LuccaRomanelli submitted the Omarchy theme sync PR (+308 lines), I merged it and immediately committed <code>fix: harden Omarchy theme integration and fix broken tests</code> — <strong>+163 lines</strong> of fixes and tests in the next commit. The contributor implemented the feature. The maintainer hardened it.</p>
<p>When @rafaself submitted the table hint fix, there were 3 iterative commits the same day — <code>enhance</code>, <code>improve</code>, <code>streamline</code> — showing the same progressive refinement pattern I do with the AI agent. The next day, he sent a separate commit updating faraday, nokogiri and rack for security advisories.</p>
<p>On the 28th, I sat down and did everything in one shot: merged the 4 PRs, committed the hardening, updated 5 gems (brakeman, bootsnap, selenium-webdriver, web-console, mocha), and published the v0.2.0 release notes. A classic <em>&ldquo;release day&rdquo;</em>.</p>
<p>FrankMD today has 226 commits, 1,804 tests (425 Ruby + 1,379 JavaScript), and active contributors. It isn&rsquo;t <em>&ldquo;my little personal project&rdquo;</em> anymore. It became software with a community. And community doesn&rsquo;t show up for a test-less prototype that <em>&ldquo;works on my machine&rdquo;</em>. It shows up for a project with green CI, documentation, versioned releases, and code you can actually read.</p>
<h2>FrankMega: Even the Smallest Project Needs Post-Production<span class="hx:absolute hx:-mt-20" id="frankmega-even-the-smallest-project-needs-post-production"></span>
    <a href="#frankmega-even-the-smallest-project-needs-post-production" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>FrankMega was built in <a href="/en/2026/02/21/vibe-code-built-a-mega-clone-in-rails-in-1-day-frankmega/">1 day</a>. 26 commits, secure file sharing with Rails 8, 210 tests, deploy via Docker + Cloudflare Tunnel. Post published the same day. Done, right?</p>
<p>Three days later, 2 commits:</p>
<ul>
<li><code>db4bb705</code> — Add macOS, Linux, and Windows package MIME types to seed defaults</li>
<li><code>b0c4829a</code> — Fix MIME types to match Marcel detection, add normalizes strip</li>
</ul>
<p>Users tried to share <code>.dmg</code>, <code>.deb</code>, and <code>.msi</code> files. The MIME type detection (via the Marcel gem) didn&rsquo;t recognize those formats because they weren&rsquo;t in the seed defaults. Two commits. 22 lines. 15 minutes.</p>
<p>But without them, FrankMega couldn&rsquo;t be used to share installer packages — which is exactly the most common use case on a dev&rsquo;s home server.</p>
<blockquote>
  <p>No prompt in the world predicts that your users will want to share <code>.deb</code> files on day one.</p>

</blockquote>
<p>The simplest project of all, with the shortest post-production. And even then, it needed iteration.</p>
<h2>The Consolidated Numbers<span class="hx:absolute hx:-mt-20" id="the-consolidated-numbers"></span>
    <a href="#the-consolidated-numbers" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><table>
  <thead>
      <tr>
          <th>Project</th>
          <th>Commits (total)</th>
          <th>Tests</th>
          <th>Post-production</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>The M.Akita Chronicles</td>
          <td>335</td>
          <td>1,422</td>
          <td>56 commits, 10 days in production</td>
      </tr>
      <tr>
          <td>Frank Sherlock</td>
          <td>103</td>
          <td>621</td>
          <td>53 commits, 4 extra releases</td>
      </tr>
      <tr>
          <td>FrankMD</td>
          <td>226</td>
          <td>1,804</td>
          <td>14 commits, 3 contributors</td>
      </tr>
      <tr>
          <td>FrankMega</td>
          <td>28</td>
          <td>210</td>
          <td>2 commits, MIME fixes</td>
      </tr>
      <tr>
          <td><strong>Total</strong></td>
          <td><strong>692</strong></td>
          <td><strong>4,057</strong></td>
          <td><strong>125 post-publication commits</strong></td>
      </tr>
  </tbody>
</table>
<p>692 commits. 4,057 tests. 4 projects in production. February 2026. Me and an AI agent.</p>
<h2>Why One-Shot Prompting Is a Myth<span class="hx:absolute hx:-mt-20" id="why-one-shot-prompting-is-a-myth"></span>
    <a href="#why-one-shot-prompting-is-a-myth" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Look at the 125 post-production commits and tell me which one of them could have been predicted by a spec.</p>
<p>Gmail cuts off emails larger than 102KB, and you only find that out when you send the first long email. The Steam API returns dates with Portuguese abbreviations (<code>&quot;fev&quot;</code>, <code>&quot;abr&quot;</code>), and you only find that out when the parser breaks on generation Sunday. macOS users try to share <code>.dmg</code> files and can&rsquo;t because the MIME type isn&rsquo;t in the seed. Marvin opens every comment with <em>&ldquo;Ah,&rdquo;</em> and you only notice it after reading 30 in a row and wanting to claw your eyes out. The LLM model has to be swappable with 1 line of config, but it&rsquo;s spread across 24 files. Windows uses UNC paths starting with <code>\\?\</code>, and CI explodes in your face. And TTS in &ldquo;Auto&rdquo; mode? Generates an accent that sounds like Lisbon Portuguese trying to be carioca.</p>
<p>None of this is &ldquo;debugging&rdquo; in the traditional sense. It&rsquo;s navigating a problem space that only reveals itself when the software meets reality. Each one of these was a real-time decision made by someone with context.</p>
<p>The one-shot prompt fantasy is that, if you write a sufficiently detailed spec, the AI produces the perfect software. But the perfect spec would require you to know in advance everything that&rsquo;s going to go wrong. And if you knew in advance everything that&rsquo;s going to go wrong, you wouldn&rsquo;t need the spec — you&rsquo;d already have the software done in your head.</p>
<p>Good software is the result of hundreds of micro-decisions made with the system running. Not a single macro-decision made before the first line is written.</p>
<h2>The Role of the Experienced Developer<span class="hx:absolute hx:-mt-20" id="the-role-of-the-experienced-developer"></span>
    <a href="#the-role-of-the-experienced-developer" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>AI accelerated all of this. Without it, 692 commits in a month would be impossible for one person. But acceleration without direction is just faster entropy.</p>
<p>In every project there were decisions the AI couldn&rsquo;t have made on its own. Switching from Claude to Grok-4 because the previous model was weak in a specific domain. Benchmarking SCRFD against alternatives <em>before</em> implementing face detection, because the wrong choice would cost days. The +163 lines of hardening I committed immediately after merging the Omarchy PR, because I saw where it was going to break. The TTS revert from Auto mode the same day, because I know that &ldquo;Auto&rdquo; on a TTS model for Brazilian Portuguese will produce a mixed accent.</p>
<p>The circuit breaker case is the most illustrative. When I added rate limiting, the Brave Search API started returning 429s every once in a while. If I had asked, the agent would have implemented a retry with exponential backoff. But I didn&rsquo;t ask for retry. I asked for a circuit breaker:</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">WebSearcher</span>
</span></span><span class="line"><span class="cl">  <span class="no">CIRCUIT_BREAKER_SECONDS</span> <span class="o">=</span> <span class="mi">120</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">search</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="ss">max_results</span><span class="p">:</span> <span class="mi">5</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="o">[]</span> <span class="k">if</span> <span class="n">circuit_open?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">brave_search</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">max_results</span><span class="p">)</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">response</span><span class="o">.</span><span class="n">code</span> <span class="o">==</span> <span class="mi">429</span>
</span></span><span class="line"><span class="cl">      <span class="n">trip_circuit!</span>
</span></span><span class="line"><span class="cl">      <span class="no">Rails</span><span class="o">.</span><span class="n">logger</span><span class="o">.</span><span class="n">warn</span><span class="p">(</span><span class="s2">&#34;WebSearcher rate limited (429)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="o">[]</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">rescue</span> <span class="no">Net</span><span class="o">::</span><span class="no">OpenTimeout</span><span class="p">,</span> <span class="no">Net</span><span class="o">::</span><span class="no">ReadTimeout</span> <span class="o">=&gt;</span> <span class="n">e</span>
</span></span><span class="line"><span class="cl">    <span class="n">trip_circuit!</span>
</span></span><span class="line"><span class="cl">    <span class="o">[]</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>If the API returns 429 or times out, it stops trying for 120 seconds. No retry, no backoff, no queue. Why? Because I know the cron runs every day at 8am and that on newsletter generation Sundays the query volume triples. A herd of retries delaying everything is worse than empty results in one section.</p>
<p>The AI implements a circuit breaker when you ask it to. But it isn&rsquo;t going to ask to implement a circuit breaker. It doesn&rsquo;t have the operational context. That&rsquo;s knowledge that comes from running systems in production for decades.</p>
<blockquote>
  <p>The agent writes the code. I decide what code to write. And that decision requires experience no prompt can replace.</p>

</blockquote>
<h2>The Lessons<span class="hx:absolute hx:-mt-20" id="the-lessons"></span>
    <a href="#the-lessons" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p><strong>1. Software in production diverges from the spec in hours, not months.</strong> The first M.Akita Chronicles bugs showed up in the first real newsletter. FrankMega&rsquo;s MIME types broke on the first uploads. Anyone who thinks deploy means done is living in a world that doesn&rsquo;t exist.</p>
<p><strong>2. Post-production isn&rsquo;t &ldquo;maintenance&rdquo;.</strong> 56 commits in 10 days on the M.Akita Chronicles aren&rsquo;t patches. They&rsquo;re new features (Steam Gaming, preview, moods), architecture refactoring (LLM config centralization), security hardening (rate limiting). That&rsquo;s development. The software didn&rsquo;t stop evolving just because I said <em>&ldquo;done&rdquo;</em> in a blog post.</p>
<p><strong>3. TDD protects evolution.</strong> 99 new tests in 10 days. FrankMD&rsquo;s 1,804 tests let me merge 4 external contributor PRs without fear. Without tests, every merge is Russian roulette.</p>
<p><strong>4. Small releases keep you sane.</strong> 7 Frank Sherlock releases in 7 days. Green CI, compiled binaries, release notes. Something broke? Roll back one version. Compare that with &ldquo;6 months + big bang release&rdquo; and tell me which one works better.</p>
<p><strong>5. Community shows up for real projects.</strong> Nobody sends a PR to a test-less prototype that <em>&ldquo;works on my machine&rdquo;</em>. FrankMD got 3 contributors because it has green CI, documentation, and versioned releases.</p>
<p><strong>6. The developer&rsquo;s experience is the bottleneck, not the AI&rsquo;s speed.</strong> 692 commits in a month. But every commit that mattered required decades of experience to know it was needed. The AI types fast. I know what to type.</p>
<p><strong>7. One-shot is for demos. Iteration is for production.</strong> If the goal is a 10-minute video showing a <em>&ldquo;finished SaaS&rdquo;</em>, one-shot will do. If the goal is software that survives contact with real users, only iteration works. And sustainable iteration demands discipline: TDD, CI, small releases, continuous refactoring. No shortcut.</p>
<h2>To the Senior Still Sitting on Their Hands<span class="hx:absolute hx:-mt-20" id="to-the-senior-still-sitting-on-their-hands"></span>
    <a href="#to-the-senior-still-sitting-on-their-hands" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>So far I&rsquo;ve been hitting the amateurs who think they can fire off a prompt and the AI spits out a finished system. Fair enough. But there&rsquo;s another group that worries me just as much: the senior developer who saw the AI mess up three times, declared <em>&ldquo;this is useless&rdquo;</em>, and went back to doing everything by hand.</p>
<p>I get the reasoning. The AI hallucinates, generates code with subtle bugs, suggests over-engineered solutions. All true, and I documented every one of these problems in the previous posts. But tell me something: have you never done exactly the same thing? Have you never spent 2 hours reading documentation only to find out it was a typo in the config? The difference is that when the AI gets it wrong, you catch it in the tests and fix it in minutes. When you get it wrong on your own, it takes you the same amount of time to make the mistake and even longer to find it, because you trust your own code.</p>
<p>Let&rsquo;s look at the concrete numbers of what I shipped in February, one person and one AI agent:</p>
<table>
  <thead>
      <tr>
          <th>Project</th>
          <th>Time</th>
          <th>Result</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>M.Akita Chronicles</td>
          <td>8 days</td>
          <td>4 apps (Rails + Python + Hugo), 335 commits, 1,422 tests, 3 weeks in production</td>
      </tr>
      <tr>
          <td>Frank Sherlock</td>
          <td>7 days</td>
          <td>Tauri desktop app (Rust + React), 103 commits, 621 tests, 7 releases, binaries for 3 OSes</td>
      </tr>
      <tr>
          <td>FrankMD</td>
          <td>~4 days</td>
          <td>Rails 8 Markdown editor, 226 commits, 1,804 tests, 3 external contributors</td>
      </tr>
      <tr>
          <td>FrankMega</td>
          <td>1 day</td>
          <td>Rails 8 file sharing, 28 commits, 210 tests, Docker + Cloudflare deploy</td>
      </tr>
  </tbody>
</table>
<p>Now do the math in your head. How long would it take you to build Frank Sherlock on your own? A Tauri app from scratch, with an LLM classification pipeline in Rust, OCR via Python, full-text search with FTS5, duplicate detection by perceptual hash, face detection with native ONNX, video support with keyframe extraction, CI for 3 platforms with macOS code signing and notarization, auto-update, and automatic publishing to the AUR. With 621 tests. In Rust, which doesn&rsquo;t forgive.</p>
<p>To be honest: without AI, a good senior dev would take at least 3-4 weeks on this, probably more. I did it in 7 days, alone, and every release is published with binaries anyone can download and install.</p>
<p>I already estimated The M.Akita Chronicles in the <a href="/en/2026/02/20/zero-to-post-production-in-1-week-using-ai-on-real-projects-behind-the-m-akita-chronicles/">previous post</a>: ~200 user stories. In Scrum with a senior team of 2-3 devs, no impediments, that would be 10-15 weeks. I did it in 8 days. Today, 3 weeks later, the system keeps running, evolving, with 99 more tests than when it <em>&ldquo;was done&rdquo;</em>.</p>
<p>FrankMega is more modest, but secure file sharing with Rails 8, I18n in 2 languages, 22 security issues fixed, Docker deploy, 210 tests. I did it in 1 day. A good senior, without AI, would do it in 1-2 weeks at best.</p>
<p>And I&rsquo;m not talking about disposable prototypes. People from outside send PRs to FrankMD. Real subscribers get the M.Akita Chronicles newsletter every Monday. Anyone can download Frank Sherlock from GitHub Releases or the AUR. Green CI, Brakeman clean, real tests, automated deploys. That&rsquo;s production software, not a conference demo.</p>
<p><em>&ldquo;Yeah, but I write better code without AI.&rdquo;</em> Maybe. But how long does it take you? What I showed here isn&rsquo;t that AI writes perfect code. Far from it. It&rsquo;s that with TDD, CI, pair programming with the agent, and continuous refactoring, the end result is production code with quality. 4,057 tests are there to prove it. Brakeman clean. 125 post-production commits show the code can take evolution without turning into a ball of mud.</p>
<p>And using AI here and there to generate a snippet of code, like glorified autocomplete, also isn&rsquo;t the answer. You&rsquo;re leaving 90% of the gain on the table. What I did in February was full-time pair programming with an agent. From the first commit to production deploy. With the same discipline I&rsquo;d use with a human pair. Result: 4 projects in production in 1 month, with quality I put my name on. Because I did put my name on it.</p>
<p>If you&rsquo;re a senior and you&rsquo;re still waiting for AI to <em>&ldquo;get better&rdquo;</em> before you really start using it, here&rsquo;s my message: it&rsquo;s already good enough. The 692 commits are there to prove it. The bottleneck now is you learning to work with it.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>In February 2026, I built 4 projects from zero to production with AI. But the build is the easy part. What separates real software from a demo is the 125 commits that came after, when the bugs nobody predicted appeared, when external contributors sent PRs that needed hardening, when new features came up from real use and not from a requirements spreadsheet.</p>
<p>AI made me absurdly more productive. Without it, Frank Sherlock wouldn&rsquo;t have face detection in 7 days. Without it, M.Akita Chronicles wouldn&rsquo;t be in its 3rd week of operation with 1,422 tests. The speed is real.</p>
<p>But none of the decisions that mattered came from the AI. Switching models, reverting the TTS the same day, looking at the contributor PR and seeing where it was going to break, asking for a circuit breaker instead of retry. All of that was me. The AI executed. The decisions were mine.</p>
<p>The AI is the accelerator. Extreme Programming techniques (TDD, small releases, pair programming, continuous refactoring) are the brake and the steering wheel. Without discipline, AI produces fast code that piles up technical debt even faster. With discipline, AI produces software that actually evolves, week after week.</p>
<p>692 commits. 4,057 tests. 4 projects in production. And tomorrow, Monday, at 7am, M.Akita Chronicles subscribers get the 3rd newsletter. Generated, reviewed, and sent by a system that will never be <em>&ldquo;done&rdquo;</em>. Because finished software is dead software.</p>
<blockquote>
  <p>&ldquo;Finished software is dead software. Live software iterates.&rdquo;</p>

</blockquote>
]]></content:encoded><category>frank</category><category>llms</category><category>software-engineering</category><category>off-topic</category></item><item><title>Integration Tests in a Monorepo | Behind The M.Akita Chronicles</title><link>https://akitaonrails.github.io/en/2026/02/20/integration-tests-in-a-monorepo-behind-the-m-akita-chronicles/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/02/20/integration-tests-in-a-monorepo-behind-the-m-akita-chronicles/</guid><pubDate>Fri, 20 Feb 2026 12:48:45 +0000</pubDate><description>&lt;p&gt;This post is part of a series; follow along via the tag &lt;a href="https://akitaonrails.github.io/en/tags/the-m-akita-chronicles/"&gt;/themakitachronicles&lt;/a&gt;. This is part 8.&lt;/p&gt;
&lt;p&gt;And make sure to subscribe to my new newsletter &lt;a href="https://themakitachronicles.com/"target="_blank" rel="noopener"&gt;The M.Akita Chronicles&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&amp;ndash;&lt;/p&gt;
&lt;p&gt;Unit tests are every developer&amp;rsquo;s comfort zone. You mock everything, isolate everything, run it in milliseconds, and the green in the terminal gives you a false sense of security. Until the day you deploy and discover that service A writes in a format service B can&amp;rsquo;t read. Or that the Hacker News RSS switched from Atom to RSS and your parser blew up. Or that Yahoo Finance started returning 429 after the third ticker because the rate limit that worked in tests doesn&amp;rsquo;t exist in real life.&lt;/p&gt;</description><content:encoded><![CDATA[<p>This post is part of a series; follow along via the tag <a href="/en/tags/the-m-akita-chronicles/">/themakitachronicles</a>. This is part 8.</p>
<p>And make sure to subscribe to my new newsletter <a href="https://themakitachronicles.com/"target="_blank" rel="noopener">The M.Akita Chronicles</a>!</p>
<p>&ndash;</p>
<p>Unit tests are every developer&rsquo;s comfort zone. You mock everything, isolate everything, run it in milliseconds, and the green in the terminal gives you a false sense of security. Until the day you deploy and discover that service A writes in a format service B can&rsquo;t read. Or that the Hacker News RSS switched from Atom to RSS and your parser blew up. Or that Yahoo Finance started returning 429 after the third ticker because the rate limit that worked in tests doesn&rsquo;t exist in real life.</p>
<p>Unit tests prove the pieces work in isolation. Integration tests prove the <strong>system</strong> works. And in a project with multiple applications sharing data via the filesystem, the difference between the two is the difference between &ldquo;the code compiles&rdquo; and &ldquo;the newsletter lands in the inbox&rdquo;.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-22-50.jpg" alt="unit tests"  loading="lazy" /></p>
<h2>The Problem: Three Apps, One Filesystem, Zero Guarantees<span class="hx:absolute hx:-mt-20" id="the-problem-three-apps-one-filesystem-zero-guarantees"></span>
    <a href="#the-problem-three-apps-one-filesystem-zero-guarantees" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The project has an unusual architecture: three Rails/Hugo applications in a monorepo, integrated through a shared <code>content/</code> directory:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>akitando-news/
├── marvin-bot/    # Gera conteúdo (Discord bot, AI, scraping)
├── newsletter/    # Monta e envia a newsletter
├── blog/          # Hugo &#43; Hextra → Netlify
├── podcast-tts/   # Servidor TTS (Python/FastAPI)
└── content/       # Markdown &#43; YAML, a cola entre tudo</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>marvin-bot writes Markdown with YAML frontmatter into <code>content/anime/</code>, <code>content/hacker_news/</code>, etc. The newsletter reads those files to assemble the newsletter. The blog serves the same content as static pages. The podcast reads the assembled newsletter to generate the dialogue script.</p>
<p>The contract between these systems is <strong>implicit</strong>. There&rsquo;s no schema. No versioned API. No protobuf. The contract is: &ldquo;the Markdown file has this frontmatter, with these fields, in this structure&rdquo;. If marvin-bot changes the format of a field — say, switches <code>image</code> from string to array — the newsletter breaks silently. No unit test in either app detects it.</p>
<p>That&rsquo;s the fundamental flaw of testing distributed systems with unit tests: <strong>you&rsquo;re testing each side of the contract separately, but never the contract itself</strong>.</p>
<h2>Layer 1: Unit Tests With Paranoid Isolation<span class="hx:absolute hx:-mt-20" id="layer-1-unit-tests-with-paranoid-isolation"></span>
    <a href="#layer-1-unit-tests-with-paranoid-isolation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Before talking about integration, I need to explain what the unit tests do in this project — because without that foundation, the integration tests would have nothing to stand on.</p>
<p>The 977 marvin-bot tests run in parallel using every core on the machine. Each test worker has its own directory:</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="n">parallelize</span><span class="p">(</span><span class="ss">workers</span><span class="p">:</span> <span class="ss">:number_of_processors</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">parallelize_setup</span> <span class="k">do</span> <span class="o">|</span><span class="n">worker</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <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">content_dir</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl">    <span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;tmp&#34;</span><span class="p">,</span> <span class="s2">&#34;test_content_w</span><span class="si">#{</span><span class="n">worker</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">to_s</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>But worker-level isn&rsquo;t enough. Inside each worker, multiple tests run sequentially — and if one test leaves a file in <code>content/stories/</code>, the next test might find spurious data. The solution: <strong>one unique directory per test</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="n">setup</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@base_content_dir</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">content_dir</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@test_content_dir</span> <span class="o">=</span> <span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="vi">@base_content_dir</span><span class="p">,</span> <span class="no">SecureRandom</span><span class="o">.</span><span class="n">hex</span><span class="p">(</span><span class="mi">8</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">application</span><span class="o">.</span><span class="n">config</span><span class="o">.</span><span class="n">content_dir</span> <span class="o">=</span> <span class="vi">@test_content_dir</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="no">FileUtils</span><span class="o">.</span><span class="n">mkdir_p</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="vi">@test_content_dir</span><span class="p">,</span> <span class="s2">&#34;stories&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="no">FileUtils</span><span class="o">.</span><span class="n">mkdir_p</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="vi">@test_content_dir</span><span class="p">,</span> <span class="s2">&#34;newsletters&#34;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="no">FileUtils</span><span class="o">.</span><span class="n">mkdir_p</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="vi">@test_content_dir</span><span class="p">,</span> <span class="s2">&#34;images&#34;</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="n">teardown</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">FileUtils</span><span class="o">.</span><span class="n">rm_rf</span><span class="p">(</span><span class="vi">@test_content_dir</span><span class="p">)</span> <span class="k">if</span> <span class="vi">@test_content_dir</span>
</span></span><span class="line"><span class="cl">  <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">content_dir</span> <span class="o">=</span> <span class="vi">@base_content_dir</span> <span class="k">if</span> <span class="vi">@base_content_dir</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>Pay attention to the teardown line that restores <code>@base_content_dir</code>. Without it, the next test would create its directory <strong>inside</strong> the previous test&rsquo;s directory — <code>tmp/test_content_w3/abc123/def456/</code> — and by test 50 you&rsquo;d have a path 50 levels deep. I learned this burning an entire afternoon debugging why the tests kept getting slower as the suite progressed.</p>
<p>This pattern — worker-level base + per-test subdirectory + restore on teardown — is identical across both apps. And it&rsquo;s what lets you run 977 tests in 7 seconds with 32 parallel workers.</p>
<p>The golden rule: <strong>the test <code>content_dir</code> must NEVER point to the real <code>../content</code></strong>. If it does, the teardown&rsquo;s <code>rm_rf</code> wipes out your production prompts. This isn&rsquo;t hypothetical — it happened during development.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-24-08.jpg" alt="unit tests newsletter"  loading="lazy" /></p>
<h2>Layer 2: The Integration Environment<span class="hx:absolute hx:-mt-20" id="layer-2-the-integration-environment"></span>
    <a href="#layer-2-the-integration-environment" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Unit tests mock everything: external APIs, AI, Discord, SES. That&rsquo;s correct — you don&rsquo;t want to burn $2 in GPT-5.2 tokens every time you run <code>bin/rails test</code>. But it means the tests never exercise the real pipeline.</p>
<p>The project has a third environment beyond development and test: <strong>integration</strong>. It&rsquo;s not test. It&rsquo;s not staging. It&rsquo;s a specific mode for running the full pipeline with real data, using real APIs, but without sending actual emails:</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"># config/environments/integration.rb</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Jobs rodam inline (perform_later vira perform_now)</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">active_job</span><span class="o">.</span><span class="n">queue_adapter</span> <span class="o">=</span> <span class="ss">:inline</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Emails salvos em arquivo ao invés de enviados via SES</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">action_mailer</span><span class="o">.</span><span class="n">delivery_method</span> <span class="o">=</span> <span class="ss">:file</span>
</span></span><span class="line"><span class="cl"><span class="n">config</span><span class="o">.</span><span class="n">action_mailer</span><span class="o">.</span><span class="n">file_settings</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="ss">location</span><span class="p">:</span> <span class="no">Rails</span><span class="o">.</span><span class="n">root</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;tmp/integration_emails&#34;</span><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>The <code>queue_adapter = :inline</code> is crucial. In production, <code>perform_later</code> enqueues the job into SolidQueue and a separate worker executes it. In integration, it runs <strong>right there, in the same process</strong>. That turns the asynchronous production pipeline into a synchronous pipeline you can run, observe, and debug sequentially.</p>
<p>SES is swapped for writing to disk — every email becomes an HTML file in <code>tmp/integration_emails/</code>. After running the pipeline, you open those HTMLs in the browser to visually validate that the email renders correctly.</p>
<h2>Layer 3: DevCache — LLMs Aren&rsquo;t Idempotent<span class="hx:absolute hx:-mt-20" id="layer-3-devcache--llms-arent-idempotent"></span>
    <a href="#layer-3-devcache--llms-arent-idempotent" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s a problem anyone working with AI in production knows: calling GPT-5.2 with the same prompt twice <strong>doesn&rsquo;t return the same answer</strong>. And every call costs money. If you&rsquo;re iterating on the integration pipeline — running, tweaking a prompt, running again — spending $3 per iteration gets expensive fast.</p>
<p>The solution is <code>DevCache</code>: a file-based cache that only activates in the integration env:</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">module</span> <span class="nn">DevCache</span>
</span></span><span class="line"><span class="cl">  <span class="no">CACHE_TTL</span> <span class="o">=</span> <span class="mi">1</span><span class="o">.</span><span class="n">day</span>
</span></span><span class="line"><span class="cl">  <span class="no">CACHE_ENVS</span> <span class="o">=</span> <span class="sx">%w[integration]</span><span class="o">.</span><span class="n">freeze</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">fetch</span><span class="p">(</span><span class="n">namespace</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="k">yield</span> <span class="k">unless</span> <span class="n">enabled?</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">path</span> <span class="o">=</span> <span class="n">cache_path</span><span class="p">(</span><span class="n">namespace</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">path</span><span class="o">.</span><span class="n">exist?</span> <span class="o">&amp;&amp;</span> <span class="no">File</span><span class="o">.</span><span class="n">mtime</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="o">&gt;</span> <span class="no">CACHE_TTL</span><span class="o">.</span><span class="n">ago</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="no">JSON</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">path</span><span class="o">.</span><span class="n">read</span><span class="p">,</span> <span class="ss">symbolize_names</span><span class="p">:</span> <span class="kp">true</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="n">result</span> <span class="o">=</span> <span class="k">yield</span>
</span></span><span class="line"><span class="cl">    <span class="n">path</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="no">JSON</span><span class="o">.</span><span class="n">generate</span><span class="p">(</span><span class="n">result</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</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>In production and in test, <code>DevCache.fetch</code> runs the block directly. In integration, it saves the result as JSON and returns the cached value on the next run. TTL of 1 day — enough to iterate within the same day without recomputing, short enough to not serve stale data.</p>
<p>The generation code uses it like this:</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="n">summary</span> <span class="o">=</span> <span class="no">DevCache</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;summarize&#34;</span><span class="p">,</span> <span class="n">url_key</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">AiChat</span><span class="o">.</span><span class="n">summarize</span><span class="p">(</span><span class="n">article</span><span class="o">[</span><span class="ss">:content</span><span class="o">]</span><span class="p">,</span> <span class="ss">source_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="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>First run: calls GPT-5.2, pays $0.02, saves to <code>tmp/dev_cache/summarize/{md5}.json</code>. Second run: reads from the cache, zero cost. <code>FORCE=1</code> busts the cache when you want to force regeneration — as an explicit flag, not a default.</p>
<p>DevCache exists in both apps (marvin-bot and newsletter) with identical implementation. When you run <code>integration:clean</code>, both caches are wiped.</p>
<p>The point: <strong>LLM caching in a development environment is a requirement, not an optimization</strong>. Without it, iterating on a prompt that needs 8 API calls becomes financially unworkable.</p>
<h2>Layer 4: Real Data via Rsync<span class="hx:absolute hx:-mt-20" id="layer-4-real-data-via-rsync"></span>
    <a href="#layer-4-real-data-via-rsync" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>This is where most projects stop. They have unit tests, maybe a staging environment, and ship it. The problem: staging uses test data. Test data is <strong>too clean</strong>. It doesn&rsquo;t have the edge cases real data has:</p>
<ul>
<li>Articles with titles in Japanese (anime ranking)</li>
<li>URLs that redirect 3 times before reaching the destination</li>
<li>Images that return 200 with Content-Type <code>text/html</code> (CloudFlare error page)</li>
<li>Previous newsletters with slightly different frontmatter formats</li>
<li>Stories with <code>score: null</code> because they were created before the field existed</li>
</ul>
<p>The solution: <strong>rsync from production</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"># Limpa conteúdo gerado (preserva stories e imagens de produção)</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> marvin-bot <span class="o">&amp;&amp;</span> bin/rails integration:clean
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Sincroniza content/ de produção pra local</span>
</span></span><span class="line"><span class="cl">rsync -avz --delete production:/path/to/content/ ../content/
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Roda o pipeline completo com dados reais</span>
</span></span><span class="line"><span class="cl"><span class="nv">RAILS_ENV</span><span class="o">=</span>integration bin/rails integration:pipeline</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><code>integration:clean</code> is surgical: it removes generated directories (<code>anime/</code>, <code>hacker_news/</code>, <code>newsletters/</code>, <code>podcasts/</code>) but preserves <code>stories/</code> and <code>images/</code> — which are user-submitted data coming in via Discord. After the rsync, the local directory is an exact copy of production.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-25-37.jpg" alt="cleanup"  loading="lazy" /></p>
<p>The pipeline then generates every section from that real data. If a parser breaks on a Korean title, you find out here — not in production at 5pm on a Sunday when the 8 jobs run in parallel.</p>
<h2>Layer 5: The Full Pipeline<span class="hx:absolute hx:-mt-20" id="layer-5-the-full-pipeline"></span>
    <a href="#layer-5-the-full-pipeline" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The heart of the integration system is <code>integration:pipeline</code>. It simulates the entire production week in a single run:</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="nv">RAILS_ENV</span><span class="o">=</span>integration bin/rails integration:pipeline</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>The pipeline runs in waves, honoring dependencies:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>Wave 1 (paralelo, 8 jobs):
  Book, Holidays, History, Geek History,
  Anime, Hacker News, YouTube, World Events

Wave 2 (depende de World Events):
  Market Recap

Wave 3 (depende de tudo):
  Closing Remarks

Preflight → Newsletter Assembly → Podcast</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><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-26-13.jpg" alt="waves"  loading="lazy" /></p>
<p>Each wave runs its jobs in parallel threads, exactly like production does with SolidQueue. Parallel execution isn&rsquo;t an implementation detail here — it&rsquo;s a test feature. If two jobs try to write to the same file, or if one job reads a file another is in the middle of writing, the bug shows up here.</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="n">threads</span> <span class="o">=</span> <span class="n">to_run</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="nb">name</span><span class="p">,</span> <span class="n">klass</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">  <span class="no">Thread</span><span class="o">.</span><span class="n">new</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">Base</span><span class="o">.</span><span class="n">connection_pool</span><span class="o">.</span><span class="n">with_connection</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">run_single_job</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">klass</span><span class="p">,</span> <span class="n">target_date</span><span class="p">,</span> <span class="n">results</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><span class="line"><span class="cl"><span class="n">threads</span><span class="o">.</span><span class="n">each</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:join</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><code>with_connection</code> is necessary because each thread needs its own SQLite connection — without it, connections leak and the pool gets exhausted.</p>
<p>At the end, the pipeline prints a status table and a billing summary:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-34-56.jpg" alt="billing"  loading="lazy" /></p>
<p>That billing summary isn&rsquo;t vanity — it&rsquo;s cost control. If a refactored prompt triples the output tokens, you see it here before going to production and burning 10x on a Sunday with 200 stories.</p>
<h2>Layer 6: Preflight — Automated Structural Validation<span class="hx:absolute hx:-mt-20" id="layer-6-preflight--automated-structural-validation"></span>
    <a href="#layer-6-preflight--automated-structural-validation" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>After generating every section, the pipeline runs <code>ContentPreflight</code>. It doesn&rsquo;t validate the <em>content</em> — it validates the <em>structure</em>. Each section gets checked:</p>
<ul>
<li><strong>Does the file exist?</strong> If <code>content/anime/2026-02-16.md</code> doesn&rsquo;t exist, something failed.</li>
<li><strong>Minimum item count?</strong> The anime section needs at least 3 items. If it has 1, the generator had a problem.</li>
<li><strong>Required markers?</strong> The Markdown must contain <code>[COMMENTARY]</code> and <code>[AKITA]</code> — without them, newsletter assembly doesn&rsquo;t know where to cut.</li>
<li><strong>Specific data?</strong> Market recap needs at least 5 ticker rows.</li>
</ul>
<p>The result is a status per section: <code>pass</code>, <code>degraded</code>, <code>fail</code>, or <code>skip</code>. The pipeline uses this to decide whether the newsletter can be assembled automatically or needs human intervention:</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-36-19.jpg" alt="preflight"  loading="lazy" /></p>
<p>In production, this result goes to Discord as an embed. In the integration pipeline, it&rsquo;s printed to the terminal. The logic is the same — the notification channel changes.</p>
<h2>Layer 7: Cross-App — Newsletter Assembly<span class="hx:absolute hx:-mt-20" id="layer-7-cross-app--newsletter-assembly"></span>
    <a href="#layer-7-cross-app--newsletter-assembly" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Here&rsquo;s where the monorepo pays dividends. marvin-bot needs to invoke the newsletter to assemble the final newsletter. In production, these are different servers. In the integration pipeline, it&rsquo;s a <code>system()</code> call:</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="n">env_vars</span> <span class="o">=</span> <span class="s2">&#34;RAILS_ENV=integration CONTENT_DIR=</span><span class="si">#{</span><span class="no">Shellwords</span><span class="o">.</span><span class="n">escape</span><span class="p">(</span><span class="n">content_dir</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="n">cmd</span> <span class="o">=</span> <span class="s2">&#34;cd </span><span class="si">#{</span><span class="n">newsletter_dir</span><span class="si">}</span><span class="s2"> &amp;&amp; </span><span class="si">#{</span><span class="n">env_vars</span><span class="si">}</span><span class="s2"> bin/rails integration:generate_all&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">system</span><span class="p">(</span><span class="n">cmd</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><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-38-17.jpg" alt="assembly"  loading="lazy" /></p>
<p>The newsletter reads the files marvin-bot just generated in <code>content/</code>, assembles the newsletter, and saves the result in <code>content/newsletters/</code>. CONTENT_DIR is passed explicitly — both apps point to the same directory.</p>
<p>If the frontmatter format changed and the newsletter can&rsquo;t parse it, the error shows up here. If the newsletter&rsquo;s <code>SectionParser</code> has diverged from marvin-bot&rsquo;s <code>SectionParser</code> (they&rsquo;re identical copies, but maintained by hand), here&rsquo;s where you find out.</p>
<p>After the newsletter, the podcast: the pipeline invokes <code>podcast:integration</code>, which generates the dialogue script from the assembled newsletter, synthesizes audio via TTS, and builds the final MP3. It&rsquo;s the longest pipeline — from Markdown newsletter to Spotify-ready MP3, passing through a two-pass LLM and per-turn voice synthesis.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-47-52.jpg" alt="tts"  loading="lazy" /></p>
<h2>The Value of Testing With Production Data<span class="hx:absolute hx:-mt-20" id="the-value-of-testing-with-production-data"></span>
    <a href="#the-value-of-testing-with-production-data" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let me give concrete examples of bugs that only showed up with real data:</p>
<p><strong>1. Japanese anime titles broke the slugify.</strong>
<code>ContentWriter.slugify</code> used Rails&rsquo; <code>parameterize</code>, which strips non-ASCII characters. Title: &ldquo;進撃の巨人 Season 4&rdquo; → slug: &ldquo;season-4&rdquo;. Two anime with the subtitle &ldquo;Season 4&rdquo; collided on the filesystem. It never showed up in tests because the fixtures used English titles.</p>
<p><strong>2. The HackerNews parser assumed RSS, but the feed switched to Atom.</strong>
<code>rss/channel/item</code> became <code>feed/entry</code>. Unit tests mocked the XML. The parser itself was never tested against the real feed. Running integration with the real rsynced feed, the parser returned zero items — caught by preflight as <code>fail</code>.</p>
<p><strong>3. Yahoo Finance returned 429 after the 5th ticker.</strong>
Rate limiting didn&rsquo;t exist in tests (HTTP was mocked). In the real integration, the first 5 tickers went through, then everything failed. The fix — sleeping between requests — would never have been implemented without real data exposing the problem.</p>
<p><strong>4. Images with a valid URL but the wrong Content-Type.</strong>
A CDN returned 200 for image URLs, but with an HTML body (Cloudflare error page). <code>UrlValidator.reachable?</code> returned <code>true</code>. <code>ImageProcessor.download_image</code> downloaded the HTML and tried to resize it. mini_magick crashed with an obscure error. The fix: validate Content-Type on the HEAD response — but this scenario would never surface with mocked URLs.</p>
<p><strong>5. Newsletter assembly failed on stories with <code>score: null</code>.</strong>
Old stories, created before the <code>score</code> field had a default, had <code>nil</code> in the frontmatter. Assembly tried to do <code>sort_by { |s| s[:frontmatter][&quot;score&quot;] }</code> — <code>nil &lt;=&gt; &quot;high&quot;</code> raises <code>ArgumentError</code>. It only appeared when I rsynced production data that had 3 months of accumulated stories.</p>
<p>None of these bugs would be caught by a unit test. All of them were caught by the integration pipeline with real data.</p>
<h2>Practical Tips<span class="hx:absolute hx:-mt-20" id="practical-tips"></span>
    <a href="#practical-tips" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><h3>1. Separate Cost From Fidelity<span class="hx:absolute hx:-mt-20" id="1-separate-cost-from-fidelity"></span>
    <a href="#1-separate-cost-from-fidelity" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The full integration pipeline costs around $0.40 in API calls. Not much, but if you run it 10 times in a day while iterating, that&rsquo;s $4. DevCache solves this: the first run pays, the following runs are free.</p>
<p>But there are scenarios where you <strong>want</strong> to pay again — when you change a prompt and need to see the real result. <code>FORCE=1</code> busts the cache selectively. And <code>SKIP_EXISTING=1</code> skips sections that have already been generated, saving time when you only want to re-run a specific section.</p>
<h3>2. The Integration Environment Is Not Staging<span class="hx:absolute hx:-mt-20" id="2-the-integration-environment-is-not-staging"></span>
    <a href="#2-the-integration-environment-is-not-staging" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Staging is a server running 24/7 simulating production. The integration environment is a <strong>local execution mode</strong> you turn on when needed and turn off when you&rsquo;re done. No dedicated infrastructure required. No separate database required. Same code, same machine, different flags.</p>
<p>The difference is philosophical: staging tests &ldquo;does the system work on the staging server?&rdquo;. Integration tests &ldquo;does the system work <strong>with this specific data</strong>?&rdquo;. The first is environment-dependent. The second is data-dependent. And data bugs are more common than environment bugs.</p>
<h3>3. Clean Before Rsync, Not After<span class="hx:absolute hx:-mt-20" id="3-clean-before-rsync-not-after"></span>
    <a href="#3-clean-before-rsync-not-after" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Order matters: clean the generated content <strong>before</strong> syncing from production. If you do the opposite — rsync first, clean after — you lose the production data you just downloaded.</p>
<p><code>integration:clean</code> is surgical by design. It removes anime/, hacker_news/, newsletters/ (generated), but preserves stories/ and images/ (production). That enables an iterative workflow: rsync once, run the pipeline, tweak a prompt, run again — without re-downloading 500MB of content.</p>
<h3>4. Preflight Is Your Smoke Test<span class="hx:absolute hx:-mt-20" id="4-preflight-is-your-smoke-test"></span>
    <a href="#4-preflight-is-your-smoke-test" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Don&rsquo;t trust &ldquo;ran without exception = correct&rdquo;. A generation job can run, produce a Markdown with 2 items instead of 10, and return success. Preflight catches that by verifying minimum counts, required markers, and expected structure.</p>
<p>In practice, every LLM bug I&rsquo;ve found was caught by preflight: truncated responses, wrong format, missing sections. GPT-5.2 is good, but it isn&rsquo;t deterministic — and when it fails, it fails in creative ways no schema validation would catch.</p>
<h3>5. Billing Summary Is Not Optional<span class="hx:absolute hx:-mt-20" id="5-billing-summary-is-not-optional"></span>
    <a href="#5-billing-summary-is-not-optional" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you&rsquo;re using LLMs in production, <strong>every</strong> test pipeline needs to report cost. Not for accounting — for engineering. A prompt that works but costs 3x more than the previous one is a performance bug. Input and output tokens per provider, cost per job, total cost — all visible on every run.</p>
<h3>6. Monorepo Helps, But Doesn&rsquo;t Solve<span class="hx:absolute hx:-mt-20" id="6-monorepo-helps-but-doesnt-solve"></span>
    <a href="#6-monorepo-helps-but-doesnt-solve" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Having the three apps in the same repository helps: a <code>git bisect</code> covers changes on both sides of the contract. A PR that changes the frontmatter format in marvin-bot <strong>can</strong> also update the parser in newsletter — in the same commit.</p>
<p>But &ldquo;can&rdquo; isn&rsquo;t &ldquo;guarantees&rdquo;. The contract between the apps is file-based and implicit. There&rsquo;s no shared type, no common interface. <code>SectionParser</code> is copied manually between the two apps. If someone edits one and forgets the other, they diverge. The integration test is the <strong>only</strong> verification that both sides agree.</p>
<h3>7. Preview Before Sending<span class="hx:absolute hx:-mt-20" id="7-preview-before-sending"></span>
    <a href="#7-preview-before-sending" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The pipeline doesn&rsquo;t end at generation. The newsletter has a <code>newsletter:preview</code> that renders the final result as HTML in light and dark mode:</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="nb">cd</span> newsletter
</span></span><span class="line"><span class="cl"><span class="nv">FILE</span><span class="o">=</span>../content/newsletters/2026-02-16.md bin/rails newsletter:preview
</span></span><span class="line"><span class="cl"><span class="c1"># → tmp/preview/newsletter_light.html</span>
</span></span><span class="line"><span class="cl"><span class="c1"># → tmp/preview/newsletter_dark.html</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>Opening those HTMLs in the browser is the visual integration test. The Markdown can be perfect, the frontmatter correct, every field present — but an anime image shows up stretched because the aspect ratio changed. Or Marvin&rsquo;s commentary block doesn&rsquo;t have enough contrast in dark mode. Those bugs are visual — no automated test catches them.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/frankmd/2026/02/screenshot-2026-02-20_12-48-32.jpg" alt="final assembly"  loading="lazy" /></p>
<h3>8. The CI Pipeline Is Different From the Integration Pipeline<span class="hx:absolute hx:-mt-20" id="8-the-ci-pipeline-is-different-from-the-integration-pipeline"></span>
    <a href="#8-the-ci-pipeline-is-different-from-the-integration-pipeline" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>These are different things. CI runs on every commit:</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">bin/ci  <span class="c1"># rubocop + bundler-audit + brakeman + tests (~22s)</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>Integration runs when you want to validate the pipeline end-to-end — before a deploy, after changing prompts, or after rsyncing fresh data from production. CI is fast and cheap. Integration is slow and costs money. Both are necessary.</p>
<h2>The Cost of Not Having Integration<span class="hx:absolute hx:-mt-20" id="the-cost-of-not-having-integration"></span>
    <a href="#the-cost-of-not-having-integration" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let me be direct: if this project didn&rsquo;t have the integration pipeline, I would have shipped at least 3 broken newsletters.</p>
<p>One with the anime section missing because the XML parser assumed RSS and the feed was Atom. One with every YouTube image pointing to 404 because the thumbnail URL pattern changed. And one with the market recap showing last month&rsquo;s tickers because Yahoo Finance blocked the requests and the job failed silently.</p>
<p>Unit tests would have passed in all those scenarios. CI would have gone green. The deploy would have shipped. And at 7am on a Monday, 300 people would have received a newsletter full of holes.</p>
<p>The integration pipeline is expensive — in setup time, in API cost, in maintenance complexity. But it&rsquo;s orders of magnitude cheaper than sending broken content to real subscribers. Especially when you only get one shot a week.</p>
<h2>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The testing hierarchy in this project is:</p>
<ol>
<li><strong>Unit tests</strong> (1,330 across both apps, 7 seconds in parallel, $0): prove each piece works in isolation</li>
<li><strong>CI pipeline</strong> (rubocop + audit + brakeman + tests, ~22 seconds, $0): prove the code is healthy</li>
<li><strong>Integration pipeline</strong> (real data, real APIs, ~3 minutes, ~$0.40): proves the <strong>system</strong> works</li>
<li><strong>Visual preview</strong> (HTML in the browser, manual, $0): proves the final result is presentable</li>
</ol>
<p>Each layer catches a different type of bug. None of them is enough alone. And the one that surprises most — the one that catches the bugs you swore didn&rsquo;t exist — is the third: real data, real pipeline, no mocks.</p>
<p>The confidence you have in your system is proportional to how real the data in your tests is. Mock everything and you get 100% coverage and zero confidence. Test with production data and you get the truth — however inconvenient.</p>
]]></content:encoded><category>the-m-akita-chronicles</category><category>testing</category><category>software-engineering</category></item><item><title>Vibe Code: I Built a Little App 100% with GLM 4.7 (TV Clipboard)</title><link>https://akitaonrails.github.io/en/2026/01/28/vibe-code-built-a-little-app-fully-with-glm-4-7-tv-clipboard/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2026/01/28/vibe-code-built-a-little-app-fully-with-glm-4-7-tv-clipboard/</guid><pubDate>Wed, 28 Jan 2026 08:41:52 -0300</pubDate><description>&lt;p&gt;Yesterday I posted the following on 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;
This is a concept I&amp;rsquo;ve talked about multiple times in videos on my channel and in talks. Everything in programming happens more or less at the Pareto ratio: 80/20:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;80% of the code is &amp;ldquo;easy&amp;rdquo; and usually gets done in 20% of the time. The most important 20% will eat up most of the remaining 80% of total time.&lt;/li&gt;
&lt;li&gt;For bugs and issues like performance or security, if you fix the top 20% you solve 80% of the general problems.&lt;/li&gt;
&lt;li&gt;20% of the scope is truly important, which is why you can always simplify, replace, change, or even cut 80% of it - and that&amp;rsquo;s how you squeeze a project into budget/deadline.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything is based on Top X. &lt;a href="https://akitaonrails.com/2018/09/18/akitando-11-projetos-aprendendo-a-priorizar/"target="_blank" rel="noopener"&gt;Learning to prioritize&lt;/a&gt; is learning to identify the 20%. The difference between someone senior/experienced and someone amateur/beginner is that beginners think 100% is important and can&amp;rsquo;t pick the 20%. What experience brings is the ability to isolate that 20% faster and faster.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Yesterday I posted the following on 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>
This is a concept I&rsquo;ve talked about multiple times in videos on my channel and in talks. Everything in programming happens more or less at the Pareto ratio: 80/20:</p>
<ul>
<li>80% of the code is &ldquo;easy&rdquo; and usually gets done in 20% of the time. The most important 20% will eat up most of the remaining 80% of total time.</li>
<li>For bugs and issues like performance or security, if you fix the top 20% you solve 80% of the general problems.</li>
<li>20% of the scope is truly important, which is why you can always simplify, replace, change, or even cut 80% of it - and that&rsquo;s how you squeeze a project into budget/deadline.</li>
</ul>
<p>Everything is based on Top X. <a href="https://akitaonrails.com/2018/09/18/akitando-11-projetos-aprendendo-a-priorizar/"target="_blank" rel="noopener">Learning to prioritize</a> is learning to identify the 20%. The difference between someone senior/experienced and someone amateur/beginner is that beginners think 100% is important and can&rsquo;t pick the 20%. What experience brings is the ability to isolate that 20% faster and faster.</p>
<p><strong>HOWEVER</strong>, it&rsquo;s very easy to just say this. Everyone has heard it at least once, but most likely still doesn&rsquo;t understand what it means. That&rsquo;s why I decided to write this (LONG) article: I&rsquo;ll show you <strong>in practice</strong> what the easy 80% and the hard 20% are, and how that actually affects code quality, time spent and cost.</p>
<p>This article is for beginners mainly, but also for you: the senior who has juniors to mentor, or the teachers with students. Use it as discussion and study material with your apprentices. This is the kind of thing that should be taught.</p>
<p>Since this is going to be long, remember that on desktop, on the right side there&rsquo;s a menu with the sections so you can jump straight to each topic and study it piece by piece.</p>
<h2>The App: TV Clipboard<span class="hx:absolute hx:-mt-20" id="the-app-tv-clipboard"></span>
    <a href="#the-app-tv-clipboard" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>I picked a little app I had been wanting for a while for my personal use. It&rsquo;s simpler than a to-do list, it doesn&rsquo;t need a database or any other external service. In practice it&rsquo;s a peer-to-peer (p2p) communicator over websockets.</p>
<p>The use case is this: I have a gamer mini-PC that I hook up to the living room TV. I usually leave a mouse by the armchair to navigate on it (it&rsquo;s a damned Windows 11). But occasionally I need to type something (passwords, fill out some form). And I don&rsquo;t want to leave my important accounts logged into that PC (I don&rsquo;t trust gamer PCs).</p>
<p>Either I have to grab my bluetooth keyboard, or use the Windows on-screen keyboard. Both options are inconvenient in a living room. BUT, what if I could open a website (that&rsquo;s already in my bookmarks), a QR Code shows up, I scan it with my smartphone and from there I can type or paste whatever is in my mobile clipboard and transfer it to the mini-PC clipboard instantly? That&rsquo;s the 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>Then I open the camera on my phone, scan that QR Code and it opens this page:</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>Done. Now I can type anything or paste the password from my password manager (by clicking &ldquo;paste&rdquo;) and when I send it, on the TV I&rsquo;ll see this:</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>It shows that it was received, and it goes straight to the TV clipboard automatically. It shows it &ldquo;blurred&rdquo; because if it&rsquo;s a password I don&rsquo;t want it legible in large text on the TV - and yes, the mouse cursor is gigantic because it&rsquo;s easier for me to see on the TV.</p>
<p>This is useful not just for the TV, but for any situation where I want to send text from my phone to one of my PCs. A lot of people end up using WhatsApp or other messengers for this. I, for instance, have a &ldquo;Just me&rdquo; group that only has me in it, where I keep sending links and silly stuff from my phone. Then I always keep WhatsApp Web open in a tab on my PC and I can grab it from there. WhatsApp for me is a remote clipboard.</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 just me"  loading="lazy" /></p>
<p>But that&rsquo;s not safe for sending things like passwords, crypto addresses, or any other sensitive message. WhatsApp is a company with zero transparency. The advertising &ldquo;claims&rdquo; they have end-to-end encryption, but I don&rsquo;t believe it. It&rsquo;s equally likely they open everything in transit and keep it logged - it&rsquo;s data worth money, even more in this era of LLM training.</p>
<blockquote>
  <p>Recommendation: NEVER COMMUNICATE ANYTHING IMPORTANT VIA WHATSAPP.</p>

</blockquote>
<p>For file transfers I already use <a href="https://localsend.org/"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>That&rsquo;s where the inspiration came from: I wanted a LocalSend for Clipboard. I&rsquo;m sure there are already dozens of apps that do exactly this. But I wanted to take the opportunity to do this exercise.</p>
<h2>The Plan: 100% Vibe Code<span class="hx:absolute hx:-mt-20" id="the-plan-100-vibe-code"></span>
    <a href="#the-plan-100-vibe-code" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Honestly, I know enough about front-end, I started my career as a &ldquo;webmaster&rdquo; in the 90s. But I hate front-end. It&rsquo;s slow, it&rsquo;s tedious, yuck. In particular, I hate dealing with CSS. Even more particularly, I take no pleasure in writing Javascript. So I&rsquo;m always lazy about starting a web app from scratch. But with Vibe Coding, this is one of the parts I can finally get rid of, especially if I don&rsquo;t care about the exact look as long as it&rsquo;s not crooked.</p>
<p>While we&rsquo;re at it, I&rsquo;m going to use a language I know the basics of but don&rsquo;t have enough experience in, much less fluency (meaning, on my own I&rsquo;d be slow): <strong>GoLang</strong>. Why Go:</p>
<ul>
<li>it compiles to a native binary: I don&rsquo;t even need the performance, but having a compiler helps the LLM get feedback on obvious errors.</li>
<li>no dependencies: later I&rsquo;ll want to deploy it to my home server, and it&rsquo;ll be infinitely easier to set up Docker with a dependency-free binary compared to something like Ruby or Python where you have to install a ton of stuff.</li>
<li>full standard tooling: besides the compiler it already has a formatter, linter, tests and everything else that also gives good feedback to the LLM</li>
<li>simple, short and unambiguous syntax. I like how Go is Python-like. I don&rsquo;t consider either of them &ldquo;pretty&rdquo; (aesthetically, I much prefer Swift, Kotlin or Ruby). But unlike Javascript and even Rust, it&rsquo;s much less verbose and much less ambiguous.</li>
<li>rich standard library. Unlike Javascript, you don&rsquo;t have to keep installing 500 external libs for basic things.</li>
<li>since I&rsquo;m going to use websockets, GoLang&rsquo;s channels will make my life easier. Its async mechanism is simpler, more robust and better than Node&rsquo;s reactor.</li>
<li>most importantly: <strong>because I want to</strong>. It&rsquo;s a personal project, that only I&rsquo;ll use, I pick whatever I feel like and that&rsquo;s that.</li>
</ul>
<p>By the way, since I&rsquo;m not fluent in Go, I&rsquo;ll probably let some bad practice or obvious anti-pattern slip through. If that happens, you Go senior reading this, don&rsquo;t hold back, comment below and explain what you think.</p>
<blockquote>
  <p>If you want to see the end result now, all the code is <a href="https://github.com/akitaonrails/tvclipboard"target="_blank" rel="noopener">on this GitHub</a>. I&rsquo;ll be linking snippets from it all the time.</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>Another goal is to test how good the famous <a href="https://z.ai/blog/glm-4.7"target="_blank" rel="noopener"><strong>GLM 4.7</strong></a> really is. I&rsquo;ve written posts and tweets about it before. I&rsquo;ve tried running the Flash version on my local PC (it&rsquo;s good, but VERY slow). Overall, my impression is that it has similar quality to Claude Opus, GPT Codex, Gemini, but it&rsquo;s WAY slower (at least 2x slower). Meaning if I had used Gemini or Claude, I&rsquo;d get the same result but probably in half the time, though at a higher token cost (GLM is &ldquo;cheaper&rdquo;).</p>
<blockquote>
  <p>Let&rsquo;s get to the <strong>TL;DR</strong>: it managed to implement the app the way I wanted, but it was SLOW. I spent an entire day, having lunch and dinner in front of the PC while Crush kept thinking, a total of more than <strong>10 HOURS</strong>. And, according to OpenRouter, I spent just over <strong>USD 40</strong> (over 200 reais). It was neither fast nor cheap, considering the app&rsquo;s scope. Reminder: it&rsquo;s simpler than a 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>In terms of size, even a simple app like this one resulted in <strong>more than 3,000 lines of code (LOC)</strong> of 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>And for Javascript it came out to <strong>more than 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>Oh right, out of curiosity, I added <code>loc.sh</code> and <code>loc-js.sh</code> scripts to do this counting.</p>
<h2>Phase 1: Making It &ldquo;Work&rdquo; - what does that mean?<span class="hx:absolute hx:-mt-20" id="phase-1-making-it-work---what-does-that-mean"></span>
    <a href="#phase-1-making-it-work---what-does-that-mean" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Contrary to what many amateurs might think, it&rsquo;s not enough to type a description of the application (like I did in the previous section) as a prompt, throw it at the LLM and it will automatically come out with a working app: <strong>IT WON&rsquo;T</strong>.</p>
<p>This is what I was saying 2 years ago and it&rsquo;s still true: it doesn&rsquo;t matter how much more LLMs evolve. They might get a bit faster, a bit more efficient, but the architecture is still the same since 2022: transformers are glorified text generators, with a random sampling factor that always embeds entropy into the process (it will never be exact, deterministic). There will ALWAYS be a need for human intervention (and a lot of it).</p>
<p>Now I&rsquo;ll demonstrate with a real example what that means.</p>
<p>In the end, I stopped this project after <strong>43 commits</strong>. I followed the process I always say is the best: small prompts, one feature at a time, verify that it works and is correct, have it git commit, then move on to the next feature or requirement.</p>
<blockquote>
  <p>On a back-of-the-envelope count, each commit took about half a dozen prompts on average. Let&rsquo;s say 43 commits x 6 prompts = nearly <strong>258 prompts</strong> over more than 12 hours. Some prompts were a simple sentence that got solved in 2 seconds. Most took minutes and multiple iterations from the agent (Crush).</p>

</blockquote>
<p>GLM 4.7&rsquo;s context window is average: around 200k tokens. Crush has a mechanism where, when usage gets well past 90%, it asks to summarize the context so far so it can reset and reload from that summary to keep going. But it still has bugs and sometimes crashes and loses context:</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>Then I have to recover the last prompt, sometimes type part of the interrupted context myself, so it can pick up where it stopped. It&rsquo;s tedious and you have to keep paying attention. This is a problem that Gemini suffers from less because its context window is MUCH larger (but attention in the middle degrades as it gets bigger, it&rsquo;s a trade-off).</p>
<blockquote>
  <p><em>&ldquo;Ah, Akita must be dumb. I built a Todo app and it didn&rsquo;t even get close to 250 prompts. He did it wrong.&rdquo;</em></p>

</blockquote>
<p>Now let&rsquo;s get to the part that separates the boys from the men. Indeed, to <em>&ldquo;make it work&rdquo;</em> you don&rsquo;t need all those prompts. Of the 43 commits that took 10+ hours, I already had an app that <em>&ldquo;worked&rdquo;</em> after 10 small commits, which took about 1 hour.</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>If you want to see the &ldquo;v0.1&rdquo; version, just do this:</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>This version has <strong>1,827 LOC</strong> of Go and <strong>255 LOC</strong> of Javascript. But the Go backend itself is only <strong>549 LOC</strong> vs <strong>1,431 LOC</strong> at the end. And that&rsquo;s because in this minimal version I had already added backend unit tests (1,278 LOC), which most beginners never bother with. So you could &ldquo;solve&rdquo; my problem in just over 800 LOC, less than 1 hour and half a dozen prompts.</p>
<p>The project structure looked like this. Save this image to compare with how it ends up at the end:</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>In this version it already opens what I call the &ldquo;host&rdquo; page, which is the page that would open on my TV, showing the QR Code to connect. It already has the &ldquo;client&rdquo; page, which is what the QR Code opens on my phone. And it can already paste text on the client, send it to the host and copy it to the host&rsquo;s clipboard. Pretty simple.</p>
<blockquote>
  <p>This is where 99% of people would stop and call it done.</p>

</blockquote>
<h2>Phase 2: Going beyond &ldquo;just working&rdquo;<span class="hx:absolute hx:-mt-20" id="phase-2-going-beyond-just-working"></span>
    <a href="#phase-2-going-beyond-just-working" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Let me repeat: this project is simpler than a todo list and was built exclusively for me alone to use, at home, on my local network. Anyone thinking <em>&ldquo;ah, all of this is unnecessary for this app&rdquo;</em> - <strong>I KNOW!</strong>.</p>
<p>The idea is to demonstrate the minimum you have to think about for a <strong>real project</strong>. If you&rsquo;re building a toy just for yourself alone, quality doesn&rsquo;t matter.</p>
<p>For Go seniors, or Javascript seniors (because I&rsquo;ll use it on the front), I know you&rsquo;ll think <em>&ldquo;ah, he should have used framework X or library Y&rdquo;</em> - <strong>I KNOW!!</strong> 😅 At the end there will be a section where I&rsquo;ll discuss some of those other options, wait before commenting!!</p>
<p>Back to the Pareto concept. I said 20% of the most important work costs 80% of the time, but that&rsquo;s just an order of magnitude. In my example, the whole project took more than 10 hours and &ldquo;to make it work&rdquo; took only 1 hour. So it&rsquo;s closer to 90/10 than 80/20.</p>
<blockquote>
  <p>What the hell cost 90% of the time if 10% &ldquo;already worked&rdquo;??</p>

</blockquote>
<h3>1. Token Management<span class="hx:absolute hx:-mt-20" id="1-token-management"></span>
    <a href="#1-token-management" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The first thing I think about when I see a QR Code is that the link inside it should be disposable and volatile. It&rsquo;s not a rule and obviously depends on the app. For my little app it&rsquo;s definitely &ldquo;overkill&rdquo; but I&rsquo;ll do it as an example. (I won&rsquo;t keep repeating <em>&ldquo;yes, for my app it wasn&rsquo;t needed&rdquo;</em>, this is the last time).</p>
<p>I don&rsquo;t want a visitor who uses my app at home to be able to bookmark the link and use it from outside (I&rsquo;ll expose my service on the internet to be able to use it from anywhere later). I want each use to be unique and to expire after a few minutes, requiring a new QR Code.</p>
<blockquote>
  <p>So I need some kind of token system that expires.</p>

</blockquote>
<p>This article is already going to be very long, so I won&rsquo;t keep copying the full code of every feature. I&rsquo;ll always point to the <a href="https://github.com/akitaonrails/tvclipboard/commit/05dd60290e1d2419855e2f79ed48326415641da8"target="_blank" rel="noopener">commit</a> where I implemented it so you can look it up later, and only show snippets for illustration.</p>
<p>Anyway, the goal will be to have a URL like <code>http://clip.example.com/?token=blablabla</code>. Nothing out of this world.</p>
<p>Yes, it could be a JWT, but my use is very simple so I had it built from scratch, and GLM 4.7 came up with a JSON structure with a random private key and a timestamp, which it decided to encrypt with AES (it really wasn&rsquo;t necessary) and encode in hexadecimal to be the token (it ends up hyper long, more than 100 characters).</p>
<p>It turned out to be a little monster like this:</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>I didn&rsquo;t like it but I decided to leave it because it&rsquo;s working. I can mess with it later - in fact, a lot of things were done &ldquo;out of order&rdquo; because I just did them as I remembered them. I didn&rsquo;t have a step-by-step plan written beforehand. I literally pulled it out of my head in real time.</p>
<blockquote>
  <p>The goal of this is to remember that Session Management is a concern every dev must have in mind in a real system.</p>

</blockquote>
<h3>2. First Refactor<span class="hx:absolute hx:-mt-20" id="2-first-refactor"></span>
    <a href="#2-first-refactor" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The main code was already over 500 LOC, the tests over 1000 LOC. That&rsquo;s too big to stay in a single file. So I asked GLM to refactor it into packages. This kind of thing I know Claude Code handles more easily, but GLM did well too. In the end it looked something like this:</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>
This is what we call <strong>&ldquo;Separation of Concerns&rdquo;</strong>. Each package has an isolated domain. One package to deal only with configuration options, another to handle only the HTTP server, another to handle only QR Code generation, and another to handle only token management. Each with unit tests in the same package.</p>
<p>Asking for tests from the start was a huge help to the LLM, because every so often it would run the test, get confused about why it wasn&rsquo;t working and the penny would drop that it needed to adjust the implementation. All automated feedback is important for the LLM to have context to work with.</p>
<p>And refactoring isn&rsquo;t something you do only once. It&rsquo;s a process that has to be done from time to time to consolidate and simplify the code, improving <strong>maintainability</strong>.</p>
<blockquote>
  <p>The goal here is to remember that &ldquo;just because it works&rdquo; doesn&rsquo;t mean the code is frozen and you never touch it again.</p>

</blockquote>
<h3>3. Docker Support<span class="hx:absolute hx:-mt-20" id="3-docker-support"></span>
    <a href="#3-docker-support" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Since I was going to run it on my home server, Docker support is mandatory. But regardless of that, it&rsquo;s good practice to keep a <a href="https://github.com/akitaonrails/tvclipboard/blob/master/Dockerfile"target="_blank" rel="noopener"><code>Dockerfile</code></a> that works. It&rsquo;s another kind of &ldquo;automated test&rdquo;: see if you can &ldquo;build&rdquo; an image and run it. And document it for another user so they don&rsquo;t need to install anything on their system to run it, just run 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>In fact, the command above works, in case you want to test it right now. I uploaded the image to my Docker Hub account.</p>
<p>And if you hadn&rsquo;t already implemented it this way, this also forces you to add configuration options as environment variables (like <code>TVCLIPBOARD_SESSION_TIMEOUT</code>) or via command-line arguments (like <code>--expires</code>), exposing what is configurable.</p>
<blockquote>
  <p>The goal here is to remember how important it is to automate builds and document how to use it for other people.</p>

</blockquote>
<h3>4. Organizing Javascript and adding Cache Busting<span class="hx:absolute hx:-mt-20" id="4-organizing-javascript-and-adding-cache-busting"></span>
    <a href="#4-organizing-javascript-and-adding-cache-busting" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>To &ldquo;just work&rdquo;, way back at the beginning, all you had to do was make an &ldquo;index.html&rdquo; and go writing <code>if host</code> or <code>if client</code> in the middle of the HTML and JS. But at least GLM started out by duplicating a <code>host.html</code> and a <code>client.html</code>. But the javascript was all inline between <code>&lt;script&gt;...&lt;/script&gt;</code> tags and duplicated in both files, so another refactoring step was to ask it to separate <code>common.js</code>, <code>client.js</code>, <code>host.js</code>.</p>
<p>This is the bare minimum of maintainability. Again: refactoring doesn&rsquo;t end in a single step.</p>
<p>But I could run into a problem. Being a very simple little system, I had no <strong>ETAGs</strong> implemented and no asset pipeline like WebPack or similar.</p>
<p>In a dynamic web framework, most will support generating CSS and JS files dynamically, changing their names to things like <code>/assets/host-20260128123456.js</code>, and every time we edit some JS, the pipeline regenerates that file with a different name.</p>
<p>When we open it in a web browser, the browser always tries to &ldquo;cache&rdquo; static assets like images, JS, CSS, so it doesn&rsquo;t keep asking for the same thing all the time. If it already downloaded <code>host.js</code>, on the next &ldquo;reload&rdquo;, if the same file is requested, it just pulls from the cache instead of asking the server again.</p>
<p>The problem with that is if you change the content of the file and keep the same name. The browser will think it&rsquo;s the same thing and you&rsquo;ll spend hours scratching your head thinking <em>&ldquo;damn it, I already fixed that, why isn&rsquo;t it showing up??&rdquo;</em> - and it might be cache.</p>
<p>To avoid this, there are <strong>Cache Busting</strong> techniques, like ETAG headers. But the most common is just to add some kind of timestamp indicator in the URL, for example <code>/static/js/host.js?v=20260128120431</code>. This way, every time my server restarts, the timestamp will change (it&rsquo;s not the best rule, but for now it works). The URL will be different and the browser will be forced to download the newest version, making sure I don&rsquo;t waste time hunting down cache issues.</p>
<p>Again, any minimally competent web framework with HTML template support will have this, but I asked to do it from scratch:</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>
Since I serve static HTML pages, my server code needs to modify the URLs before sending them to the browser, and that&rsquo;s what the commit above does.</p>
<p>While writing this post, I noticed it added cache busting for &ldquo;.js&rdquo; files but forgot about &ldquo;style.css&rdquo;, which was left without it. I didn&rsquo;t notice because I barely touched the original CSS it made. But again, to show that LLMs are never perfect and you have to keep constantly checking if it didn&rsquo;t forget anything or leave anything behind. I had it fix this <a href="https://github.com/akitaonrails/tvclipboard/commit/2338999c505f1466c4913f41c8c7cbea72b369c9"target="_blank" rel="noopener">in this last commit</a></p>
<blockquote>
  <p>The goal here is to remember that you can&rsquo;t just drop files on the server without knowing how they&rsquo;re being served. Every front-end dev needs to know how a browser works. &ldquo;Knowing HTML&rdquo; isn&rsquo;t enough.</p>

</blockquote>
<h3>5. Adding Command-Line Arguments and doing more cleanup<span class="hx:absolute hx:-mt-20" id="5-adding-command-line-arguments-and-doing-more-cleanup"></span>
    <a href="#5-adding-command-line-arguments-and-doing-more-cleanup" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The <a href="https://github.com/akitaonrails/tvclipboard/commit/278604425edf52ae682a0e11e2952780f2783fd7"target="_blank" rel="noopener">next</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/59de9fb135c4b9e0f38343f7296527f0b1c5a31c"target="_blank" rel="noopener">three</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/ce0c25f37284f923568417c2543c66923fe449ec"target="_blank" rel="noopener">commits</a> are nonsense I should have squashed into a single 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="cleanup commits"  loading="lazy" /></p>
<p>This is more of a reminder that it&rsquo;s not entirely bad to let an agent like Crush do git commits automatically, but you need to pay attention because you might have to tell it to undo (git has ways to redo commits, with options like <code>-amend</code>).</p>
<p>I just added command-line options. Now the binary has this support:</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>And since it forgot to document it in the README, the other commit is for that. This is why I said the right thing to do was to squash into a single commit, because they&rsquo;re the same task.</p>
<h3>6. Dev vs Production environment configuration<span class="hx:absolute hx:-mt-20" id="6-dev-vs-production-environment-configuration"></span>
    <a href="#6-dev-vs-production-environment-configuration" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is another point every beginner ignores. The classic <em>&ldquo;ah, but it works on my machine&rdquo;</em>.</p>
<p>On the web, especially, &ldquo;your machine&rdquo; is always going to be a <code>localhost</code>, or <code>127.0.0.1</code> or <code>0.0.0.0</code> (watch my <a href="https://www.youtube.com/watch?v=0TndL-Nh6Ok&amp;list=PLdsnXVqbHDUcTGjNZuRYCVj3AZtdt6oG7"target="_blank" rel="noopener">Networking playlist</a> to understand the difference).</p>
<p>The problem: if I run this app locally on my PC, I access it via <code>http://localhost:3333</code>. When the QR Code gets generated, it&rsquo;ll use &ldquo;localhost&rdquo;. Now imagine your phone trying to load a &ldquo;localhost&rdquo; link. <strong>Obviously, it won&rsquo;t work.</strong> Because your phone doesn&rsquo;t have the app running on that port on localhost. I need my PC&rsquo;s IP. So the first thing I have to ask to fix is that, if it&rsquo;s localhost, it should look up my PC&rsquo;s IP - which will be something like <code>192.168.0.xxx</code>. With my PC and my phone on the same DHCP, on the same network, they can find each other via that private IP.</p>
<p>But what about when I run it in Docker? The internal IP of a Docker container is different from the external IP (the container is always either on NAT or on Bridge). The container&rsquo;s &ldquo;external&rdquo; IP will be something like <code>192.168.xxx</code>, but the internal IP will be something like <code>172.xxx</code>. Again, if the QR Code uses the internal IP, there will be problems.</p>
<p>So I need a new configuration, usually called <strong>PUBLIC URL</strong> or <strong>PUBLIC HOST</strong> or just <strong>HOST</strong>. Where I <strong>explicitly declare it</strong>.</p>
<p>For example, when I&rsquo;m done, as I said, I&rsquo;ll leave my service exposed on the internet using a real personal domain. Something like <code>https://clip.example.com</code> with TLS support and all. So I need my <code>docker-compose.yml</code> to have an option like:</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>And that way the QR Code is generated with this name instead of having to guess. But at the same time, if nothing is passed, it assumes &ldquo;localhost&rdquo;.</p>
<p>In this <a href="https://github.com/akitaonrails/tvclipboard/commit/b129e728101fd9fca400d48c0082b2f3dd679cf3"target="_blank" rel="noopener">first commit</a> it doesn&rsquo;t yet do all of this - I built it little by little. But that&rsquo;s how you start this kind of feature.</p>
<blockquote>
  <p>The goal here is to remember that &ldquo;running on my machine&rdquo; (localhost) means nothing. We don&rsquo;t put &ldquo;your machine&rdquo; into production. &ldquo;Working&rdquo; means &ldquo;working OUTSIDE your machine&rdquo;.</p>

</blockquote>
<h3>7. More code cleanup<span class="hx:absolute hx:-mt-20" id="7-more-code-cleanup"></span>
    <a href="#7-more-code-cleanup" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I&rsquo;m deliberately mentioning &ldquo;code cleanup&rdquo; all the time so it&rsquo;s very clear that a considerable part of the work of programming is &ldquo;cleaning up&rdquo; code that has already been written.</p>
<p>The <a href="https://github.com/akitaonrails/tvclipboard/commit/c9505acd840bbcbead456e3823ecd4a8d6e022a6"target="_blank" rel="noopener">next</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/67c5a01f4791257c8f4b19388c9c39bf1ccabb33"target="_blank" rel="noopener">four</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">are</a> just maintenance.</p>
<p>For example, the LLM inadvertently left a duplicated snippet inside the code. It didn&rsquo;t break anything because it was in the Docker Compose configuration:</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>
On top of that, there&rsquo;s an <strong>accidental</strong> behavior in the way this app works that I decided to keep as a &ldquo;feature&rdquo;. There always has to be a browser that opens first and automatically becomes the Host and displays a QR Code. The next browser to open needs the link from that QR Code and becomes the &ldquo;client&rdquo;.</p>
<p>But when the host closes, the client loses the connection and is automatically <strong>promoted</strong> to host. I didn&rsquo;t ask for that but it ended up working out, so I <a href="https://github.com/akitaonrails/tvclipboard/commit/63d7fbfaca798dec1eca48ff214561e01b22e670"target="_blank" rel="noopener">asked to keep it and document it</a>.</p>
<blockquote>
  <p>The goal here is to remember that LLMs aren&rsquo;t perfect, they&rsquo;ll leave leftovers behind, and you need to keep monitoring all the time.</p>

</blockquote>
<h3>8. Optimizing Javascript<span class="hx:absolute hx:-mt-20" id="8-optimizing-javascript"></span>
    <a href="#8-optimizing-javascript" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The <a href="https://github.com/akitaonrails/tvclipboard/commit/8db01bb053f8e045708b60293f569655a4cf2311"target="_blank" rel="noopener">next</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/8f7080cfce86257d5fc0e594f8098c115858a963"target="_blank" rel="noopener">three</a> <a href="https://github.com/akitaonrails/tvclipboard/commit/97df6c5b125de522c4ca65d276df353a97670ae6"target="_blank" rel="noopener">commits</a> are about cleaning up the Javascript.</p>
<p>Last time we touched it, I had only asked it to pull the inline Javascript out of the HTML and separate it into reusable &ldquo;.js&rdquo; files. But the time came to take a look at them.</p>
<p>Right off the bat, I could see there was duplication between client and host, so I asked <a href="https://github.com/akitaonrails/tvclipboard/commit/8db01bb053f8e045708b60293f569655a4cf2311"target="_blank" rel="noopener">to deduplicate</a> and move things into &ldquo;common.js&rdquo;.</p>
<p>On top of that there were several small issues:</p>
<ul>
<li>a bunch of variables and functions in the global scope (global pollution: bad practice)</li>
<li>websockets that open but with nothing checking the close</li>
<li>timers being started but never cleared (leaks)</li>
</ul>
<p>Javascript code that &ldquo;just works&rdquo; is always like this: everything global, everything open, nothing checked, multiple leaks. That&rsquo;s normal and every LLM will do it this way.</p>
<p>I also don&rsquo;t think you need to start from the beginning demanding SOLID and everything else. First make it work, then make it work properly, finally make it work fast. It&rsquo;s always in that order, especially in a simple scope like this.</p>
<p>That&rsquo;s why now it&rsquo;s time to ask it to refactor. And the first thing to do is always to isolate the scope, for example, in a block like this:</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>The problem with this is that every function the HTML was calling, like &ldquo;onclick&rdquo;, will fail because there&rsquo;s nothing in the global scope anymore. Once everything is hidden, now you have to <a href="https://github.com/akitaonrails/tvclipboard/commit/97df6c5b125de522c4ca65d276df353a97670ae7"target="_blank" rel="noopener">expose</a> only what actually needs to be public.</p>
<p>This is called <strong>IIFE Encapsulation</strong> (<em>Immediately Invoked Function Expression</em>). There are thousands of practices like this that Javascript needs to get &ldquo;cleaner&rdquo;. I know, I should be using modules, Typescript, blah blah, but this is a simple example.</p>
<blockquote>
  <p>The goal here is to remember that, just because it &ldquo;works&rdquo; doesn&rsquo;t mean it&rsquo;s &ldquo;good&rdquo;, especially with Javascript.</p>

</blockquote>
<h3>9. Optimizing the Golang<span class="hx:absolute hx:-mt-20" id="9-optimizing-the-golang"></span>
    <a href="#9-optimizing-the-golang" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is the longest section. The Golang code already &ldquo;works&rdquo;, but it&rsquo;s not good yet. I know it has race conditions in channels, I know the error handling is bad, I know there are still micro security issues, so I want to fix what I can.</p>
<p>The next 6 commits deal with:</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>GoLang itself has a tool to check for <strong>race conditions</strong>, the <a href="https://go.dev/doc/articles/race_detector"target="_blank" rel="noopener"><strong>Data Race Detector</strong></a>. Just pass the <code>--race</code> option to commands like <code>go build</code> or <code>go test</code>. And luckily GLM was smart and immediately used this tool when I asked it to check for these issues.</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> happen when two asynchronous tasks try to modify the same data structure, for example. One way to &ldquo;fix&rdquo; them is to put Locks or Mutexes on structures that can only be modified synchronously. Any application with anything asynchronous will eventually have race condition issues. It&rsquo;s not &ldquo;maybe&rdquo;, it&rsquo;s &ldquo;when&rdquo;.</p>
<p>A server has to be &ldquo;well-behaved&rdquo;, it can&rsquo;t just &ldquo;crash&rdquo; or &ldquo;panic&rdquo;. But the first &ldquo;working&rdquo; code GLM wrote had several snippets like this:</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>This is wrong. Besides that, we need the server to correctly accept system signals like SIGINT, SIGTERM, etc. This is when we hit &ldquo;Ctrl-C&rdquo;, for example, or when Docker tells the container to shut down. It won&rsquo;t simply crash the program, first it asks the program to shut itself down, giving it a chance to do cleanup, like flushing files, closing ports and things like that.</p>
<p>This is called <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>I didn&rsquo;t stop to check how right GLM was but according to itself, some of the issues it itself found in the code it itself wrote included things like:</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>As you can see, just because it &ldquo;compiles&rdquo; doesn&rsquo;t mean it&rsquo;s ok. Just because it &ldquo;runs&rdquo; doesn&rsquo;t mean it&rsquo;s ok. Even asking it to check <strong>MULTIPLE TIMES</strong>, the LLM doesn&rsquo;t catch all the errors in one go. Each of these commits took hours to resolve and I had to intervene multiple times, and even so I&rsquo;m still not sure it caught everything (I&rsquo;d have to review more carefully - but I wanted to publish this article quickly 😂).</p>
<h3>10. Adding Security Systems<span class="hx:absolute hx:-mt-20" id="10-adding-security-systems"></span>
    <a href="#10-adding-security-systems" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Again, none of this is necessary in a system that only I will use on my local network, but you as a developer have to take responsibility.</p>
<blockquote>
  <p>ALWAYS assume every user of your system is MALICIOUS! That&rsquo;s the RULE, not the exception.</p>

</blockquote>
<p>A silly example: the user can paste and transmit any text over websockets to my server. The 3 most obvious things that immediately crossed my mind were:</p>
<p><strong>a) When the host receives the text, it renders it into the HTML in the &ldquo;Received&rdquo; box</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" />
What if the user sends a <code>&lt;script&gt;....&lt;/script&gt;</code>? That will render into the HTML and the browser will EXECUTE it. That&rsquo;s how <strong>Code Injection</strong> works. That&rsquo;s how <strong>XSS</strong> (<em>Cross-Site Scripting</em>) works. This was very common in old forums or comment systems. To avoid it you must <strong>SANITIZE</strong> everything coming from the user.</p>
<p>Luckily, the way GLM did it is already sanitized because it writes the content with <code>textContent</code> instead of <code>innerHTML</code>, so the text is written literally without being executed:</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" />
It happened to get this right on the first try, but you need to check because it could have done it wrong.</p>
<p><strong>b) There was no message size check</strong></p>
<p>A user should never be able to send text of any arbitrary size. They can always try to POST a gigabyte file directly to see if it blows up some <strong>Overflow</strong> error on the server, exposing some security exploit.</p>
<p>EVERY user field needs limits and it needs to be checked not only on the client/front-end, but more importantly, on the 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) You have to reject Spammers!!</strong></p>
<p>Again, you can&rsquo;t assume everyone will use your web app nicely. The rule is to expect everyone to try to take it down or break in. And one way to do that is to write a script to keep making thousands of POSTs per second to try some kind of <strong>Denial of Service</strong>, where the server queues up so many requests it blows up.</p>
<p>The right thing is to configure this in another service that sits in front of yours, like <a href="https://www.cloudflare.com/ddos/"target="_blank" rel="noopener"><strong>Cloudflare</strong></a>.</p>
<p>But it doesn&rsquo;t hurt to implement a <em>fallback</em> in your application itself. A way to configure how many requests a single user can make at a time (e.g. 4 requests per second - no one is going to paste text that many times per second in an app like this).</p>
<p>If they try more than that, the app has to immediately <strong>REJECT</strong> and drop the connection, without wasting time processing any more, freeing resources for other real requests. This is called <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) Another issue beginners don&rsquo;t stop to think about is CORS and Origin Check.</strong></p>
<p>As I said before, we have Javascripts like <code>/static/js/common.js</code>. It&rsquo;s not a huge problem if someone else uses them because they don&rsquo;t have any big secrets in them. But at the very least I don&rsquo;t want some site like &ldquo;<a href="http://malware.pk/index.html"target="_blank" rel="noopener">http://malware.pk/index.html</a>&rdquo; to include <code>https://clip.example.com/static/js/common.js</code> inside it and for me to end up paying for its traffic.</p>
<p>I want only the <strong>SAME ORIGIN</strong>, in this case &ldquo;clip.example.com&rdquo;, to be able to load files from the same server. No one from outside is welcome and that&rsquo;s the <strong>BEST PRACTICE</strong>. Study <strong>CORS</strong> (Cross-Origin Resource Sharing). This is more important if your system spans multiple domains where one depends on the other, then you want - in a controlled way - to allow only certain other domains that are under your control.</p>
<blockquote>
  <p>The rule for everything on networks is <strong>DENY ALL</strong> and then <strong>ALLOW [specific]</strong>. It should NEVER be <strong>ALLOW ALL</strong>.</p>

</blockquote>
<p>Implementing this took a while, because GLM kept tying itself in knots over how to allow &ldquo;localhost&rdquo;, but also &ldquo;192.168.xxx:3333&rdquo;, but also &ldquo;clip.example.com&rdquo; (via port 443), etc. It&rsquo;s not hard, GLM just got itself all tangled up and I had to intervene several times with prompts to steer it in the right direction. It ended up taking multiple commits for it to get it right.</p>
<p><strong>e) Another little thing beginners don&rsquo;t think about: browser resource PERMISSIONS.</strong></p>
<p>Every time you load this system on &ldquo;localhost&rdquo; and it receives a message to drop into the clipboard, this alert pops up asking for permission:</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>And that&rsquo;s normal and correct: nothing should have access to things outside the browser. That&rsquo;s why the Javascript asks <em>&ldquo;send this to the clipboard&rdquo;</em>, the browser intercepts it, checks the permission and, if it doesn&rsquo;t have it, asks for it. Every time it will ask. But if you use a real domain (e.g. clip.example.com), it asks once and then remembers per domain. That&rsquo;s one of the reasons I&rsquo;m going to expose it on the internet: to have a real domain.</p>
<blockquote>
  <p>Exposing services on the internet is DANGEROUS. That&rsquo;s why I wrote <a href="https://akitaonrails.com/en/2025/09/09/accessing-my-home-server-with-a-real-domain/"target="_blank" rel="noopener">this article explaining how to use real domains</a> and <a href="https://akitaonrails.com/en/2025/09/10/protecting-your-home-server-with-cloudflare-zero-trust/"target="_blank" rel="noopener">this other article about using Cloudflare Zero Trust</a>.</p>

</blockquote>
<p><strong>f) There was another security factor which is &ldquo;overkill&rdquo; but I wanted to implement anyway.</strong></p>
<p>Let&rsquo;s say I want to run only on localhost. Let&rsquo;s say it&rsquo;s a private but shared network (guests, roommate, office, etc). Running on localhost there&rsquo;s no TLS, meaning HTTP packets travel in plain text.</p>
<p>Anyone with <a href="https://www.wireshark.org/"target="_blank" rel="noopener"><strong>Wireshark</strong></a> can &ldquo;sniff&rdquo; the packets on the network and intercept mine. And if I happen to be passing sensitive data, like a password, it will be visible in the clear.</p>
<p>The right thing is to run with TLS (HTTPS). But just as a &ldquo;stopgap&rdquo;, I asked GLM to implement a simple obfuscation (it&rsquo;s not a fully secure process, but it uses AES-GCM), which is enough to not be obvious with a sniffer. For this it uses the <a href="https://w3c.github.io/webcrypto/"target="_blank" rel="noopener"><strong>Web Crypto API</strong></a>.</p>
<p>But there&rsquo;s a catch: Web Crypto API is only available if the site opens over HTTPS or if it&rsquo;s localhost. But if I try <code>http://192.168.xxx:3333</code>, it won&rsquo;t be available. For that there&rsquo;s <a href="https://github.com/akitaonrails/tvclipboard/commit/5dd813d9ca58324a12b8e6ebf8fcaf8c7be99e9b"target="_blank" rel="noopener">this commit</a> that deals with this and documents it.</p>
<blockquote>
  <p>The goal here is to show that security, optimization, and maintainability aspects are much more than a beginner thinks. In fact, they&rsquo;ll never think about them, because you need to have studied beforehand to even know they exist.</p>

</blockquote>
<h3>11. Open Source Licenses<span class="hx:absolute hx:-mt-20" id="11-open-source-licenses"></span>
    <a href="#11-open-source-licenses" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This topic alone would be material for a long article, but in practice I tend to think simply:</p>
<ul>
<li>if it&rsquo;s a project where I don&rsquo;t care whether anyone contributes or not, I just open it with PERMISSIVE licenses, like BSD, MIT</li>
<li>if it&rsquo;s a project where I&rsquo;d like contributions and that no one uses commercially, I just put a RESTRICTIVE license on it, like GPL</li>
<li>if it&rsquo;s restrictive and it&rsquo;s an app that could turn into a SaaS (so the code isn&rsquo;t distributed, so there would be no obligation to contribute), then I add AGPL (Affero)</li>
</ul>
<p>That&rsquo;s what I did here: I licensed it under <a href="https://github.com/akitaonrails/tvclipboard/blob/master/LICENSE"target="_blank" rel="noopener">AGPL 3.0</a>.</p>
<p>The warning is to not let LLMs write the license file. Always take texts on laws, regulations and rules from the official site. The LLM always produces a shortened or incomplete or even wrong version. Legal documents need to be EXACT, they can&rsquo;t have wrong words and ambiguous meanings or incomplete statements.</p>
<blockquote>
  <p>LLMs are not good at producing correct legal text on the first try, there will always be errors. Always assume such text is wrong: there&rsquo;s no &ldquo;compiler&rdquo; for laws.</p>

</blockquote>
<h3>12. Fixing the problem of overly long tokens<span class="hx:absolute hx:-mt-20" id="12-fixing-the-problem-of-overly-long-tokens"></span>
    <a href="#12-fixing-the-problem-of-overly-long-tokens" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Way back I said that the tokens GLM generated for me are too big (over 100 characters). This results in super gigantic links.</p>
<p>It&rsquo;s not a problem because no one has to type this and it&rsquo;s not meant to be bookmarked. This is more of a personal choice: I like short tokens in URLs.</p>
<p>The advantage of the long token is that I don&rsquo;t need to implement any kind of &ldquo;database&rdquo; (key-value store, or KV Store - Memcached, Redis). That&rsquo;s because all the session info (ID and expiration timestamp) is encoded inside that token, which is why it&rsquo;s long. It&rsquo;s a json struct, encrypted with AES, converted to Hex.</p>
<p>Trying to keep the same concept, <a href="https://github.com/akitaonrails/tvclipboard/commit/eb274968d8ec83c28abce7ffd7352ad6eebd86bc"target="_blank" rel="noopener">in this first commit</a> I only asked it to try to optimize the size of the structure. So GLM on its own chose to try to change from UUID (36 chars) to a random 24-char Hex ID. Swap standard ISO timestamp (24 chars) for UNIX (10 chars). This went from 130-140 chars down to around 112 chars. A reduction on the order of 20% to 25%.</p>
<p>It wasn&rsquo;t a bad attempt. But in the end I wasn&rsquo;t really swallowing tokens that big (it could be a cookie too, I know, but I wanted to keep everything in the URL).</p>
<p>In the end I decided I wanted really short tokens (8 chars, base62) and so I&rsquo;d need to keep the Session in some &ldquo;storage&rdquo;. I decided it would be &ldquo;in-memory&rdquo; (process-affinity, but I&rsquo;m only going to run 1 instance, no load balancer, so it&rsquo;s fine). And that would be a FIFO with rotation (older keys get discarded to make room for newer ones, and they expire that way).</p>
<p><a href="https://github.com/akitaonrails/tvclipboard/commit/a34b265b585a5661c2595fe474b4613a5aba13c0"target="_blank" rel="noopener">This is the commit for that feature</a> and I still had to intervene several times. For some reason it thought it would be a good idea to have a background goroutine cleaning up expired tokens from this storage. But that&rsquo;s completely unnecessary if it&rsquo;s a FIFO. It did it anyway, I had to tell it to undo. That&rsquo;s the routine.</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. More refactoring and cleanup (almost done!)<span class="hx:absolute hx:-mt-20" id="13-more-refactoring-and-cleanup-almost-done"></span>
    <a href="#13-more-refactoring-and-cleanup-almost-done" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The last <strong>6 commits</strong> are <strong>MORE MAINTENANCE!!</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>Sounds like a joke, but the amount of maintenance I did is actually small. In a real project this is part of the routine. Every time you add something new, you break something that was already there, and you need to adjust it to avoid accumulating <strong>technical debt</strong>.</p>
<p>Remember we ran the <strong>race condition</strong> check? We added new code, so it has to be checked again. And it found more issues fixed <a href="https://github.com/akitaonrails/tvclipboard/commit/df348dbe170451c38b9c9fdf703b57ce6c253578"target="_blank" rel="noopener">in this commit</a>. I don&rsquo;t remember now whether these were already there way back and it skipped them without fixing, or whether the last edits ended up creating these new bugs.</p>
<p>After adding so many things and refactoring, I decided to bump up Go code coverage a bit more. The <code>go test</code> command has the <code>-coverprofile</code> option to help check which parts of your code haven&rsquo;t been tested yet. So I asked GLM to add tests for new features like Rate Limiting, message size check, origin check and <a href="https://github.com/akitaonrails/tvclipboard/commit/fc32a575c53d61b7fce5ab941d069dd45d66fc81"target="_blank" rel="noopener">other things</a>.</p>
<p>I asked for a trivial thing which is a <a href="https://github.com/akitaonrails/tvclipboard/commit/2867f8df052f89febc095644ed28aaaefd0b198a"target="_blank" rel="noopener">Close button</a> on the client side, to not keep piling up tabs on my smartphone.</p>
<p>After I had it swap the token system, there&rsquo;s no more need to encrypt the timestamp structure. But it left behind unit tests about that. So I <a href="https://github.com/akitaonrails/tvclipboard/commit/9b5d9f5d3b6504788e73426cc3dc2c6a2a9fe358"target="_blank" rel="noopener">told it to remove them</a>. Again: LLMs will leave things incomplete behind. A lot of times it doesn&rsquo;t know whether to keep code so as not to break the test, or to drop the test because the code is no longer needed.</p>
<p>Then I asked Go to run the <a href="https://github.com/akitaonrails/tvclipboard/commit/1b0e2efd0dfaedf96b2c54eea62438cc7b081924"target="_blank" rel="noopener">linter</a> and support <a href="https://github.com/akitaonrails/tvclipboard/commit/128cf14feff3f1d36a2568c9af8b9665c6b6377a"target="_blank" rel="noopener">golangci-lint</a> for last-pass checks. A Linter is a sort of formatting, syntax and good-practice check for a particular language. But most of the adjustments were extra whitespace, a few bad indentations and stuff like that. Nothing major.</p>
<p>One last thing that was my own negligence is that I was forgetting to add Javascript tests. A considerable part of the code is front-end dealing with WebSockets and everything. I had to add them, and that was <a href="https://github.com/akitaonrails/tvclipboard/commit/bb0d4895f1cfb5b62c0133930b017c93db5d60a8"target="_blank" rel="noopener">in this commit</a>.</p>
<p>Since I was doing it all in one go, I also asked it to add ESLint, and to prioritize testing everything only unit-wise, with mocks and stubs so everything is headless and doesn&rsquo;t need to load Chromium. Just test the publicly exposed functions. This raised front-end test coverage from zero to 73%, which is great coverage. See the <a href="https://github.com/akitaonrails/tvclipboard/commit/bb0d4895f1cfb5b62c0133930b017c93db5d60a8"target="_blank" rel="noopener">commit</a> to see how it turned out.</p>
<h3>14. Last thing: I18n<span class="hx:absolute hx:-mt-20" id="14-last-thing-i18n"></span>
    <a href="#14-last-thing-i18n" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is something I remembered while I was already writing this article. Completely unnecessary, but I wanted to see if GLM could do it. And <a href="https://github.com/akitaonrails/tvclipboard/commit/065c52046551b2d8ff426a7206366455e19638c4"target="_blank" rel="noopener">it &ldquo;did&rdquo;</a>. I didn&rsquo;t detail how I wanted it and I let it come up with the solution. I would have had the Go server swap the strings before sending the HTML to the client.</p>
<p>But GLM preferred to add this on the front-end side and swap the strings via Javascript. It&rsquo;s not wrong, but it&rsquo;s not the best way.</p>
<p>Language translation is exactly one of the things AIs were originally built to be good at: Natural Language Processing (NLP). I had it extract all the English strings and then make a Brazilian Portuguese version, and I can say it did it well:</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>In a real web app this is something we always need to worry about: not leaving user-facing messages &ldquo;hardcoded&rdquo; and always extracting and testing them. Which brings me to the last part:</p>
<h2>Wouldn&rsquo;t it have been better to use Framework X or Library Y?<span class="hx:absolute hx:-mt-20" id="wouldnt-it-have-been-better-to-use-framework-x-or-library-y"></span>
    <a href="#wouldnt-it-have-been-better-to-use-framework-x-or-library-y" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>The Go crowd might not like that I didn&rsquo;t use Gin, Fiber or another web framework. But really in this case the app is too simple. There are no more than 3 endpoints. The most complicated part would be the WebSockets, which wouldn&rsquo;t look much different using a framework. Or bigger checks like CORS, which also wouldn&rsquo;t have much less LOC.</p>
<p>On the Javascript side, maybe I18n would have been worth importing a ready-made package, but again, it&rsquo;s very simple. There was no major feature I had to reinvent.</p>
<p>Of course, this is because this app is miserably small, as I said at the beginning. For a real larger app, I would indeed recommend using a popular framework with good community support, well documented and so on. But things the size of a ToDo List really don&rsquo;t make a difference unless the exercise was about learning a particular framework.</p>
<p>Thinking about that, you have to be careful because no LLM will pick for you. If you start with small prompts, one feature at a time (which is the right way), it tends to write things all from scratch, without importing any library.</p>
<blockquote>
  <p>You have to know beforehand which library to ask for each thing!</p>

</blockquote>
<p>Again, a beginner wouldn&rsquo;t know, so they&rsquo;d end up reinventing the wheel dozens of times instead of using popular libraries.</p>
<p>Remember I said, way back at the beginning, to save the structure of the project in version <strong>v0.1</strong> that &ldquo;already worked&rdquo;? Here&rsquo;s how it looks at the end in a real project:</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>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>Phew! That was long! Congrats if you made it this far!</p>
<p>My experience on this little project was like having a <strong>super motivated intern</strong> that I needed to spend the <strong>whole day</strong> looking over the shoulder of, and every other minute saying:</p>
<p><em>&ldquo;Whoa! Stop everything, this is wrong!&rdquo;</em>
<em>&ldquo;Whoa! Undo it and do it again but this other way!&rdquo;</em>
<em>&ldquo;Whoa! No, you already did this wrong before and you&rsquo;re repeating it!&rdquo;</em></p>
<p>I&rsquo;m not kidding when I said it was more than 250 prompts. And that&rsquo;s a low estimate, I think I wrote MANY more than that.</p>
<p>As soon as I finished the Vibe Coding process, I pushed to GitHub and started writing this article, so there are still hours of <strong>REVIEW</strong> of the whole code, from beginning to end, to see if I missed anything. You couldn&rsquo;t put it in production without doing that! If anyone sees weird things in the code, feel free to post in the comments.</p>
<p>It really felt like I was mentoring an intern. But with one problem: unlike a real intern, the LLM has already forgotten everything I taught it.</p>
<p>When I do a new project with GLM, it will make practically ALL the same mistakes and mix-ups. It&rsquo;s like I was mentoring a blind intern with the memory of a goldfish.</p>
<p><strong>It&rsquo;s extremely exhausting!!</strong></p>
<blockquote>
  <p>In the end it took around 12 hours to code everything, and I have the impression that a senior Go developer would have done it on their own in half the time. A junior would have taken this long over 2 or 3 days (stopping to research).</p>

</blockquote>
<p>I already knew this, I&rsquo;ve repeated it dozens of times, but the notion of:</p>
<p><em>&ldquo;I don&rsquo;t need to hire programmers anymore, AI does everything by itself.&rdquo;</em> - is a <strong>GIANT FALLACY</strong>.</p>
<p>And it&rsquo;s not going to get much better than this. We&rsquo;re already at the top of the S curve I&rsquo;ve explained in every podcast I&rsquo;ve been on and in videos on my channel. It costs orders of magnitude more energy to train the next version than it took for all previous versions combined, for <strong>incremental</strong> progress.</p>
<p>While the architecture stays the same, it&rsquo;s not going to get past this. There needs to be some NEW BREAKTHROUGH (which hasn&rsquo;t happened to date, since 2021 when Transformers came out).</p>
<p>A Claude Code will be a bit faster than GLM. The next version of GLM will be a bit faster. Maybe the 12 hours I spent will turn into 6 hours (for this miserable app). But the <strong>PROCESS</strong> will be the same: it does it wrong, I tell it to fix it, and it does this <em>ad eternum</em>.</p>
<p>There will never be such a thing as writing a single prompt and out comes a perfect, secure, optimized app. It will ALWAYS come out buggy on the first try. That&rsquo;s why someone who is not a programmer should not build software to put in production with only Vibe Coding: it <strong>must</strong> go through review by a real experienced programmer.</p>
<p>This is not optional.</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>This section was written the day after publishing the original article. Once I was done, I asked <strong>Claude Code</strong> to do a deep quality analysis, focused on security and tests. And it found <strong>SEVERAL</strong> errors. Remember when I said you couldn&rsquo;t put it in production without reviewing carefully first?</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">This is the commit</a> with the fixes, take a look.</p>
<p>So it &ldquo;already worked&rdquo; in Phase 1, with a ton of problems. I intervened and spent 90% of the time in Phase 2 adding the foundation. I didn&rsquo;t review it properly and I published the file, now I came back and there are still problems.</p>
<blockquote>
  <p>Software is NEVER &ldquo;done&rdquo;.</p>

</blockquote>
<p>Even what Claude Code found doesn&rsquo;t mean it fixes EVERYTHING. It means it fixes SOME THINGS. No software ever has &ldquo;a guarantee&rdquo; of being perfect. That doesn&rsquo;t exist.</p>
<p>Even if I had started with Claude Code, as I said before, it would indeed have been faster. But it all depends on what you ask it. If you don&rsquo;t know exactly the requirements and specs for security, or performance, or scalability, or maintainability BEFORE asking, it will do SOME THINGS. Not necessarily the PERFECT THINGS, just SOME THINGS.</p>
<p>Then someone might say <em>&ldquo;See? Now that you&rsquo;ve reviewed it with Claude Code, it&rsquo;ll be perfect&rdquo;</em></p>
<p>So, afterwards I also asked <a href="https://github.com/akitaonrails/tvclipboard/commit/78dcfb11e58d026e2bc34476d849561724879fa8"target="_blank" rel="noopener"><strong>GPT 5.2 Codex</strong></a> to do the same extensive analysis and guess what: it also found more little issues. Far fewer, of course. But look at it: after GLM 4.7 and Opus 4.5, GPT 5.2 still found more things. IT NEVER ENDS!</p>
<p>In particular, remember how earlier I had tokens encrypted, then I changed the requirement asking for them not to be and to stay in-memory on the server? Old code from that feature was left behind. GPT 5.2 thought the intention was to finish implementing the encryption and started putting it back. I had to interrupt it and tell it to stop and remove it. Not even Opus 4.5 had removed this <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>Always, you who ask are the one responsible for asking exactly and checking afterwards.</p>
]]></content:encoded><category>vibe-coding</category><category>software-engineering</category></item><item><title>RANT - LLMs are LOOT BOXES!</title><link>https://akitaonrails.github.io/en/2025/05/02/rant-llms-are-loot-boxes/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2025/05/02/rant-llms-are-loot-boxes/</guid><pubDate>Fri, 02 May 2025 19:20:00 -0300</pubDate><description>&lt;p&gt;I know this is going to sound way too negative, and once again, let me repeat that I am not anti-AI, anti-LLM, or anything like that. Quite the opposite: I like it SO, SO much that I have been dedicating ALL my hours to researching it in depth.&lt;/p&gt;
&lt;p&gt;It has been very useful to me, especially for summarizing papers, reconciling research across multiple sites, summarizing topics, and things like that.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I know this is going to sound way too negative, and once again, let me repeat that I am not anti-AI, anti-LLM, or anything like that. Quite the opposite: I like it SO, SO much that I have been dedicating ALL my hours to researching it in depth.</p>
<p>It has been very useful to me, especially for summarizing papers, reconciling research across multiple sites, summarizing topics, and things like that.</p>
<p><strong>&ldquo;BUT ..&rdquo;</strong></p>
<p>And it is a huge BUT, it has been PRETTY useless for real programming. I am not going to repeat every problem I have already run into: I detailed plenty of them in previous posts. But my conclusion is simple: it can help with code, on small, focused, isolated tasks, with a lot of prompting to help, and you - the programmer - need to be alert about what to pick and what to reject (and you will reject most of it).</p>
<p>Better prompts have limits. RAGs have limits. Loras have limits. Bigger context windows have limits. Using agents that hand back stacktraces and asking for deep thinking has limits. I have already hit EVERY one of these limits, and I have not found any &ldquo;workaround&rdquo; that can make it work 100% well 100% of the time - which is exactly what the marketing promises.</p>
<p><em>&ldquo;But Microsoft, Google, Meta, NVIDIA have already stated that 30% of their code is already written by AI and next year programmers will be replaced&rdquo;</em></p>
<p>That is the great half-truth, half-lie (interpret it however you want).</p>
<h2>&ldquo;What 30%??&rdquo;<span class="hx:absolute hx:-mt-20" id="what-30"></span>
    <a href="#what-30" class="subheading-anchor" aria-label="Permalink for this section"></a></h2><p>You, who are not a programmer, did you know that project documentation (which is extensive), language translation files (which are extensive, do you know how many languages a Microsoft or Google supports globally?), HTML pages that duplicate that extensive documentation, all of it is &ldquo;considered&rdquo; part of the code and sits in the same &ldquo;code&rdquo; repository like GitHub?</p>
<p>You, who are not a programmer, did you know that the VOLUME in characters/bytes of that material very often nearly matches, or can even exceed, the byte volume of the actual code itself?</p>
<p>That is just one small example that comes to mind. Launch pages for new products, the &ldquo;hot-sites&rdquo; which are DISPOSABLE sites built to be thrown away. That is also code volume. There is A LOT of &ldquo;code&rdquo; that is not &ldquo;THE code&rdquo;. Every programmer who works at a big company knows this.</p>
<p>Your little home project has no documentation, no automated tests, no build scripts, no deploy or packaging automation, no separate language files, no hot-site, no multi-language product site. It is just a &ldquo;hello.py&rdquo; and that is ALL of your code. I understand why you extrapolate that a big project is the same thing.</p>
<p>PART of the &ldquo;30%&rdquo; everyone keeps talking about is this. Sure, there is some real code in there, but nowhere near what you think.</p>
<h3>Conflict of Interest<span class="hx:absolute hx:-mt-20" id="conflict-of-interest"></span>
    <a href="#conflict-of-interest" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I am stunned. If a &ldquo;drug dealer&rdquo; gives you a free sample and you ask &ldquo;but is it safe?&rdquo;, what do you think he is going to answer?</p>
<p>The owner of Meta has AI products. The owner of Google, the owner of Microsoft, RENT OUT GPUs for AI. The owner of NVIDIA SELLS GPUs to AI datacenters. The owner of OpenAI, Anthropic, they all RENT that infrastructure and RESELL you <strong>TOKEN CREDITS</strong>.</p>
<p>What do you think their incentive is? <strong>SELLING TOKEN USAGE</strong>.</p>
<p>You saw what I reported in previous posts. In less than 1 week of testing I burned nearly USD 150 in credits. That would be more than 850 BRL in a week, and after running a HUNDRED attempts I got merely &ldquo;reasonable&rdquo; refactorings HALF of the time, and I got good passing unit tests LESS THAN HALF of the time (actually, almost NEVER). But I did spend tokens, LOTS OF TOKENS.</p>
<p>Features that the marketing tells you will improve things, like DEEP THINKING/REASONING, only &ldquo;sometimes&rdquo; give better code answers. They really are better at compiling papers or text research. But for code, in MOST attempts, I did not see that much difference. Except it took MUCH longer and burned MUCH more tokens. You know what Deep Thinking does? It adds more steps before the answer and stuffs the context with tokens. Those extra tokens: YOU PAY EXTRA FOR.</p>
<p>Features that the marketing also tells you will improve things, like RAG-based solutions, deep down are just more &ldquo;prompt injection&rdquo; - which always burns context and BURNS MORE TOKENS. It works, sure, but the incentive is to burn more tokens.</p>
<p>Features that the marketing is now trying to push on you, like MCP, agents, and LLM automation, combined with RAGs and all the rest. Devin, n8n, and everything else are just a more automated way to BURN MORE TOKENS.</p>
<p>Every incentive, from everyone, is to burn more tokens, not fewer. And in my tests, with many models (I will not say all of them), I got better code answers by <strong>TURNING OFF DEEP THINKING</strong>. You can configure that in tools like Aider or directly in the OpenAI API. Reduce <code>thinking_tokens</code> or set <code>reasoning_effort</code> to &ldquo;Low&rdquo; or, even better, &ldquo;NONE&rdquo;.</p>
<h3>LOOT BOXES<span class="hx:absolute hx:-mt-20" id="loot-boxes"></span>
    <a href="#loot-boxes" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I also explained in the <a href="https://www.akitaonrails.com/en/2025/04/29/dissecando-um-modelfile-de-ollama-ajustando-qwen3-pra-codigo"target="_blank" rel="noopener">article about how to build your own Ollama Modelfile</a> that, essentially, the text generation process (swallow it, LLMs are next-word generators) has random components. Temperature, Top_P, Top_K. More: the training has non-linear components (e.g. ReLU). It is not deterministic, it is a DRAWING from a probability distribution.</p>
<p>That means, fundamentally, there is no way for an LLM to be 100% right 100% of the time. It is mathematically impossible. It can always be &ldquo;almost right&rdquo; or &ldquo;looking like it is right&rdquo;. But if you diligently double-check everything, all the time, you are going to start finding small mistakes. In the case of code, it is a lot more obvious. It is VERY wrong, MANY times.</p>
<p>And the whole process looks a lot like those &ldquo;free&rdquo; online games with LOOT BOXES or GACHA systems, you know? Like a Genshin Impact and friends.</p>
<p><img src="https://new-uploads-akitaonrails.s3.us-east-2.amazonaws.com/my994es2wx2rj3mfkyu9i6h4s9ik" alt="Loot Box"  loading="lazy" /></p>
<p>What??? You are a boomer and have no idea what Loot Boxes are? &hellip;</p>
<p>I AM A BOOMER AND I KNOW, there is no excuse. Go get up to speed (and it is the main reason I choose not to play that kind of online game).</p>
<p>The economy of those games works like this: you do not have the option of buying the specific items you want. Instead you pay, say, 20 credits for the chance to take part in a drawing (which happens on pre-scheduled dates and times, not all the time). In that drawing you have a (HIGH) probability of only winning a 10-credit item OR an <strong>incredible</strong> (VERY LOW) probability of winning an exclusive 1000-credit item!!!!</p>
<p>Sounds like the deal of the century, right??? (LITERALLY IT IS!!!)</p>
<p>You always win the cheapest item in the drawing. And every once in a while it gives you a MEDIUM item so you do not give up and believe you still have a real chance. Only if you spend A LOT (and by that point you have already spent more than 1000 credits anyway), only then maybe it hands you a big item, to shoot you up with more dopamine and deepen your <strong>addiction</strong>.</p>
<p>That is exactly what happens with LLMs. Their answers have odds similar to a Loot Box. Sometimes it surprises you with an excellent answer, but most of the time you are stuck asking &ldquo;dude, are you sure about that answer?&rdquo;, or &ldquo;dude, isn&rsquo;t that second line wrong?&rdquo;, or &ldquo;dude, re-read what I asked and try again&rdquo;, and your whole day goes on like that.</p>
<p>You are not watching it, but under the hood every time it has to correct itself it keeps burning the same credits. By the end of the day, there goes USD 50. Sounds like a little, but do it every day and see how it adds up.</p>
<p>Except your <strong>CONFIRMATION BIAS</strong> makes you remember well when it got things right, but keeps &ldquo;forgetting&rdquo; all the other times it got things wrong. And your perception is that <em>&ldquo;Yeah, it works.&rdquo;</em></p>
<p>Worse, your low self-esteem and need to belong to a group, to <strong>CONFORM</strong>. Reinforced by what you read in the news every day, like:</p>
<ul>
<li>30% of all Microsoft code is already written by AI</li>
<li>The CEO of Anthropic says that next year programmers will be replaced by virtual workers</li>
<li>The CEO of NVIDIA says he would not recommend his son to study programming anymore</li>
</ul>
<p>And all of it RAISES THE BIAS: you think they are right, and the times you yourself saw the LLM getting it wrong <em>&ldquo;must have been a small exception, I am the one who is dumb&rdquo;</em> and you BELIEVE the LIE.</p>
<p>Who remembers my talks from 15 years ago where I used to show this video?</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/iRh5qy09nNw"
    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>This is a well known phenomenon, today amplified even more by social networks and by the dumbness of journalists who just parrot what they hear and no longer ask critical questions like: IS THERE A CONFLICT OF INTEREST??</p>
<h3>&ldquo;Does OpenAI use your chats for training?&rdquo;<span class="hx:absolute hx:-mt-20" id="does-openai-use-your-chats-for-training"></span>
    <a href="#does-openai-use-your-chats-for-training" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Here I have to fix a lie I have been telling you all. I always kept saying no. Your conversation is not used for training, because it would pollute the training material with garbage that in the end would not improve the answers.</p>
<p>It is a half-truth. Indeed, I doubt it a lot and I will stake my reputation on it: no provider wants to risk someone finding a copyrighted text and getting sued. That already happens, the New York Times sued because ChatGPT managed to reproduce a text they had published. And that kind of &ldquo;proves&rdquo; it was part of the training material.</p>
<p>There are people who paste private things in there like health conditions, financial situation, and even critical data like passwords, addresses, stuff they should not share but end up publishing because they have no clue about cybersecurity. Everybody falls for a scam every day because of that. OpenAI or Meta or Alibaba are not eager to have that show up in a chat one day and turn into a viral international news story about how irresponsible they were.</p>
<p><strong>BUT</strong></p>
<p>And it is a HUGE &ldquo;BUT&rdquo;, yes they do use your chat. Everyone&rsquo;s. Without copying any of your data. They just need the METADATA, the data that describes your chat.</p>
<p>Off the top of my head, being naive and amateurish about training, I can see at least 2 important pieces of information you hand them: the length of your chat and the sentiment of your responses in the chat.</p>
<p>Determining sentiment from text is a well solved problem, there are algorithms with or without neural networks that can tell if a text looks happy, sad, angry, anguished, and so on. They do not need the content of your text, they just need the SENTIMENT you had at that moment.</p>
<p>Add to that the LENGTH of your Chat and as I said <a href="https://www.akitaonrails.com/en/2025/05/02/rant-llms-sao-loot-boxes"target="_blank" rel="noopener">in the previous post</a>, the incentive is to make you burn MORE TOKENS (LONGER chats). IN THE NEXT POST I will show you how they do alignment, the model &ldquo;fine-tuning&rdquo;. An OpenAI uses thousands of lines of prompt/completion pairs to &ldquo;force&rdquo; the model to answer in a certain way. Think of an Excel full of pre-written questions and answers that, according to their metadata, we know are the most POPULAR answers.</p>
<p>The goal is not to make the model answer more correctly, because sometimes the right answer is very harsh or inconvenient for some people. I am almost certain that the data extracted from millions of people&rsquo;s chats is just that: sentiment and length. It must help craft prompts that force the model to answer &ldquo;less correctly&rdquo; but &ldquo;more pleasantly&rdquo;, encouraging the user to KEEP talking in the chat and BURN MORE TOKENS.</p>
<p>It is the oldest behavior in the world. Every shopkeeper knows this. Every salesperson knows this. Everyone who works in services knows this. If you are correct and direct, you will sound arrogant and the customer will not come back. Better to sound less correct but more inviting, pleasant, helpful, and so on. And the customer comes back, because that is how the average person works.</p>
<p>So, yes: I do think your data serves to re-align the model after training, to make it answer more pleasantly. They do not even bother hiding it anymore. Did you not see Sam Altman himself a few days ago saying they made ChatGPT 4 answer in a friendlier way, with more personality?</p>
<p>That is what I think they are doing. Making the model smarter is turning out to be a challenge. Because - as I have been saying for a year now - we are hitting the top of the S-curve. To keep selling tokens, to bring you back and get you hooked on talking, it does not need to be smarter, it just needs to be more pleasant.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>There is no LLM that will be 100% right 100% of the time, the fundamental architecture PREVENTS that from happening, no matter how many optimizations we pile on. There is now and there will always be an ENTROPY component. It is that randomness that produces text which sounds like a human talking and not a robot.</p>
<p>Automations that are too long and too complex built on top of this fragile foundation are a HUGE mistake. Sure, for simple, short, focused tasks, it has a higher HIT RATE. For real code? It is SUPER low.</p>
<p>Never let an MCP commit directly to your Git repository. Tools like Aider ship &ldquo;autocommit = true&rdquo; by default and that is an enormous stupidity! Do not do that.</p>
<p>I warned you.</p>
<p>But is there no way to improve LLMs at coding? Maybe. 100% never, but bumping up the rate I think we can. And to prove I am not anti-LLM or anything like that, in the next article I am going to try to teach HOW to improve an LLM for programming (with medium probability, it is no miracle!).</p>
]]></content:encoded><category>llms</category><category>coding-agents</category><category>software-engineering</category><category>off-topic</category></item><item><title>Why it is just lazy to bad-mouth Ruby on Rails</title><link>https://akitaonrails.github.io/en/2017/08/03/why-is-it-just-lazy-to-bad-mouth-ruby-on-rails/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2017/08/03/why-is-it-just-lazy-to-bad-mouth-ruby-on-rails/</guid><pubDate>Thu, 03 Aug 2017 18:01:00 -0300</pubDate><description>&lt;p&gt;It&amp;rsquo;s inevitable these days: we will see an article proclaiming the demise of Ruby on Rails every once in a while. It&amp;rsquo;s the easiest click bait, like &lt;a href="https://thenextweb.com/dd/2017/07/26/ruby-rails-major-coding-bootcamp-ditches-due-waning-interest/#.tnw_kWT917pu"target="_blank" rel="noopener"&gt;this one from TNW&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, you may say &lt;em&gt;&amp;ldquo;another Ruby fanboy.&amp;rdquo;&lt;/em&gt; That&amp;rsquo;s fair, but a terrible argument, as it&amp;rsquo;s a poor and common &lt;a href="https://en.wikipedia.org/wiki/Ad_hominem"target="_blank" rel="noopener"&gt;argumentum ad hominem&lt;/a&gt;. And on the subject of fallacies, the click-bait article above is wrong exactly because it falls for a blatant &lt;a href="https://en.wikipedia.org/wiki/Post_hoc_ergo_propter_hoc"target="_blank" rel="noopener"&gt;Post hoc ergo propter hoc&lt;/a&gt; fallacy plus some more &lt;a href="https://en.wikipedia.org/wiki/Confirmation_bias"target="_blank" rel="noopener"&gt;confirmation bias&lt;/a&gt; which we are all guilty of falling for all the time.&lt;/p&gt;</description><content:encoded><![CDATA[<p>It&rsquo;s inevitable these days: we will see an article proclaiming the demise of Ruby on Rails every once in a while. It&rsquo;s the easiest click bait, like <a href="https://thenextweb.com/dd/2017/07/26/ruby-rails-major-coding-bootcamp-ditches-due-waning-interest/#.tnw_kWT917pu"target="_blank" rel="noopener">this one from TNW</a>.</p>
<p>Now, you may say <em>&ldquo;another Ruby fanboy.&rdquo;</em> That&rsquo;s fair, but a terrible argument, as it&rsquo;s a poor and common <a href="https://en.wikipedia.org/wiki/Ad_hominem"target="_blank" rel="noopener">argumentum ad hominem</a>. And on the subject of fallacies, the click-bait article above is wrong exactly because it falls for a blatant <a href="https://en.wikipedia.org/wiki/Post_hoc_ergo_propter_hoc"target="_blank" rel="noopener">Post hoc ergo propter hoc</a> fallacy plus some more <a href="https://en.wikipedia.org/wiki/Confirmation_bias"target="_blank" rel="noopener">confirmation bias</a> which we are all guilty of falling for all the time.</p>
<p>I&rsquo;m not saying that the author wrote fallacies on purpose. Unfortunately, it&rsquo;s just too easy to fall for fallacies. Especially when everybody has an intrinsic desire to confirm one&rsquo;s biases. Even trying to be careful, I end up doing that as well.</p>
<p>On the specific subject of Rails, start here:</p>
<ul>
<li><a href="http://www.akitaonrails.com/en/2016/05/23/rails-venceu-o-elefante-na-sala"target="_blank" rel="noopener">Rails has won: The Elephant in the Room</a></li>
<li><a href="http://www.akitaonrails.com/en/2016/08/19/a-comunidade-ruby-e-a-reputacao"target="_blank" rel="noopener">The Ruby Community and Reputation</a></li>
<li><a href="http://www.akitaonrails.com/en/2017/05/14/guilda-de-programadores-religiao-e-esportes"target="_blank" rel="noopener">Programmers Guild - Religion and Sports</a></li>
</ul>
<p>As a general rule, anyone announcing the future demise of anything is most probably wrong. It&rsquo;s fun to play guessing games, but articles in high profile websites like TNW should be more careful as many beginners in the field end up trusting their opinions.</p>
<p>Which is why I usually tell people to stop trusting opinions. Everybody has an opinion, which is why it&rsquo;s so cheap. Then people artificially grade &ldquo;celebrities&rdquo; or &ldquo;gurus&rdquo; as having opinions of higher value. I don&rsquo;t buy it, neither should you.</p>
<p><a href="https://en.wikipedia.org/wiki/Black_swan_theory"target="_blank" rel="noopener">Black Swans</a> do happen. But it&rsquo;s an exercise in futility to try to predict them. You may be lucky, once or twice. But it&rsquo;s useless to attempt. Which is why I am a high proponent of <a href="https://en.wikipedia.org/wiki/Antifragility"target="_blank" rel="noopener">Antifragility</a>, so I will give you the general rules to understand what to do.</p>
<p>Am I saying that Ruby on Rails will live on forever? Don&rsquo;t be absurd, of course not. Nothing lasts forever. More than 2 decades actively working in the field taught me that betting everything on one horse is a mistake. It&rsquo;s valid in the financial markets, it&rsquo;s valid in the tech market.</p>
<p>Instead of stock or bonds, your asset is your knowledge and your practice. Diversification of your portfolio is usually a better bet.</p>
<p>You invested all your 401k in Bitcoins. Now you will try everything to try to confirm your bias by subscribing to every news piece and opinion that confirms your bias, and you will denounce every article that may have an argument against your bet. Am I saying to not invest in Bitcoins? Hell no. Just don&rsquo;t be stupid and invest everything on anything.</p>
<p>You invested all your time learning <a href="https://en.wikipedia.org/wiki/Google_Web_Toolkit"target="_blank" rel="noopener">Google Web Toolkit</a> in 2006. Good luck for you finding an article to confirm that bias. This is one case, an after-the-fact, that we can&rsquo;t do much but consensually agree on its death.</p>
<blockquote>
  <p>The job of a proper professional programmer is not mastery of a single tool or even a single technology stack. Your job is to be as good as possible in learning and practicing in the technology field.</p>

</blockquote>
<p>Having said all that, and even though the article&rsquo;s structure is inaccurate, The feeling is not wrong. Ruby on Rails no longer represents what it was 10 years ago. Now, before you start freaking out, let&rsquo;s explore it down memory lane.</p>
<h3>2003 - A New Renaissance<span class="hx:absolute hx:-mt-20" id="2003---a-new-renaissance"></span>
    <a href="#2003---a-new-renaissance" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>As an after-the-fact exercise, one has to realize that a series of events helped unfold the Rails phenomenon between 2003 and 2011. It was not by design. It was not planned. It could&rsquo;ve been something else.</p>
<ul>
<li>We were just recovering from the 2001 Internet dot-com Bubble Crash.</li>
<li>Social networks were starting to emerge, we had Orkut, MySpace, Friendster. Still no Facebook on the radar.</li>
<li>Blogs were starting to pick up steam and competing with traditional news outlets.</li>
<li>Big vendors focused solely on the Enterprise market at that point and the rest were deemed amateurish.</li>
<li>In the mainstream either you were doing .NET or Java on the Enterprise or doing hotsites in Flash and PHP for ad hoc agencies.</li>
<li>Even open source tools like Eclipse were primarily used through enterprisey umbrellas like WebSphere or WebLogic.</li>
<li>The Agile Manifesto had just been published in 2001 and the mainstream market didn&rsquo;t pay much attention. PMI and RUP were the order of the day.</li>
</ul>
<p>Then we reached 2004.</p>
<ul>
<li>Google successfully launched Gmail to much fanfare. Dynamic HTML and Ajax demoed how we could achieve Desktop-level applications in the Web. This is also where Javascript earns a new role beyond stupid web mini games or ads.</li>
<li>Facebook skyrocketed and we were shown glimpses of what granular social networks look like.</li>
<li>Apple consolidated its comeback as a relevant tech giant with the successful resurrection of OS X, proving that Unix on the consumer desktop was viable.</li>
<li>Microsoft entered the 21st century after a super heavy antitrust case that almost obliterated the company. Bill Gates resigned, Steve Ballmer took the reins and they had their darkest decade</li>
<li>Years of vendor lock-in finally started to wear off developers, no one actually wanted to use IBM, BEA, Oracle anymore and the promise of the open source revolution started in the 90&rsquo;s finally started to solidify</li>
<li>The dot-com crash left a depression in the tech market but it also left behind an infrastructure that was finally being put to good use</li>
</ul>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/654/s-l225.jpg" alt="How Bill Blew it"  loading="lazy" /></p>
<p>Ruby on Rails shows up as a very small curiosity, in an obscure tech conference in Brazil, created by a small but promising agency called 37signals. Signaling something most developers had never seen before: a flexible language - unlike Java or C# or PHP - that could be used to implement Gmail-like web applications with a flavor of the Apple vision and steering away to everything that Microsoft and IBM represented while incorporating Agile-like techniques such as Test-Driven Development in a practical way and out-of-the-box.</p>
<p><a href="https://www.youtube.com/watch?v=Gzj723LkRJY"target="_blank" rel="noopener"><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/655/75879992_fa890a9a07.jpg" alt="15 minute blog"  loading="lazy" /></a></p>
<p>What else could you ask for in 2005?</p>
<p>Then <strong>2006</strong> happens: Amazon AWS releases S3 and EC2 and the term &ldquo;Elastic Cloud&rdquo; becomes the new hype.</p>
<p>Up to that point, &ldquo;web development&rdquo; either meant expensive and bureaucratic IT departments in big corporations or inexpensive but weak shared hosting in services such as Dreamhost, 1&amp;1, Media Temple.</p>
<p>Elastic Cloud holds the key to surpass the enterprise and make it possible for independent developers to build the elusive &ldquo;web scale&rdquo;.</p>
<p>Ruby on Rails is exactly where it should be: starting to be adopted by a new crop of Silicon Valley tech start-ups trying to make a dent in the universe in this new Ajax-based Cloud-era. Remember that Rails was first released with RJS, a way to write Javascript through Ruby using Prototype and Scriptaculous. jQuery was not even mainstream yet.</p>
<p>Twitter shows up. Groupon shows up. Engine Yard shows up. And much more follow.</p>
<p>It&rsquo;s all happening blistering fast, bear in mind that I am only touching the surface here.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/4XpnKHJAok8"
    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><strong>2007</strong> arrives and at least 3 big things happen:</p>
<ul>
<li>Another Rails-based start-up is born: Heroku. If AWS EC2 coined the term &ldquo;Infrastructure as a Service&rdquo; (IaaS) Heroku defined what a &ldquo;Platform as Service&rdquo; (PaaS) means. It was able to put EC2 to good use and make its power approachable.</li>
<li>At the same time, developers have grown bitter of version control systems such as CVS and again, big vendor options such as Rational ClearCase, Microsoft SourceSafe, and others were just terrible. Subversion had emerged but its many shortcomings would become its demise. Linus Torvalds decides to fix the problem and release Git. Mainstream developers finally learn basic computer science terms such as DAGs (&ldquo;Directed Acyclic Graphs&rdquo;).</li>
<li>The iPhone is released and a new mobile revolution takes place. Dominant players like Nokia and Blackberry were shaken to their core. Developers want a piece of it. Mobile Safari becomes high profile overnight. And its WebKit core rises to stardom, eclipsing old veterans like Firefox/Gecko and Opera.</li>
</ul>
<p><strong>2008</strong>: the birth of GitHub and the dawn of the DevOps.</p>
<p>Git was great but too low level. Several other contenders tried to become the new standard such as Mercurial and Bazaar. But the birth of GitHub would change all that. Again, another Rails-based start-up making the news.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/653/Theres-An-App-for-That-Tanglewood-Moms.jpg" alt="There’s an App for that"  loading="lazy" /></p>
<p>But the honeymoon is showing signs of reaching an end: prominent Ruby developer, Zed Shaw, <a href="https://techcrunch.com/2008/01/01/zed-shaw-puts-the-smack-down-on-the-rails-community/"target="_blank" rel="noopener">loudly leaves the community</a> by January.</p>
<p>During this year we also had the cold war between Engine Yard&rsquo;s Merb framework and 37signals&rsquo; Rails, resulting in the <a href="http://yehudakatz.com/2008/12/23/rails-and-merb-merge/"target="_blank" rel="noopener">controversial Rails-Merb merge</a>, placing another crack in the community.</p>
<p>But we still had a lot of work to do, so we moved on.</p>
<p><strong>2009</strong>: &ldquo;there&rsquo;s an app for that!&rdquo;</p>
<p>Apple finally releases a proper native SDK. Developers start building a new generation of productivity apps and games. WebKit starts growing in market share. Apple solidifies the concept of aesthetic in software development and Ruby on Rails is at its high at that point.</p>
<p>We begin the NoSQL era. When data storage and fetch start to really become a bottleneck, we quickly learn that traditional enterprisey databases are not &ldquo;web scale&rdquo; enough. MongoDB leads the pack. Web development starts an era of craze over NoSQL.</p>
<p>In parallel, Twitter, once the poster-child of the Rails generation, creates a crack in the Ruby community when it declares that it&rsquo;s started to use yet another obscure language: <a href="http://www.artima.com/scalazine/articles/twitter_on_scala.html"target="_blank" rel="noopener">Scala</a>!</p>
<p>This starts the endless obsession era about &ldquo;concurrency&rdquo;, yet another elusive concept that many talk big and talk a lot but only a few actually grasp its meaning. It&rsquo;s the start of the window of opportunity for Node.js, Scala, Erlang, Clojure, Go.</p>
<p>The honeymoon is over.</p>
<p><strong>2010</strong>:</p>
<ul>
<li>The demise of Sun and Oracle as the new stewardship for Java. If things were not playing out cool for Java, that was a heavy nail in the coffin.</li>
<li>Apple declares open war against Flash. Another big push for Javascript and front-end heavy web apps.</li>
</ul>
<p><a href="https://www.apple.com/hotnews/thoughts-on-flash/"target="_blank" rel="noopener"><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/656/apple-no-flash-250x376.jpg" alt="Thoughts on Flash"  loading="lazy" /></a></p>
<p>In my personal opinion, this is the year when Rails reached its peak in terms of influence. It was the go-to stack for developers in Macs wanting to create their brand new Lean Start-up.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/icMA9rYNmgU"
    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>

<h3>2011: A New Era<span class="hx:absolute hx:-mt-20" id="2011-a-new-era"></span>
    <a href="#2011-a-new-era" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>In around 6 years the software development market did titanic shifts:</p>
<ul>
<li>Distributed development derived from years of Open Source workflow becomes the new standard. Git and GitHub were key players. Big vendors have to play catch-up with their stinky enterprise alternatives such as TFS.</li>
<li>Agile is taken seriously in a new crop of tech startups evangelizing proper software development. Tests shift from unproductive obnoxious things into invaluable core technique. Tools like Hudson/Jenkins combined with Git workflow pave the way for what continuous integration and later continuous deployment would become.</li>
<li>AWS and the Heroku example pave the way to make old IT obsolete and create a new generation of infrastructure as software, and &ldquo;DevOps&rdquo;. There is now the urge to deploy super web scale with zero friction. Time to market becomes a key factor: release early, release often.</li>
<li>Apple releases the AppStore and helps drive the Software as Service market, with apps requiring live internet to function. The &ldquo;Cloud&rdquo; reaches the masses.</li>
<li>By making everybody on Earth carry an always-on powerful device, a smartphone, we were never so close to the Orwellian future of 1984. Everything a person does is a data point. Big Data becomes a thing. Data Sciences becomes viable as a mainstream profession. I.A. related disciplines become a necessity to reason about so much data.</li>
</ul>
<p>Android is getting closer to the iPhone. Microsoft is resurrecting from its ashes. In October, Steve Jobs dies, marking the end of the era of Apple&rsquo;s heavy influence.</p>
<p>But around 2011 another thing happened: we win.</p>
<p>Independent developers surpassed the status quo of enterprise developers. There is a shift of power. The conventions for the software development field don&rsquo;t come from the old big tool vendors anymore. We are not listening to IDE vendors like Microsoft, BEA, Oracle. We are now listening to the winners of this generation of startups: Apple, Google, Facebook.</p>
<p>The concept of <em>&ldquo;one language to rule them all&rdquo;</em> was shattered in the web camp by Ruby on Rails and in the native application space by Apple. 2 very unusual and strange languages, Ruby and Objective-C, reached mainstream status. They proved that developers can learn and master exotic languages turning them into profitable products.</p>
<p>With those foundations set, a new <strong>gold rush</strong> emerges: new languages galore, free from the notion that to become successful one had to stick to Java, C#, or C++ like languages, people finally started looking elsewhere to differentiate themselves, and thus a new generation of exotic languages arrive: Scala, Haskell, Ocaml, F#, Erlang, Clojure, Rust, Elixir, etc. The sky is the limit.</p>
<p>By killing Flash and accelerating HTML 5, ES6, and with all the controversies around concurrency, Javascript gained new life and fast adoption unlike nothing else before.</p>
<h3>Epilogue<span class="hx:absolute hx:-mt-20" id="epilogue"></span>
    <a href="#epilogue" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Is Ruby on Rails dead? No.</p>
<p>Is Ruby on Rails still the leader of the pack? No.</p>
<p>What is the legacy of the Ruby on Rails Generation? It played an important role in defining what we now consider the new status quo for web development: Git, CI, CD, Cloud, Metrics. It brought the Apple aesthetics thinking to the table in a big way.</p>
<p>That new crop of tech startups had a serious side-effect though: it put new engineers in the spotlight, elevating them to rock star levels and turning them into Steve Jobs wannabes. They just started taking everything we accomplished for granted. Recipe for disaster.</p>
<p>And while the big vendors&rsquo; influence became thin compared to the 90&rsquo;s, they are not dead. After 2011 we ushered in a new era of chaos, an era of dissatisfied developers. Where every couple of years a new derivation emerges and we have that unsatisfying feeling that we need to rewrite everything all the time.</p>
<p>The Ruby on Rails pool got diluted into smaller niche languages such as Javascript itself, Go, Elixir, Scala.</p>
<p>The Java/Microsoft mainstream is gaining new traction. Aesthetics also got diluted. Careful engineering is getting diluted. Social networks ubiquity and the easy path of quick cloud deployment is making people less and less patient. Crafting requires patience. People crave Likes, a new generation of vanity metrics arose.</p>
<p>We now have a larger number of independent non-enterprise developers with little to no patience, with an exaggerated sense of confidence, and <a href="https://en.wikipedia.org/wiki/Not_invented_here"target="_blank" rel="noopener">not-invented-here</a> syndrome, taking everything for granted and caring less and less about proper value.</p>
<p>In conclusion, the death of Ruby on Rails is indeed greatly exaggerated, it will keep going for many years still - especially if the Ruby Core Team can actually deliver on the <a href="https://blog.heroku.com/ruby-3-by-3"target="_blank" rel="noopener">3x3 goal</a>. People like to share doomsday articles while optimistic and more realistic reports such as this <a href="https://reinteractive.com/posts/320-why-ruby-on-rails-is-still-the-best-choice"target="_blank" rel="noopener">Why Ruby on Rails is still the best choice?</a> are largely ignored. Such is the way of social networks vanity.</p>
<p>But it will not be what it once was because the times have changed. We can now live by the &ldquo;On Rails&rdquo; workflow without Ruby on Rails. And it can&rsquo;t go on breaking the status quo, because it became the status quo.</p>
<p>I wonder where that will lead us. I do have a few clues, but as I said, I am not in the game of publishing click-bait headlines.</p>
]]></content:encoded><category>rails</category><category>web-development</category><category>software-engineering</category></item><item><title>Estimates are Promises - A Better Metaphor</title><link>https://akitaonrails.github.io/en/2017/06/26/estimates-are-promises-a-better-metaphor/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2017/06/26/estimates-are-promises-a-better-metaphor/</guid><pubDate>Mon, 26 Jun 2017 17:44:00 -0300</pubDate><description>&lt;p&gt;If you didn&amp;rsquo;t know, I frequently answer questions over on Quora. &lt;a href="https://www.quora.com/profile/Fabio-Akita"target="_blank" rel="noopener"&gt;Follow me there&lt;/a&gt;; I&amp;rsquo;ve written almost 600 answers so far, many of them close to my longest posts here on the blog.&lt;/p&gt;
&lt;p&gt;One of the most popular ones answers the question &lt;a href="https://www.quora.com/What-is-the-hardest-thing-you-do-as-a-software-engineer/answer/Fabio-Akita"target="_blank" rel="noopener"&gt;&amp;ldquo;What is the hardest thing you do as a software engineer?&amp;rdquo;&lt;/a&gt;. I wrote something similar in Brazilian Portuguese in the post &lt;a href="http://www.akitaonrails.com/en/2013/08/23/off-topic-estimativas-sao-promessas-promessas-devem-ser-cumpridas"target="_blank" rel="noopener"&gt;&amp;ldquo;Estimativas são Promessas. Promessas devem ser cumpridas.&amp;rdquo;&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In a nutshell, you can never give an estimate that is &amp;ldquo;correct&amp;rdquo;. If you could, it would not be an &amp;ldquo;estimate&amp;rdquo;, it would be a &amp;ldquo;prediction&amp;rdquo;.&lt;/p&gt;</description><content:encoded><![CDATA[<p>If you didn&rsquo;t know, I frequently answer questions over on Quora. <a href="https://www.quora.com/profile/Fabio-Akita"target="_blank" rel="noopener">Follow me there</a>; I&rsquo;ve written almost 600 answers so far, many of them close to my longest posts here on the blog.</p>
<p>One of the most popular ones answers the question <a href="https://www.quora.com/What-is-the-hardest-thing-you-do-as-a-software-engineer/answer/Fabio-Akita"target="_blank" rel="noopener">&ldquo;What is the hardest thing you do as a software engineer?&rdquo;</a>. I wrote something similar in Brazilian Portuguese in the post <a href="http://www.akitaonrails.com/en/2013/08/23/off-topic-estimativas-sao-promessas-promessas-devem-ser-cumpridas"target="_blank" rel="noopener">&ldquo;Estimativas são Promessas. Promessas devem ser cumpridas.&rdquo;</a>.</p>
<blockquote>
  <p>In a nutshell, you can never give an estimate that is &ldquo;correct&rdquo;. If you could, it would not be an &ldquo;estimate&rdquo;, it would be a &ldquo;prediction&rdquo;.</p>

</blockquote>
<p>Let us assume we have neither precognition powers nor magic crystal balls to tell us the future.</p>
<p>To estimate something is to &ldquo;guess&rdquo; the value of something. It&rsquo;s always a guess, the same thing as a valuation. And like any guess, it can never be deemed &ldquo;correct&rdquo;: it&rsquo;s just a likely candidate among an infinite range of possible values.</p>
<blockquote>
  <p>There is zero connection between a guess and the outcome. Understand this simple truth: saying something can happen does not MAKE it happen.</p>

</blockquote>
<p>Estimating tomorrow&rsquo;s weather as rainy does not MAKE it rain. Estimating the result of the Super Bowl does not MAKE it happen. There is zero correlation between an estimate and the actual outcome.</p>
<p>I stated in those articles that <strong>&ldquo;Estimates are Promises&rdquo;</strong>. The intention was to provoke a reaction, since most people assume that estimates can never be promises, exactly because of what I just explained.</p>
<p>What makes a promise special is that once you promise something, you&rsquo;re expected to <strong>ACT</strong> on delivering it.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/643/Skin-in-the-Game.png" alt="Skin in the Game"  loading="lazy" /></p>
<blockquote>
  <p>No one that has no &ldquo;skin in the game&rdquo; should give estimates.</p>

</blockquote>
<p>If you&rsquo;re not an active player in the game, you shouldn&rsquo;t be handing out guesses. The same way no one makes promises on someone else&rsquo;s behalf.</p>
<p>Can you actually make credible promises and keep them? You can, but first you have to understand a few more truths about reality.</p>
<p>You&rsquo;ve probably already seen plenty of articles explaining project management methodologies and techniques. Most of them manage to explain the Whats and the Hows, but as usual, they fail on the Whys.</p>
<p>Why do we need those methodologies? Why are they necessary? Why do they work? What hidden mechanisms do they set in motion?</p>
<p>What makes Agile techniques any different from your usual homeopathy or a budget self-help cliché?</p>
<h3>It&rsquo;s always all about Pareto<span class="hx:absolute hx:-mt-20" id="its-always-all-about-pareto"></span>
    <a href="#its-always-all-about-pareto" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>There is no such thing as a <strong>precise</strong> project scope. There is a limit to adding detail, and past it you just get <a href="http://www.investopedia.com/terms/l/lawofdiminishingmarginalreturn.asp"target="_blank" rel="noopener"><strong>diminishing returns</strong></a>.</p>
<p>The most precise level of detail a feature scope can have is the actual code of the feature. And this matters: the lines of code are <strong>NOT</strong> the outcome. What end users actually experience is the execution of that code at runtime.</p>
<p>The programming itself is the <strong>actual blueprint</strong>. The diagrams, the use cases, the user stories, and anything else that comes before the programming are just a draft, a mere sketch.</p>
<p>A naive architect or designer might think that detailed diagrams, use case documents, and fancy PowerPoint slides are worth as much as an engineering blueprint. They&rsquo;re worth less: they&rsquo;re the equivalent of a <strong>sketch on a napkin</strong>. Volatile and, in practice, mostly worthless.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/644/Screenshot_from_2017-06-26_16-19-14.png" alt="Napkin Sketch"  loading="lazy" /></p>
<p><em>&ldquo;But isn&rsquo;t the programming the same as the construction phase itself, laying bricks on top of bricks?&rdquo;</em> <strong>NO</strong>, that&rsquo;s not what a programmer does. The bricklaying is the job of the language interpreter or the compiler that spits out the executable binary.</p>
<p>This is the metaphor that drives non-programmers crazy. In engineering, the construction itself is the most expensive and time-consuming part. In programming, the expensive part is the blueprint (writing the code), and the &ldquo;construction&rdquo; is trivial: just compiling, which is automatic.</p>
<p>In the same way, a project scope is just a set of sketches. We have to drop the notion that a <strong>&ldquo;complete&rdquo;</strong> scope will ever exist. There&rsquo;s no way to say &ldquo;100% of the scope&rdquo; or &ldquo;closed scope&rdquo;, because a software project scope is, by definition, always incomplete.</p>
<p>On top of that, I&rsquo;ll argue that roughly 80% of this so-called &ldquo;scope&rdquo;, what I prefer to call a sketch, is mostly worthless to most end-user activities: the admin section, the institutional pages nobody reads, convoluted sign-up flows, and so on.</p>
<p>This is why every feature list <strong>MUST</strong> be prioritized. You can usually get by with 20% of the features, which is roughly what people mean when they say &ldquo;MVP&rdquo; or &ldquo;minimum viable product&rdquo;. <strong>Release as early as you can</strong>, get user feedback, and refine the rest of the &ldquo;sketch&rdquo; you call a backlog.</p>
<p>So instead of aiming for an all-or-nothing proposition, hunting for stupidly complex equations to figure out a &ldquo;precise&rdquo; estimate of an incomplete sketch, assume you can actually deliver EARLY the first 20% that really matters and figure out the rest in iterations.</p>
<p>Oh yeah, this is what we call &ldquo;Agile&rdquo;, by the way.</p>
<h3>Agile is about Risk Management<span class="hx:absolute hx:-mt-20" id="agile-is-about-risk-management"></span>
    <a href="#agile-is-about-risk-management" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>People assume Agility is about managing the project management instruments themselves: the backlog, the rituals, the metrics.</p>
<p>Having Agile-like instruments doesn&rsquo;t make you Agile.</p>
<p>Being Agile is keeping <strong>Risk</strong> under control.</p>
<p>Instead of treating projects as an all-or-nothing endeavor, start thinking about them the way an investor thinks about a <strong>portfolio of stocks</strong>. You don&rsquo;t expect the whole portfolio to turn a profit; you assume some stocks will underperform. You just don&rsquo;t know which ones, so you dilute your risk.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/645/635824249336942840-ThinkstockPhotos-152955327.jpg" alt="Portfolio stocks"  loading="lazy" /></p>
<p>Trying to predict the stock market is an exercise in futility.</p>
<p>Trying to predict the precise implementation of a project, especially the long ones, is also an exercise in futility.</p>
<p>So you have to deal with uncertainty the right way: by becoming <a href="https://www.amazon.com/Antifragile-Things-That-Disorder-Incerto/dp/0812979680"target="_blank" rel="noopener"><strong>Antifragile</strong></a>.</p>
<blockquote>
  <p>&ldquo;Some things benefit from shocks; they thrive and grow when exposed to volatility, randomness, disorder, and stressors and love adventure, risk, and uncertainty. Yet, in spite of the ubiquity of the phenomenon, there is no word for the exact opposite of fragile. Let us call it antifragile. Antifragility is beyond resilience or robustness. The resilient resists shocks and stays the same; the antifragile gets better&rdquo; - Nassim Taleb</p>

</blockquote>
<p>Instead of the preposterous exercise in futility of trying to predict uncertainty and random events, you do the reasonable thing: you assume Black Swans exist and you can&rsquo;t predict them. So you prepare for uncertainty the only reasonable way there is, by not trying to predict them.</p>
<p>Expose the small mistakes early and correct them often. Implementing everything in a black box and doing a Big Bang deployment is the easiest path to <strong>failure</strong>. Delivering often, exposing the bugs, and fixing them constantly is accepting that mistakes will happen and gaining strength along the way.</p>
<p><a href="https://www.amazon.com/Antifragile-Things-That-Disorder-Incerto/dp/0812979680"target="_blank" rel="noopener"><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/646/Antifragile-3.jpg" alt="Antifragile"  loading="lazy" /></a></p>
<h3>A Better Metaphor<span class="hx:absolute hx:-mt-20" id="a-better-metaphor"></span>
    <a href="#a-better-metaphor" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/642/steel_mill_blast_furnace_coking_coal_iron_ore_600.jpg" alt="Iron Ore Furnace"  loading="lazy" /></p>
<p>Imagine that you, whether the non-programmer client or the programmer who has no clue how to explain the process to that client, have an iron ore furnace to manage.</p>
<p>The thing about those furnaces is that if you heat them too much, they can explode in your face. If you cool them down too much, the ore hardens.</p>
<p>Your job is to add coal to the furnace, and you decide the rate. Too fast and it gets exploding hot. Too slow and you might put out the fire and lose your furnace.</p>
<p>Now try to estimate a constant intake of coal to keep the furnace in good shape.</p>
<p>You can&rsquo;t.</p>
<p>The easiest way out is to install a <strong>thermometer</strong> that tracks the furnace&rsquo;s current temperature.</p>
<p>You stay safe within a certain margin of temperatures, speeding up or slowing down the coal intake by checking the thermometer all the time.</p>
<p>ALL THE FREAKING TIME.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/647/iron-4.jpg" alt="Iron Furnace"  loading="lazy" /></p>
<p>This is what Agile-based &ldquo;Velocity&rdquo; (or any of the fancier Monte Carlo simulations) actually is: a thermometer.</p>
<p>If the Velocity is too high, your team is probably working extra hours or delivering lower-quality code. This will backfire: either your team burns out too fast, or the code piles up <a href="http://www.akitaonrails.com/en/2017/06/22/the-economics-of-software-development"target="_blank" rel="noopener">technical debt</a> too fast and you won&rsquo;t be able to pay it back. Keep that pace and Velocity drops to a halt (the furnace explodes).</p>
<p>If the Velocity is too low, your team is slacking off, your backlog is a useless piece of shit no one can understand even after 10-hour meetings, or you already let Velocity run too high and now you&rsquo;re paying back technical debt, or your team is dead from burnout (fire extinguished).</p>
<p>You want to keep Velocity <strong>steady</strong>, constant. This is what being Agile is all about: keeping an eye on the thermometer and responding.</p>
<h3>The Triforce of Projects<span class="hx:absolute hx:-mt-20" id="the-triforce-of-projects"></span>
    <a href="#the-triforce-of-projects" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/650/ironTriangle.jpg" alt="Iron Triangle"  loading="lazy" /></p>
<p>Welcome to the Iron Triangle of Project Management.</p>
<blockquote>
  <p>Repeat after me: if I want to lock down the time, the cost, and the scope, <strong>I am a moron</strong>.</p>

</blockquote>
<p>Repeat again.</p>
<p>You should lock down time and cost. If you&rsquo;ve read this far, you know you can never lock down &ldquo;scope&rdquo;; you can only make it fat, and not necessarily more valuable. This is why I always say the very definition of a Product Manager or Product Owner is to be the bastion of ROI (Return on Investment).</p>
<p>Now, why?</p>
<p>Because the Iron Triangle has the following corollary:</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/651/320px-Project-triangle.svg.png" alt="Project Triangle"  loading="lazy" /></p>
<ul>
<li>If you want it fast and good, it can&rsquo;t be cheap</li>
<li>If you want it fast and cheap, it can&rsquo;t be good</li>
<li>If you want it good and cheap, it can&rsquo;t be fast</li>
</ul>
<p>This is a Law, you can&rsquo;t fiddle with it. Pick your choice.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/652/keep-calm-and-make-your-choice-9.png" alt="Make your choice"  loading="lazy" /></p>
<h3>How do I keep Promises then?<span class="hx:absolute hx:-mt-20" id="how-do-i-keep-promises-then"></span>
    <a href="#how-do-i-keep-promises-then" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Now, with those 3 truths in hand:</p>
<ul>
<li>You can&rsquo;t have your lunch and eat it too (cheap, fast, and good)</li>
<li>You&rsquo;re managing the temperature of the furnace</li>
<li>You only need 20% of the &ldquo;sketch&rdquo; you call &ldquo;scope&rdquo;</li>
</ul>
<p>Yes, any experienced developer can give you a &ldquo;ballpark&rdquo; estimate. A ballpark goes like this:</p>
<ul>
<li>1 month (maybe 1 month and a half, but definitely less than 2)</li>
<li>3 months (it&rsquo;s more than 2 months, less than 6 months)</li>
<li>6 months (it&rsquo;s more than 4 months, less than 9 months)</li>
<li>it&rsquo;s more than 6 months, probably less than 1 year</li>
</ul>
<p>Don&rsquo;t even try to granulate more than that. It&rsquo;s useless.</p>
<p><strong>Lock the time.</strong> And <strong>lock the cost</strong> (that being the number of developers times the hourly rate times the total amount of estimated hours). That&rsquo;s it.</p>
<p>Now jot down what the client calls &ldquo;scope&rdquo; as user stories in a backlog and have him prioritize it.</p>
<p>Start the iterations. After each one, release to a staging environment. Non-programmers, beware: ALWAYS make sure whichever programmer you hire delivers testable versions of the finished stories to a publicly available URL you can actually visit and test.</p>
<p>If your programmer refuses to do that or gives excuses: <strong>FIRE HIM</strong>.</p>
<p>If your programmer or company or whatever promises you a &ldquo;closed scope&rdquo; price, promises to do everything, and you believe them, you&rsquo;re too gullible.</p>
<p>Do you think it&rsquo;s funny to play this stupid game of <em>&ldquo;I&rsquo;ll pretend to tell you that I know the truth and you will pretend that you believe what I tell you.&rdquo;</em></p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/648/e433998315b73da036e65884b67eba43.jpg" alt="Actions and Words"  loading="lazy" /></p>
<p>No serious professional has time for stupid games, and the only honest thing anyone can say about any software project is: <em>&ldquo;given my experience I believe the ballpark for this kind of project is within X months, given the Y and Z assumptions&rdquo;.</em></p>
<p>You don&rsquo;t have to believe him. You just need to start checking the thermometer. Any non-programmer can assess the quality of the delivery based on the frequent deliveries of the prioritized features.</p>
<p><em>&ldquo;But what if after 2 weeks I don&rsquo;t like the results?&rdquo;</em> Easy: <strong>FIRE HIM</strong>.</p>
<p>And sometimes &ldquo;firing&rdquo; isn&rsquo;t even the right word. Sometimes the relationship is difficult and the best thing to do is to part ways.</p>
<p>You have to accept losing 2 weeks, or any short period of time, as part of Risk Management. It&rsquo;s better to accept losing 2 weeks of your project budget than to blindly trust someone for 6 months and lose the entire project budget and then some.</p>
<p>Pareto again: Agility is about Risk Management. You accept that losing 20% of your budget is okay, and you play with that. And that&rsquo;s fine, because you only really need a bit more than 20% of the sketchy scope you have.</p>
<p>See what I just did with the Math here?</p>
<p>We stop playing the pretend game and actually manage the project&rsquo;s risks. You collaborate on the equivalent thermometer, which combines the prioritized backlog (the scale) and the velocity (the temperature), and you keep an eye on the partial deliveries in the staging environment.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>So yes, Estimates should be taken as seriously as Promises. You can give reasonable Estimates as long as you can manage the Risks and the Client accepts the rules of the game: there is no &ldquo;closed, complete scope&rdquo;, priorities first, and testing and accepting delivered features every week.</p>
<p>The idea behind Promises is that you have to <strong>MANAGE</strong> in order to meet them. The best way to do that is to frequently stop, re-assess, and keep going. It feels like you&rsquo;re wasting time, but you&rsquo;re actually saving yourself from wasting it.</p>
<p>If you don&rsquo;t have skin in the game, back off.</p>
<p>Velocity is meant to be kept steady, at a predictable rate. Use the variance as a sign of being too fast or too slow, adjust the other variable, measure again, and go. Just like a furnace thermometer.</p>
<p>There are several versions of &ldquo;thermometers&rdquo;, from Joel Spolsky&rsquo;s <a href="https://www.joelonsoftware.com/2007/10/26/evidence-based-scheduling/"target="_blank" rel="noopener">Evidence Based Scheduling</a> all the way to fancy Monte Carlo simulations and other stochastic processes (they are all thermometers, and NOT estimation tools).</p>
<p>What stops you from doing that is using the wrong metaphors and the wrong references.</p>
<p>Instead of hunting for equivalent metaphors in construction, factories, and other &ldquo;hard&rdquo;-ware assemblies, look elsewhere, where you&rsquo;ll find other &ldquo;soft&rdquo;-ware processes.</p>
<p>Musicians have deadlines. Painters have deadlines. Choreographers have deadlines. Sports have deadlines. Research laboratories have deadlines. How do they meet them? By constantly checking the current state, comparing it to the goals, assessing whether what they&rsquo;re doing is actually working, and changing what doesn&rsquo;t.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/649/conducting_starwars.jpg" alt="Conducting Star Wars"  loading="lazy" /></p>
<p>Hollywood has deadlines. They have far worse variables to control than any software project you&rsquo;ll ever run into, and they still manage to deliver. And profit.</p>
<p>And accept that you can&rsquo;t control every variable, so stop trying. Think of it as the financial markets. One day you have <a href="https://web.archive.org/web/20170705022520/https://motherboard.vice.com/en_us/article/zme78x/why-the-value-of-ethereum-has-skyrocketed-4000-percent"target="_blank" rel="noopener">Ethereum skyrocketing 4,000%</a> and the very next day you have it <a href="http://www.cnbc.com/2017/06/22/buyers-beware-lessons-from-the-ethereum-flash-crash.html"target="_blank" rel="noopener">falling down to ashes in a flash-crash</a>.</p>
<p>Don&rsquo;t aim to become resistant or resilient. Prepare to become Antifragile.</p>
]]></content:encoded><category>agile</category><category>management</category><category>software-engineering</category><category>off-topic</category></item><item><title>The Economics of Software Development</title><link>https://akitaonrails.github.io/en/2017/06/22/the-economics-of-software-development/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2017/06/22/the-economics-of-software-development/</guid><pubDate>Thu, 22 Jun 2017 10:56:00 -0300</pubDate><description>&lt;p&gt;The software development community is boiling nowadays with so many options all at once. You have dozens of active and very good languages such as Go, &lt;a href="https://akitaonrails.github.io/elixir"&gt;Elixir&lt;/a&gt;, Clojure. Dozens of very good frameworks, both in the back-end and front-end including React, Elm. Dozens of sound methodologies from good old Agile stuff all the way to Continuous Deployment with microservices.&lt;/p&gt;
&lt;p&gt;Now, you&amp;rsquo;re a small tech startup or even a small team in a big corporation. How to even start?&lt;/p&gt;</description><content:encoded><![CDATA[<p>The software development community is boiling nowadays with so many options all at once. You have dozens of active and very good languages such as Go, <a href="/elixir">Elixir</a>, Clojure. Dozens of very good frameworks, both in the back-end and front-end including React, Elm. Dozens of sound methodologies from good old Agile stuff all the way to Continuous Deployment with microservices.</p>
<p>Now, you&rsquo;re a small tech startup or even a small team in a big corporation. How to even start?</p>
<p>And the recommendation is: do the smallest thing that works first. Always. Of course, avoid &ldquo;quick and dirty&rdquo; as much as you can. But don&rsquo;t get paranoid and over-engineer too much.</p>
<p>Over-engineering is as expensive as doing things &ldquo;dirty&rdquo;.</p>
<h3>Add &ldquo;Time&rdquo; to your equation<span class="hx:absolute hx:-mt-20" id="add-time-to-your-equation"></span>
    <a href="#add-time-to-your-equation" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>What most people do very wrong is disregarding the variable of &ldquo;Time&rdquo;.</p>
<p>Everybody goes to tech conferences or read fancy blog posts, or watch flashy screencasts. Their wrong conclusion is: <em>&ldquo;Netflix uses it, therefore it should be good for me, because I want to become Netflix&rdquo;</em>.</p>
<p>The wrong assumption is that Netflix, or Google, or Facebook, or Spotify, is a static system that always functioned the way they advertise it today.</p>
<p>People forget that every unicorn company had a <strong>day-1</strong>. And on day-1 they did not use microservices. They did not have React.js. They did not have Go or Elixir. Some of them didn&rsquo;t even have good programmers to begin with. Netflix started as a VHS renting service, remember that? Google started off in a dorm-room with off-the-shelf hardware components assembled in a <a href="http://www.complex.com/pop-culture/2013/02/50-things-you-didnt-know-about-google/lego-server-rack"target="_blank" rel="noopener">Lego-based rack</a>.</p>
<p>You are a fan of some celebrity. That celebrity has a Lamborghini car. So you want to be like that celebrity one day. What do you do? Do you buy the same Lamborghini he has?</p>
<p>If you are that stupid, the only thing you will end up with is a HUGE debt to pay.</p>
<p>Stop envying the rich guy&rsquo;s Lamborghini.</p>
<h3>(Technical) Debt<span class="hx:absolute hx:-mt-20" id="technical-debt"></span>
    <a href="#technical-debt" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This is neither a new term nor a new theme. Every decision you make in programming is a <strong>compromise</strong> between what the &ldquo;future-ideal&rdquo; should be and what you can actually do right now. Loan to buy the equivalent Lamborghini too soon, and you will have to deal with an unpayable (Technical) Debt. You will stall, you will stop, and you will do nothing but pay that debt from now on.</p>
<p>You want to &ldquo;code faster&rdquo; therefore you skip writing automated tests, because you assume it will slow you down. And you&rsquo;re right. Test-driven development doesn&rsquo;t make you fast at first, and it isn&rsquo;t supposed to. It protects your future self from your present self, and keeps you from piling up Technical Debt.</p>
<p>Again, you forget the &ldquo;Time&rdquo; variable.</p>
<p>You write and deliver fast for the 1st month. After the first version is deployed, now you will be obliged to start paying the debt. Your productivity will slow down. Regression bugs will show up. Every new feature you try to add breaks something in unexpected ways. Because you don&rsquo;t have automated tests, you will keep fixing the same things many times over. Debt will catch up, and you will pay. One way or the other.</p>
<p>Minimal test suites are like <strong>Insurance</strong>. You don&rsquo;t need it right now, on day-1. But on day-100 you will be so glad you have it.</p>
<p>Doing too much microservices on day-1 is debt. It feels great that you&rsquo;re doing that fancy thing you read in a blog post. On day-100, with your 3 people team and a dozen microservices, from now on you will do nothing but pay the accumulated debt, with <strong>Interest</strong>. Every new microservices deployment breaks your system. And of course you didn&rsquo;t add monitoring, you didn&rsquo;t add integrated tests. So every time you code something, something else unexpectedly breaks.</p>
<p>Enjoying that early Lamborghini now?</p>
<h3>The Mythical Man-Month<span class="hx:absolute hx:-mt-20" id="the-mythical-man-month"></span>
    <a href="#the-mythical-man-month" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Philip Calçado writes very good posts and presentations about microservices in the right way. If you&rsquo;re serious about that subject I strongly recommend that you read his posts, such as his <a href="http://philcalcado.com/2017/06/11/calcados_microservices_prerequisites.html"target="_blank" rel="noopener">&ldquo;Prerequisites&rdquo;</a> post or his <a href="https://www.infoq.com/news/2017/05/economics-microservices"target="_blank" rel="noopener">&ldquo;Economics&rdquo;</a> presentation.</p>
<p>He correctly remembers Fred Brooks&rsquo; <strong>The Mythical Man-Month</strong>. I urge developers to read this small book. It&rsquo;s uncanny how the entire industry is still repeating the very same mistakes Brooks reports in his book from projects in the 60&rsquo;s!</p>
<p>To me, microservices is a by-product of tech companies with too many developers. Once you break through the 5-developers team a &ldquo;monolithic&rdquo; system with poor organization and few automated tests can become cumbersome to deal with. So the consequence is the desire to break it down. You make 2 teams and 2 microservices and coordinate. And the teams start to isolate, and play the finger-pointing game when new bugs are reported (<em>&ldquo;it&rsquo;s the other team&rsquo;s microservice&rsquo;s fault&rdquo;</em>).</p>
<p>That&rsquo;s <a href="http://www.melconway.com/Home/Conways_Law.html"target="_blank" rel="noopener">Conway&rsquo;s Law</a> if you do it wrong:</p>
<blockquote>
  <p>Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization&rsquo;s communication structure.</p>

</blockquote>
<p>Also in play is another chapter in Brooks&rsquo; book: The <a href="http://wiki.c2.com/?SecondSystemEffect"target="_blank" rel="noopener">Second System Effect</a>. Every developer, tech startup, has to assume that the first version of anything is not the best design. And the second system (the first big rewrite) is <strong>the most dangerous</strong> system you will write. As Brooks states, it goes like this:</p>
<blockquote>
  <p>An architect&rsquo;s first work is apt to be spare and clean. He knows he doesn&rsquo;t know what he&rsquo;s doing, so he does it carefully and with great restraint.</p>

</blockquote>
<blockquote>
  <p>As he designs the first work, frill after frill and embellishment after embellishment occur to him. These get stored away to be used &ldquo;next time.&rdquo; Sooner or later the first system is finished, and the architect, with firm confidence and a demonstrated mastery of that class of systems, is ready to build a second system.</p>

</blockquote>
<blockquote>
  <p>This second is the most dangerous system a man ever designs. When he does his third and later ones, his prior experiences will confirm each other as to the general characteristics of such systems, and their differences will identify those parts of his experience that are particular and not generalizable.</p>

</blockquote>
<blockquote>
  <p>The general tendency is to over-design the second system, using all the ideas and frills that were cautiously sidetracked on the first one. The result, as Ovid says, is a &ldquo;big pile.&rdquo;</p>

</blockquote>
<p>Then, what happens? The CEO, the board, the investors, or whatever, start doing the wrong thing: hiring more and more people. Loan more and more. Instead of paying the debts they end up making more debt. They fell for what Brooks reported in the first chapter and what gives the name to the book: The Mythical Man-Month.</p>
<blockquote>
  <p>Adding manpower to a late software project makes it later.</p>

</blockquote>
<blockquote>
  <p>Complex programming projects cannot be perfectly partitioned into discrete tasks that can be worked on without communication between the workers and without establishing a set of complex interrelationships between tasks and the workers performing them.</p>

</blockquote>
<blockquote>
  <p>Therefore, assigning more programmers to a project running behind schedule will make it even later. This is because the time required for the new programmers to learn about the project and the increased communication overhead will consume an ever increasing quantity of the calendar time available.</p>

</blockquote>
<p>Again, it&rsquo;s foregoing the sense of the &ldquo;Time&rdquo; variable. And confusing &ldquo;debt&rdquo; for &ldquo;investment&rdquo;.</p>
<p>First of all, do yourself a favor and <a href="http://amzn.to/2sFbkWq"target="_blank" rel="noopener">read the goddamn book</a>, twice.</p>
<h3>Do Agile right! And no, it&rsquo;s not Kanban!<span class="hx:absolute hx:-mt-20" id="do-agile-right-and-no-its-not-kanban"></span>
    <a href="#do-agile-right-and-no-its-not-kanban" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If there is one good thing the whole Lean noise generated is the notion of &ldquo;Minimum Viable Product&rdquo; or MVP. People call it &ldquo;prototype&rdquo;. Some call it &ldquo;launching Beta&rdquo; or simply &ldquo;version 1.0&rdquo;. Doesn&rsquo;t matter. It&rsquo;s the realization that you don&rsquo;t know much at the beginning so overengineering on the first version is a waste of time. Lean is about controlling &ldquo;waste&rdquo;, so we do the minimal that works, gauge results and work from there.</p>
<p>The cynical will say that any prototype that reaches production will never die. And they&rsquo;re not wrong.</p>
<p>The balance is to not do the &ldquo;quick and dirty&rdquo; version. That&rsquo;s what we have Agile techniques for. Do the minimal, organize the minimal. That&rsquo;s why we have object-oriented design patterns, from GoF to DDD. You don&rsquo;t need to do EVERY pattern, and that&rsquo;s exactly what &ldquo;overengineering&rdquo; means. But you can do the minimal monolith that will allow you to evolve later.</p>
<p>People criticize Ruby on Rails for not being organized &ldquo;enough&rdquo;. <a href="https://apotonick.wordpress.com/2015/09/05/the-only-alternative-to-a-rails-monolith-are-micro-services-bullshit/"target="_blank" rel="noopener">Nick Sutterer</a>, the creator of <a href="https://github.com/trailblazer/trailblazer"target="_blank" rel="noopener">Trailblazer</a>, has a good point.</p>
<p>Rails done wrong is bad. The conclusion should NOT be: <em>&ldquo;let&rsquo;s do microservices&rdquo;</em> though. That jump in faith is idiotic and makes no sense for all the reasons I mentioned above.</p>
<p><em>&ldquo;Let&rsquo;s do proper Agile stuff and proper object-oriented stuff&rdquo;</em> should be the initial answer.</p>
<p>And by &ldquo;Agile&rdquo; forget about idiotic post-its, idiotic numerology-based estimation equations (this is a whole post in itself, because story points and velocity are useful, but adding Montecarlo and other Gaussian-based stuff are not). The only &ldquo;Agile&rdquo; things you should be concerned about are the <a href="http://www.extremeprogramming.org/"target="_blank" rel="noopener">Extreme Programming (XP) techniques</a>, <strong>including</strong> Iteration-based timeboxes.</p>
<p>You <strong>MUST</strong> do timeboxes. Stop, re-assess, change directions, and then keep going. The model of &ldquo;Pull&rdquo; is only reasonable when your direction is very, very clear, written in stone and unchangeable, like in a factory production line! (Which is exactly where the concept of Pull, and Lean, was actually born!)</p>
<p>Iterations, like automated tests, are like Insurance. You can never avoid all waste, but you can minimize it. You can afford to throw away an Iteration-worth of work. After the iteration you measure the results, and throw it away if necessary, changing direction in the process. Throwing useless stuff away is as important as adding new stuff. If you just add, you have a <a href="https://en.wikipedia.org/wiki/Compulsive_hoarding"target="_blank" rel="noopener">hoarding disorder</a>!</p>
<p>If in doubt, do XP. Yes, it feels more &ldquo;difficult&rdquo;, and Kanban is &ldquo;simpler&rdquo; to explain. Now, is that a good reason?</p>
<h3>Forget about Raw Performance when you DON&rsquo;T need it!<span class="hx:absolute hx:-mt-20" id="forget-about-raw-performance-when-you-dont-need-it"></span>
    <a href="#forget-about-raw-performance-when-you-dont-need-it" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Another problem is choosing languages or framework because of performance.</p>
<p>If you&rsquo;re in the web development business, this is a huge WASTE.</p>
<p>Understand this truth: your servers will IDLE most of the time. And if you feel like using your web app is slow, the slowness comes from the POOR programming you did, not from the language. And no good language will rescue a bad programmer. I always say that if performance was that important, we should all be doing C.</p>
<p>Most of what you serve in HTTP-based apps, be it user-readable content, be it API GET results, can be <strong>CACHED</strong>! If you&rsquo;re not using a <a href="http://www.akitaonrails.com/2015/08/25/small-bites-adicionando-um-cdn-ao-seu-site-a-forma-facil"target="_blank" rel="noopener">CDN</a>, you&rsquo;re doing it wrong.</p>
<p>Yeah, yeah, yeah, you think you&rsquo;re building the next Spotify. You&rsquo;re not, at least not 99% of you. And the 1% doing custom engineering, with custom techniques and custom stack, successfully: you&rsquo;re the 1%. Actually I&rsquo;d say you&rsquo;re a fraction of the 1%. Do not assume that what you do is good for the rest of the population.</p>
<p>90% of what most small companies and solo web developers need is just a Shopify account and a vanilla Wordpress installation. And that&rsquo;s it.</p>
<blockquote>
  <p>Are you doing infrastructure development like building custom components to Docker, Kubernetes, Terraform? Command-line tools? Daemons? Then choose GO.</p>

</blockquote>
<blockquote>
  <p>Are you doing embedded libraries? Maybe the next generation OpenSSL? Drivers? Then choose RUST.</p>

</blockquote>
<blockquote>
  <p>Are you doing mobile development? Then don&rsquo;t have a lot of choice: do Swift for iOS, do Kotlin/Java for Android. Or do React Native for simpler apps.</p>

</blockquote>
<blockquote>
  <p>Are you really doing the next Whatsapp? The next Waze? The next Snap? You have hundreds or thousands of users in need of long-lived connections over unreliable network doing broadcasts of messages? Or you&rsquo;re building the next evolution of distributed NoSQL databases? Or anything that actually has the proper meaning of &ldquo;Distributed&rdquo; in its definition? Then choose ELIXIR.</p>

</blockquote>
<blockquote>
  <p>Are you doing a CRUD based web application? Go RAILS and never look back.</p>

</blockquote>
<blockquote>
  <p>Are you doing all of the above? Use all the alternatives then. And I hope you have the budget, because you need a big team.</p>

</blockquote>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The Economics only makes sense when you consider the day-1. Do not forget the &ldquo;Time&rdquo; variable. It makes all the difference.</p>
<p>You will always get Debt to pay. So be smart in which kind of debt you choose. Because you will have to pay it back eventually.</p>
<p>Again, read Brooks. He said it decades ago: <a href="http://worrydream.com/refs/Brooks-NoSilverBullet.pdf"target="_blank" rel="noopener">THERE IS NO SILVER BULLET</a>. Don&rsquo;t bullshit yourself. A new language, a new framework, a new architecture. None of those will save you.</p>
<p>In any tech endeavor, code is not the only thing to worry about. I would go as far as to say that in a tech startup, code is only 20% of the problem. A tech startup is just a company, like any other. If you&rsquo;re the founder or CEO, you have to deal with all the remaining 80%: marketing, accounting, human resources, legal, etc. It&rsquo;s already difficult enough without your tech team getting you unwanted Technical Debt, the unneeded Lamborghini, that you won&rsquo;t be able to pay.</p>
<p>You can start humble on day-1. Keep evolving, continuously. This is the core of any Agile or Lean process: <a href="https://www.graphicproducts.com/articles/what-is-kaizen/"target="_blank" rel="noopener">KAIZEN</a>. Choose smart debts, pay them a small bit at a time, continuously. It&rsquo;s the same reasoning as getting a loan in the bank.</p>
<blockquote>
  <p>Netflix day-7,200 is NOT your day-1.</p>

</blockquote>
<blockquote>
  <p>Facebook day-4,800 is NOT your day-1.</p>

</blockquote>
<blockquote>
  <p>Google day-6,809 is NOT your day-1.</p>

</blockquote>
<blockquote>
  <p>Be humble. Deliver fast. Pay your Debts. Keep evolving continuously. Stop believing in fairy tales and silver bullets.</p>

</blockquote>
]]></content:encoded><category>software-engineering</category><category>agile</category><category>software-architecture</category><category>off-topic</category></item><item><title>Programmers Guild - Religion and Sports</title><link>https://akitaonrails.github.io/en/2017/05/14/programmers-guild-religion-and-sports/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2017/05/14/programmers-guild-religion-and-sports/</guid><pubDate>Sun, 14 May 2017 13:45:00 -0300</pubDate><description>&lt;p&gt;DHH did a very good opening keynote to RailsConf'17 explaining about how bullshitty the current trend of developers&amp;rsquo; delusion is — that they are making &amp;ldquo;rational&amp;rdquo;, &amp;ldquo;fact-based&amp;rdquo; choices when in reality they are mostly attaching to the underlying &amp;ldquo;values&amp;rdquo; of some community. Something more akin to &amp;ldquo;religion&amp;rdquo; (faith-based choices) than proper science.&lt;/p&gt;
&lt;div class="embed-container"&gt;
&lt;iframe
src="https://www.youtube.com/embed/Cx6aGMC6MjU"
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&gt;
&lt;/iframe&gt;
&lt;/div&gt;
&lt;p&gt;My language is &amp;ldquo;faster&amp;rdquo;. My framework is &amp;ldquo;more elegant&amp;rdquo;. My tool is &amp;ldquo;more productive&amp;rdquo;. My community is &amp;ldquo;more friendly&amp;rdquo;. All &lt;strong&gt;bullshitty&lt;/strong&gt; reasons to choose something because those reasons don&amp;rsquo;t really matter.&lt;/p&gt;</description><content:encoded><![CDATA[<p>DHH did a very good opening keynote to RailsConf'17 explaining about how bullshitty the current trend of developers&rsquo; delusion is — that they are making &ldquo;rational&rdquo;, &ldquo;fact-based&rdquo; choices when in reality they are mostly attaching to the underlying &ldquo;values&rdquo; of some community. Something more akin to &ldquo;religion&rdquo; (faith-based choices) than proper science.</p>


<div class="embed-container">
  <iframe
    src="https://www.youtube.com/embed/Cx6aGMC6MjU"
    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>My language is &ldquo;faster&rdquo;. My framework is &ldquo;more elegant&rdquo;. My tool is &ldquo;more productive&rdquo;. My community is &ldquo;more friendly&rdquo;. All <strong>bullshitty</strong> reasons to choose something because those reasons don&rsquo;t really matter.</p>
<p>Although DHH did a good job describing the reality of developers&rsquo; choices, he didn&rsquo;t go as far as to explain <strong>why</strong> we all do it this way. Including himself. Including myself.</p>
<p>The religion framework theory does explain some of the behaviors, I&rsquo;d go farther still.</p>
<p>Most developers are more akin to sports fans (or <strong>fanatics</strong>, as is the root of the word &ldquo;fan&rdquo;).</p>
<p>Check out some items from the article <a href="http://waitbutwhy.com/2014/03/sports-fans-sports-fans.html"target="_blank" rel="noopener">&ldquo;Why Sports Fans are Sports Fans&rdquo;</a>:</p>
<ul>
<li>There&rsquo;s high drama.</li>
<li>It&rsquo;s fun to watch greatness.</li>
<li>It taps into our creepy side that wants to sit in the ancient Roman Coliseum and watch people fight to the death.</li>
<li>It&rsquo;s aesthetically pleasing.</li>
<li>The sports world includes a lot of gossip.</li>
<li>Sports provides an escape from life.</li>
<li>It&rsquo;s a perfect tool for your <a href="http://waitbutwhy.com/2013/10/why-procrastinators-procrastinate.html"target="_blank" rel="noopener">crippling procrastination tendencies</a>.</li>
<li>Sometimes I think we might even use sports as a way to attach ourselves to something immortal.</li>
<li>In a time when heroic triumphs aren&rsquo;t part of most people&rsquo;s lives, sports allows us to capture a little sliver of the feeling of glory.</li>
</ul>
<p>Developer niches feel more like sports teams in competition. There must be &ldquo;winners&rdquo; and &ldquo;losers&rdquo;. And people cheer over every small victory (like useless synthetic benchmarks or pseudo-statistical and meaningless indexes such as the TIOBE).</p>
<p>Fans of losing teams feel like they need to have increased loyalty to get the utmost reward.</p>
<p>Testosterone levels rise when discussing nonsense over Hacker News. Threads get really deep, each person shooting bullshitty &ldquo;evidence&rdquo; to &ldquo;prove&rdquo; their point. And every argument has an easy counter-argument, and the threads go on and on indefinitely. And every person writing will never agree to the contrary. And no one will ever switch sides based on the arguments laid out on those threads. Yet, they still waste lots of time writing anyway. Madness.</p>
<p>People - especially young people - like to have a sense of belonging, a sense of deindividuation, dissolved responsibilities. Which is some of what I wrote a couple years ago on the <a href="http://www.akitaonrails.com/2015/10/28/personal-thoughts-on-the-current-functional-programming-bandwagon"target="_blank" rel="noopener">current nonsense trend on functional languages</a>.</p>
<blockquote>
  <p>As Mike Perry describes in this post the most passionate fans tend to identify themselves as an integral part of the team they are supporting, with some of them – clearly – feeling less responsible for their individual behaviour. When disinhibition is combined with deindividuation – a loss of self-awareness and a sense of diffused responsibility – fans simply don&rsquo;t care anymore about what anyone else might think or feel. Deindividuation weakens normal restraints without which, fans are more easily influenced by their surroundings; this is when fans start acting – really – weird, in either a pleasant or less pleasant way.</p>

</blockquote>
<blockquote>
  <p>Groups of fans may mob, insult, threat or even riot. Groups provide fans with a sense of anonymity. Being part of a group removes individual accountability and diffuses responsibility. Unfortunately this behaviour appears online just as often as it does in the stadium.</p>

</blockquote>
<blockquote>
  <p>Sports Networker - <a href="http://www.sportsnetworker.com/2012/02/15/the-psychology-of-sports-fans-what-makes-them-so-crazy/"target="_blank" rel="noopener">Read more</a></p>

</blockquote>
<p>Tech conferences are like the weekend sports games. People like to feel validated being in the middle of equal-minded groups.</p>
<p>Same as with sports, developers feel that their &ldquo;teams&rdquo; (technologies, companies) are extensions of themselves. Worse, they feel like they describe their personalities. Which is why developers respond so passionately to contrary arguments: it feels like an aggression to their own selves.</p>
<blockquote>
  <p>&ldquo;When we look at motivation for following a sport team, group affiliation is one of the top ones,&rdquo; says Wann. &ldquo;Identifying strongly with a salient local team where other fans are in the environment — that&rsquo;s a benefit to social-psychological well-being.&rdquo;</p>

</blockquote>
<blockquote>
  <p>&ldquo;The whole idea behind identification is that it&rsquo;s really part of how we see ourselves and that doesn&rsquo;t change easily,&rdquo; says Robert J. Fisher, professor of marketing at The University of Western Ontario, whose research emphasizes the effects of social expectations on managerial and consumer decision making. &ldquo;If you see yourself as a member of a family, that role doesn&rsquo;t change. Those types of connections are very long-lasting and very strong.&rdquo;</p>

</blockquote>
<blockquote>
  <p>Psychological Science - <a href="https://www.psychologicalscience.org/observer/sports-complex-the-science-behind-fanatic-behavior#.WRm0CHVierx"target="_blank" rel="noopener">Read More</a></p>

</blockquote>
<p>It&rsquo;s all a big show, you&rsquo;re the audience. Just don&rsquo;t fool yourself thinking that you&rsquo;ve been &ldquo;scientific&rdquo; in your choices.</p>
<p>We are all victims of heavy <strong>Cognitive Bias</strong>, which blinds us to the truth:</p>
<blockquote>
  <p><a href="http://blogs.scientificamerican.com/guest-blog/2011/09/22/cognitive-biases-in-sports-the-irrationality-of-coaches-commentators-and-fans/"target="_blank" rel="noopener">Here&rsquo;s a great post</a> by Sam McNerney (<a href="http://twitter.com/WhyWeReason"target="_blank" rel="noopener">@WhyWeReason</a>) explaining cognitive bias. This is the phenomenon where all of us only look for what confirms their intuitions and ignore what contradicts their intuitions, what psychologists call confirmation bias. Confirmation bias helps explain why some fans will boo every single referee call against their team and English soccer fans will – still – argue with Germans about <a href="http://www.youtube.com/watch?v=__MJV11nRqU"target="_blank" rel="noopener">whether the ball did or did not pass the line in 1966</a>(!). Fans automatically see the world as they want to, not how it is.</p>

</blockquote>
<blockquote>
  <p>Sports Networker - <a href="http://www.sportsnetworker.com/2012/02/15/the-psychology-of-sports-fans-what-makes-them-so-crazy/"target="_blank" rel="noopener">Read more</a></p>

</blockquote>
<p>More interestingly, the same way with sports fans of losing teams, people find ways to cope. And superstition is one of them.</p>
<blockquote>
  <p>Superstitions are an integral part of sports, and they may also be yet another way fans cope with their team&rsquo;s performance.</p>

</blockquote>
<blockquote>
  <p>In ongoing research, Wann and his colleagues are exploring the role of fan superstition. Over half of his 1,000 participants can readily define a superstition or ritual they believe in. Moreover, some are truly convinced that their participation in ritual superstition impacts the outcome, says Wann. The more highly identified with a team the fans are, the more likely they are to believe that superstitions matter.</p>

</blockquote>
<blockquote>
  <p>&ldquo;It&rsquo;s a real struggle that sports fans experience,&rdquo; says Wann. &ldquo;They so much care about the outcome of the event they have absolutely zilch control over. How do we gain control? We may develop superstitions.&rdquo;</p>

</blockquote>
<blockquote>
  <p>Psychological Science - <a href="https://www.psychologicalscience.org/observer/sports-complex-the-science-behind-fanatic-behavior#.WRm0CHVierx"target="_blank" rel="noopener">Read More</a></p>

</blockquote>
<p>What I always have to say of all the nonsense is:</p>
<ul>
<li>
<p>If being the &ldquo;fastest&rdquo; was really a real concern, we would all be programming in C, as close to the bare metal as possible.</p>
</li>
<li>
<p>If being the &ldquo;most secure&rdquo; was really a real concern, we would never ever fetch libraries created by unknown, uncertified, untrained people. We would build everything from scratch, by ourselves, every time, and wait through months of heavy testing before ever delivering to production. You know, like the military used to do.</p>
</li>
<li>
<p>If being the &ldquo;most elegant&rdquo; was really a real concern, we would not be concerned about being the fastest or the most secure, as they usually go in opposite directions.</p>
</li>
</ul>
<p>The craft of an engineer, or any professional for that matter, is being able to choose between different compromises, and keep balancing out. Never blindly committing loyalty to a specific group or way of thinking. Amateurs commit loyalty to someone else. Professionals hold themselves responsible for their own choices, and they compromise.</p>
<p>Scalability, Maintainability, Reliability, every &ldquo;-ability&rdquo; we add to the list is a compromise. You can&rsquo;t have your cake and eat it too. You want more, you pay for more. Because every technology is a combination of compromises, you can&rsquo;t have a clear single winner or loser. Everything will win in some aspects and lose in others.</p>
<p>As Neo realizes in The Matrix: the problem is choice. The problem is always about choice. People don&rsquo;t like to choose, because that makes them <strong>accountable</strong>. It&rsquo;s far easier to make someone else make the choice and just follow, creating the delusion that you made a &ldquo;rational&rdquo; choice because &ldquo;the group&rdquo; validates it.</p>
<blockquote>
  <p>After all, <a href="https://www.quora.com/What-does-the-phrase-Nobody-ever-got-fired-for-choosing-IBM-mean"target="_blank" rel="noopener">&ldquo;Nobody ever got fired for choosing IBM.&rdquo;</a></p>

</blockquote>
<p>The reality that you will eventually realize is that you are not required to make &ldquo;the&rdquo; choice.</p>
<p>Clear &ldquo;good guys&rdquo; and &ldquo;bad guys&rdquo; only exist in fairy tales and bed-time stories for children. The world of Adults is defined by shades of grey.</p>
<p>At the end of the day, DHH is right. It doesn&rsquo;t matter the tech specs of <a href="http://www.techradar.com/news/xbox-one-s-vs-ps4-pro-vs-nintendo-switch-which-is-better"target="_blank" rel="noopener">PS4 Pro vs Xbox One S vs Nintendo Switch</a>. It doesn&rsquo;t matter the amount of teraflops. It doesn&rsquo;t matter the bandwidth. It doesn&rsquo;t matter native 4K or checkerboarding 4K. If I like Zelda, I will play in the Switch. If I like Forza I will play an XBox. If I like Uncharted I will play a PS4. And if I can afford it, I will have all of them. If I can&rsquo;t afford it, I will choose whatever I want to play right now knowing that I can switch to another platform without having to justify myself through tech specs.</p>
<p>Either I subscribe to the <a href="http://rubyonrails.org/doctrine/"target="_blank" rel="noopener">Rails Doctrine</a> or to <a href="https://www.python.org/dev/peps/pep-0020/"target="_blank" rel="noopener">The Zen of Python</a>. It doesn&rsquo;t matter. Can I play Zelda on it? Otherwise, any &ldquo;evidence&rdquo; you think you have to counter argue my choice is utter bullshit. Don&rsquo;t even try.</p>
<p>But of course, you should use Ruby if you&rsquo;re a rational developer ;-)</p>
]]></content:encoded><category>psychology</category><category>philosophy</category><category>software-engineering</category></item><item><title>Ex Manga Downloadr - Part 5: Making it more robust!</title><link>https://akitaonrails.github.io/en/2015/12/06/ex-manga-downloadr-part-5-making-it-more-robust/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2015/12/06/ex-manga-downloadr-part-5-making-it-more-robust/</guid><pubDate>Sun, 06 Dec 2015 19:20:00 -0200</pubDate><description>&lt;p&gt;And there I go again. I know some of you may be bored by this tool already, but as a playground project, I still want to make this good code. But there are 2 big problems right now.&lt;/p&gt;
&lt;p&gt;When I was testing only with MangaReader.net as a source, everything worked almost flawlessly. But adding MangaFox in &lt;a href="http://www.akitaonrails.com/en/2015/12/02/ex-manga-downloadr-parte-3-suporte-ao-mangafox"target="_blank" rel="noopener"&gt;Part 3&lt;/a&gt;, with its more restrictive rules towards scraper tools like mine (timing out more frequently, not allowing too many connections from the same place, etc), the process just kept crashing and I had to manually restart it (the resuming features I added in &lt;a href="http://www.akitaonrails.com/en/2015/12/03/ex-manga-downloadr-parte-4-aprendendo-atraves-do-refactoring"target="_blank" rel="noopener"&gt;Part 4&lt;/a&gt; paid off, but it&amp;rsquo;s not a reliable tool anymore).&lt;/p&gt;</description><content:encoded><![CDATA[<p>And there I go again. I know some of you may be bored by this tool already, but as a playground project, I still want to make this good code. But there are 2 big problems right now.</p>
<p>When I was testing only with MangaReader.net as a source, everything worked almost flawlessly. But adding MangaFox in <a href="http://www.akitaonrails.com/en/2015/12/02/ex-manga-downloadr-parte-3-suporte-ao-mangafox"target="_blank" rel="noopener">Part 3</a>, with its more restrictive rules towards scraper tools like mine (timing out more frequently, not allowing too many connections from the same place, etc), the process just kept crashing and I had to manually restart it (the resuming features I added in <a href="http://www.akitaonrails.com/en/2015/12/03/ex-manga-downloadr-parte-4-aprendendo-atraves-do-refactoring"target="_blank" rel="noopener">Part 4</a> paid off, but it&rsquo;s not a reliable tool anymore).</p>
<p>To recap, the Workflow just organizes each step of the process. Its functions are similar to this:</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">process_downloads</span><span class="p">(</span><span class="n">images_list</span><span class="p">,</span> <span class="n">directory</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">images_list</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Worker</span><span class="o">.</span><span class="n">page_download_image</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">,</span> <span class="n">directory</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="no">Task</span><span class="o">.</span><span class="n">await</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">,</span> <span class="vi">@await_timeout_ms</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="n">directory</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>It deals with a large list, maps over each element sending it to a Worker function to run, like this:</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">page_download_image</span><span class="p">(</span><span class="n">image_data</span><span class="p">,</span> <span class="n">directory</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Task</span><span class="o">.</span><span class="n">async</span><span class="p">(</span><span class="n">fn</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="ss">:poolboy</span><span class="o">.</span><span class="n">transaction</span> <span class="ss">:worker_pool</span><span class="p">,</span> <span class="n">fn</span><span class="p">(</span><span class="n">server</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="no">GenServer</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">server</span><span class="p">,</span> <span class="p">{</span><span class="ss">:page_download_image</span><span class="p">,</span> <span class="n">image_data</span><span class="p">,</span> <span class="n">directory</span><span class="p">},</span> <span class="vi">@genserver_call_timeout</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span><span class="p">,</span> <span class="vi">@task_async_timeout</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</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>It returns an asynchronous Task waiting for 2 things: for Poolboy to release a free process to use, and for the Worker/GenServer function to finish running inside that process. As I explained in <a href="http://www.akitaonrails.com/en/2015/11/19/ex-manga-downloadr-parte-2-poolboy-ao-resgate"target="_blank" rel="noopener">Part 2</a> this is so we can limit the maximum number of connections to the external source. If we didn&rsquo;t have this restriction, sending tens of thousands of asynchronous requests at once, the external source would just fail them all.</p>
<p>The first thing to bear in mind is that a &ldquo;<tt>Task.async/2</tt>&rdquo; links itself to the caller process, so if something goes wrong, the parent process dies as well.</p>
<p>The correct thing to do is to add a <a href="http://elixir-lang.org/docs/stable/elixir/Task.Supervisor.html"target="_blank" rel="noopener">Task.Supervisor</a> and make it deal with each Task child. To do that, we can just add the Supervisor in our supervised tree at &ldquo;pool_management/supervisor.ex&rdquo;:</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="n">defmodule</span> <span class="no">PoolManagement</span><span class="o">.</span><span class="n">Supervisor</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">Supervisor</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="n">children</span> <span class="o">=</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">    <span class="n">supervisor</span><span class="p">(</span><span class="no">Task</span><span class="o">.</span><span class="n">Supervisor</span><span class="p">,</span> <span class="o">[[</span><span class="nb">name</span><span class="p">:</span> <span class="no">Fetcher</span><span class="o">.</span><span class="n">TaskSupervisor</span><span class="o">]]</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="ss">:poolboy</span><span class="o">.</span><span class="n">child_spec</span><span class="p">(</span><span class="ss">:worker_pool</span><span class="p">,</span> <span class="n">pool_options</span><span class="p">,</span> <span class="o">[]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</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>And we can replace the &ldquo;<tt>Task.async/2</tt>&rdquo; calls to &ldquo;<tt>Task.Supervisor.async(Fetcher.TaskSupervisor, &hellip;)</tt>&rdquo; like this:</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">page_download_image</span><span class="p">(</span><span class="n">image_data</span><span class="p">,</span> <span class="n">directory</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Task</span><span class="o">.</span><span class="n">Supervisor</span><span class="o">.</span><span class="n">async</span><span class="p">(</span><span class="no">Fetcher</span><span class="o">.</span><span class="n">TaskSupervisor</span><span class="p">,</span> <span class="n">fn</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="ss">:poolboy</span><span class="o">.</span><span class="n">transaction</span> <span class="ss">:worker_pool</span><span class="p">,</span> <span class="n">fn</span><span class="p">(</span><span class="n">server</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="no">GenServer</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">server</span><span class="p">,</span> <span class="p">{</span><span class="ss">:page_download_image</span><span class="p">,</span> <span class="n">image_data</span><span class="p">,</span> <span class="n">directory</span><span class="p">},</span> <span class="vi">@genserver_call_timeout</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span><span class="p">,</span> <span class="vi">@task_async_timeout</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</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>This still creates Tasks that we need to await on, and as before, if the function inside crashes, it still brings down the main process. Now my refactoring found a dead end.</p>
<p>This is the 2nd problem I mentioned in the beginning of the article: a <strong>flaw in my design</strong>.</p>
<p>Instead of just mapping through each element of a large list, I should have created an Agent based GenServer to keep the list as the state and make the entire Workflow system a new supervised GenServer. If fetching one URL crashed the GenServer, its supervisor would restart it and pick up the next element in the list.</p>
<p>But, as I am in no mood for this refactoring right now (it&rsquo;s Sunday afternoon) I will concentrate on a quick fix (yes, jerry-rigged patch), just so the function in the async call does not raise exceptions.</p>
<h3>OMG! It&rsquo;s a Try/Catch block!<span class="hx:absolute hx:-mt-20" id="omg-its-a-trycatch-block"></span>
    <a href="#omg-its-a-trycatch-block" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Turns out that everything I run inside the Poolboy processes are HTTP get requests through HTTPotion. Fortunately I had already refactored every HTTPotion get call into a neat macro:</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="n">defmacro</span> <span class="n">fetch</span><span class="p">(</span><span class="n">link</span><span class="p">,</span> <span class="k">do</span><span class="p">:</span> <span class="n">expression</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&#34;Fetching from </span><span class="si">#{</span><span class="n">unquote</span><span class="p">(</span><span class="n">link</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">unquote</span><span class="p">(</span><span class="n">link</span><span class="p">),</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">http_headers</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="n">body</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="n">headers</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="mi">200</span> <span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="ss">:ok</span><span class="p">,</span> <span class="n">body</span> <span class="o">|&gt;</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">gunzip</span><span class="p">(</span><span class="n">headers</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="n">unquote</span><span class="p">(</span><span class="n">expression</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">_</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span> <span class="ss">:err</span><span class="p">,</span> <span class="s2">&#34;not found&#34;</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>Now I only need to replace 1 line in this macro:</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="o">-</span>    <span class="k">case</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">unquote</span><span class="p">(</span><span class="n">link</span><span class="p">),</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">http_headers</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl"><span class="o">+</span>    <span class="k">case</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">retryable_http_get</span><span class="p">(</span><span class="n">unquote</span><span class="p">(</span><span class="n">link</span><span class="p">))</span> <span class="k">do</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>And define this new retryable logic in the main module:</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="n">defmodule</span> <span class="no">ExMangaDownloadr</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="nb">require</span> <span class="no">Logger</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1"># will retry failed fetches over 50 times, sleeping 1 second between each retry</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@max_retries</span>  <span class="mi">50</span>
</span></span><span class="line"><span class="cl">  <span class="vi">@time_to_wait_to_fetch_again</span> <span class="mi">1_000</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">retryable_http_get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span> <span class="k">do</span><span class="p">:</span> <span class="k">raise</span> <span class="s2">&#34;Failed to fetch from </span><span class="si">#{</span><span class="n">url</span><span class="si">}</span><span class="s2"> after </span><span class="si">#{</span><span class="vi">@max_retries</span><span class="si">}</span><span class="s2"> retries.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">retryable_http_get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">retries</span> <span class="p">\\</span> <span class="vi">@max_retries</span><span class="p">)</span> <span class="k">when</span> <span class="n">retries</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">try</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="no">Logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&#34;Fetching from </span><span class="si">#{</span><span class="n">url</span><span class="si">}</span><span class="s2"> for the </span><span class="si">#{</span><span class="vi">@max_retries</span> <span class="o">-</span> <span class="n">retries</span><span class="si">}</span><span class="s2"> time.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">response</span> <span class="o">=</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">http_headers</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">case</span> <span class="n">response</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">        <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="n">_</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="n">_</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="n">status</span> <span class="p">}</span> <span class="k">when</span> <span class="n">status</span> <span class="o">&gt;</span> <span class="mi">499</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="k">raise</span> <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">HTTPError</span><span class="p">{</span><span class="ss">message</span><span class="p">:</span> <span class="s2">&#34;req_timedout&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">        <span class="n">_</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">          <span class="n">response</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">rescue</span>
</span></span><span class="line"><span class="cl">      <span class="n">error</span> <span class="k">in</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">HTTPError</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="k">case</span> <span class="n">error</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">          <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">HTTPError</span><span class="p">{</span><span class="ss">message</span><span class="p">:</span> <span class="n">message</span><span class="p">}</span> <span class="k">when</span> <span class="n">message</span> <span class="k">in</span> <span class="vi">@http_errors</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="ss">:timer</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="vi">@time_to_wait_to_fetch_again</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">retryable_http_get</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">retries</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="n">_</span> <span class="o">-&gt;</span> <span class="k">raise</span> <span class="n">error</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><span class="line"><span class="cl">  <span class="o">...</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>I strongly <a href="http://www.akitaonrails.com/en/2015/12/01/o-obrigatorio-por-que-elixir-visao-pessoal"target="_blank" rel="noopener">stated</a> that in Elixir we should <strong>not</strong> use &ldquo;try/catch&rdquo; blocks, but there you have it.</p>
<p>This is the consequence of the flaw in my initial Workflow design. If I had coded the Workflow module to be a GenServer, with each list managed by an Agent, each failed HTTPotion call would allow the supervisor to restart it and try again. Without resorting to the ugly &ldquo;try/catch&rdquo; code.</p>
<p>Maybe this will force me to write Part 6 as being the code to remove this ugly try/catch later, so consider this a <strong>Technical Debt</strong> to make everything work now so we can refactor later and pay the debt back.</p>
<p>&ldquo;<tt>HTTPotion.get/2</tt>&rdquo; calls can raise &ldquo;HTTPotion.HTTPError&rdquo; exceptions. I am catching those errors for the time being, matching the messages against a list of errors I had already, sleeping for a certain amount of time (just a heuristic to see if the external sources respond better that way) and I recurse to itself through a limited number of &ldquo;retries&rdquo;, until it reaches zero, in which case it may even be the case that the internet connection is down or some other severe error that we would not be able to recover soon.</p>
<p>With this code in place, now even fetching from MangaFox, without tweaking down the POOL_SIZE, will run until the end, and this solves my needs for now. If anyone is interested in suggesting a better, GenServer based Workflow design, I would really appreciate a Pull Request.</p>
<p>Cheers.</p>
]]></content:encoded><category>elixir-and-erlang</category><category>software-engineering</category><category>testing</category></item></channel></rss>