{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "Marcello Oliveira · In Production",
  "home_page_url": "https://mrcll.me/",
  "feed_url": "https://mrcll.me/feed.json",
  "description": "Stories about shipping AI and software to production, in English and Portuguese.",
  "language": "en",
  "authors": [
    {
      "name": "Marcello Oliveira",
      "url": "https://mrcll.me/about/"
    }
  ],
  "items": [
    {
      "id": "https://mrcll.me/stories/prompt-injection-is-our-xss/",
      "url": "https://mrcll.me/stories/prompt-injection-is-our-xss/",
      "title": "Prompt injection is our generation’s XSS",
      "summary": "There is no textContent for a model: what XSS and XSRF teach about prompt injection, and why the defense moves from input to privilege.",
      "language": "en",
      "content_html": "<p><em>XSS and XSRF are the same flaw: a system that cannot tell data from instructions, or that acts with authority nobody asked for. LLMs inherited both.</em></p>\n<p>Picture a web app that keeps a JWT in localStorage, valid for two hours. One of its pages has an input, and whatever you type is shown to everyone who visits later. An attacker types a script into it. From then on every visitor’s browser runs that script and hands over the token, and the attacker can use their accounts for up to two hours.</p>\n<p>The fix is almost boring: write the text with textContent instead of innerHTML, or sanitize it before it reaches the DOM. The browser has a reliable way to say “this is text, not code”, and the hole closes.</p>\n<p>Its cousin, XSRF, is about a different mistake. Cookies go along with any request to a site, even when another site triggers it, and even through an invisible iframe that makes you click something you never saw. The browser acts with an authority the user did not grant in that click.</p>\n<p>Now look at an LLM assistant. It reads text from outside, tickets, emails, documents, web pages, and it usually has tools: send an email, query data, create records. Data and instructions travel in the same channel, and authority rides along without anyone asking. It is the old mix, both flaws at once.</p>\n<blockquote><p>Interactive figure (InjectionSandbox): open the web version at https://mrcll.me/stories/prompt-injection-is-our-xss/</p></blockquote>\n<p>With no defenses, the ticket becomes an order. Fencing the untrusted text helps until the attacker closes the fence themselves; try the third preset. What actually holds is taking the tool away from the task. Summarizing does not need send_email, and any side effect waits for a person to confirm it.</p>\n<h2>Nothing to escape with</h2>\n<p>XSS got solved because the browser can draw a hard line around text. A model cannot. You can ask, fence, repeat and warn, and there is still no guaranteed escape. So the defense has to move away from the input and onto privilege. What the model is allowed to do matters more than what it read.</p>\n<p>In my XSS demo a stolen token lasted two hours. For an agent with tools, the window is a single call. I treat everything a model reads as untrusted, including our own documents. I give each task the fewest tools it needs. And model output is rendered as text, never piped into innerHTML, SQL or a shell.</p>",
      "date_published": "2026-11-11T00:00:00.000Z",
      "date_modified": "2026-11-11T00:00:00.000Z",
      "tags": [
        "AI",
        "Backend",
        "Frontend"
      ],
      "image": "https://mrcll.me/og/en/story-prompt-injection-is-our-xss.png"
    },
    {
      "id": "https://mrcll.me/stories/bad-intuitive-statisticians/",
      "url": "https://mrcll.me/stories/bad-intuitive-statisticians/",
      "title": "We’re bad intuitive statisticians. So are LLMs.",
      "summary": "Monty Hall, a hostel in Foz do Iguaçu, and what temperature and top-p really do to an LLM’s next token.",
      "language": "en",
      "content_html": "<p><em>A hostel in Foz do Iguaçu, three doors and a goat taught me more about temperature and top-p than any model card.</em></p>\n<p>I was backpacking through Foz do Iguaçu with Kahneman’s <em>Thinking, Fast and Slow</em> in my bag when two other backpackers asked what it was about. I gave them the book’s most famous trap: a man is described as shy and helpful. Is he a librarian or a farmer?</p>\n<p>They both said librarian. Everyone does. But there are far more farmers than librarians, so the boring answer is the likely one. The description wins and the base rate loses. We are not intuitive statisticians, and the people who build with language models are no exception.</p>\n<p>A model does not look an answer up. At every step it holds a probability distribution over the next token and draws from it. Temperature and top-p do not make it more creative or more careful; they change the shape of that distribution before the draw. Most teams tune them the way my friends in the hostel guessed: by feel, from one demo.</p>\n<blockquote><p>Interactive figure (Sampler): open the web version at https://mrcll.me/stories/bad-intuitive-statisticians/</p></blockquote>\n<p>Drag temperature down to 0.1 and “tests” wins every single time: deterministic and confidently dull. Push it to 2 and “moon” starts turning up in your incident summary. Top-p is the guard rail. It cuts the tail before the draw, so a hot temperature cannot reach tokens that were never plausible. None of this shows in a single answer. It shows when you sample a thousand times, which is exactly what your users will do for you.</p>\n<h2>Three doors</h2>\n<p>Back in the hostel I pulled out Monty Hall. Three doors, one car, two goats. You pick a door. The host, who knows where the car is, opens another one and shows you a goat, then offers a switch. Both of them said it made no difference: two doors left, fifty-fifty. Play a round, then let the machine play a thousand.</p>\n<blockquote><p>Interactive figure (MontyHall): open the web version at https://mrcll.me/stories/bad-intuitive-statisticians/</p></blockquote>\n<p>Switching wins two times in three. Your first pick is right a third of the time, so it is wrong two thirds of the time, and the host has just shown you where not to look. His choice is evidence, and evidence changes the distribution. That is Bayes, and it is also the whole job with language models: the prompt, the retrieved documents and the examples are evidence that move what the model samples from. Temperature only decides how far into that distribution you let it reach.</p>\n<p>There was one more trap that night, and it is the one I now see most in evaluations. A Fluminense striker scores 1 of 3 shots in the league and 95 of 97 in the Libertadores. A Flamengo striker is better in both: 101 of 300 and 96 of 97. Add the totals and Fluminense wins, 96% to 50%. Simpson’s paradox. A model can beat the baseline in every slice of your eval and still lose overall, or the other way around.</p>\n<p>So I pick temperature per feature, from samples. I judge prompt changes on hundreds of runs, never on the one answer from the demo. And I split every eval by segment before I believe its total.</p>",
      "date_published": "2026-11-10T00:00:00.000Z",
      "date_modified": "2026-11-10T00:00:00.000Z",
      "tags": [
        "AI",
        "Fundamentals"
      ],
      "image": "https://mrcll.me/og/en/story-bad-intuitive-statisticians.png"
    },
    {
      "id": "https://mrcll.me/stories/floats-and-salaries/",
      "url": "https://mrcll.me/stories/floats-and-salaries/",
      "title": "0.1 + 0.2 and someone’s salary",
      "summary": "Why money is never a float: IEEE 754 bits, payroll drift and rate rounding, shown one payslip at a time.",
      "language": "en",
      "content_html": "<p><em>In a programming contest, an epsilon makes the bug go away. In payroll, an epsilon is a person who got paid one cent less.</em></p>\n<p>Here is a program that should print true:</p>\n<pre><code>double phi = (1 + sqrt(5)) / 2;\nprintf(1 / phi == phi - 1 ? \"true\" : \"false\");   /* false */</code></pre>\n<p>The golden ratio satisfies 1/φ = φ − 1 exactly. The computer disagrees in the sixteenth decimal, because the two sides reach the same number through different roundings. In competitive programming the fix is a reflex: compare with a tolerance, <code>fabs(x - y) &lt; 1e-9</code>, and move on to the next problem. I did that for years.</p>\n<p>To see why, look at what a double actually holds. 0.1 has no exact binary form, so the machine stores the closest value it can.</p>\n<blockquote><p>Interactive figure (FloatBits): open the web version at https://mrcll.me/stories/floats-and-salaries/</p></blockquote>\n<p>Then the numbers became salaries. Payroll across countries is additions, rates and roundings, millions of times a month, in currencies that do not even agree on how many decimals they have: the Japanese yen has none, the Kuwaiti dinar has three. Every rounding lands in someone’s account, and the totals must reconcile to the last unit. The scenario below is generic, not any company’s system. Add the same amount to a running total, payslip after payslip.</p>\n<blockquote><p>Interactive figure (PayrollDrift): open the web version at https://mrcll.me/stories/floats-and-salaries/</p></blockquote>\n<p>The drift is a tiny fraction of a cent. Round the total and it even looks right, which is the trap. The equality check fails, the reconciliation job raises an alarm, and someone adds an epsilon to silence it. From then on the epsilon is policy.</p>\n<h2>Where the cents really go</h2>\n<p>The money that actually goes missing comes from somewhere else: applying a rate, an FX conversion or an uplift, and rounding every line.</p>\n<blockquote><p>Interactive figure (RateRounding): open the web version at https://mrcll.me/stories/floats-and-salaries/</p></blockquote>\n<p>At a rate of 1.15, 2,376 of 100,000 lines round to the wrong cent with floating point. With integer cents and a decimal rate, none do. An epsilon cannot fix that, because nothing is “close enough” about a cent paid to the wrong person.</p>\n<p>The fix is a representation, not a comparison:</p>\n<ol><li>Store money as an integer count of minor units plus a currency, and take the number of decimals from the currency (ISO 4217), never from a global “2”.</li><li>Do rate math in a decimal type, choose one rounding rule (half-up or half-even), write it down, apply it once at a defined step, and property-test it against exact arithmetic.</li><li>Treat reconciliation as equality. If totals only match with an epsilon, the bug is upstream.</li></ol>",
      "date_published": "2026-11-09T00:00:00.000Z",
      "date_modified": "2026-11-09T00:00:00.000Z",
      "tags": [
        "Backend",
        "Fundamentals"
      ],
      "image": "https://mrcll.me/og/en/story-floats-and-salaries.png"
    },
    {
      "id": "https://mrcll.me/stories/tokens-are-compression/",
      "url": "https://mrcll.me/stories/tokens-are-compression/",
      "title": "Tokens are compression: what Shannon explains about your AI bill",
      "summary": "Shannon entropy, tokenizers and your AI bill: type any text and watch information, tokens and cost move together.",
      "language": "en",
      "content_html": "<p><em>Why some prompts cost twice as much as others, explained by a gigabyte of ones and a sentence about a fire.</em></p>\n<p>Every LLM invoice is a count of tokens, in and out. Teams look at it the way they look at a phone bill: a number that goes up. It is more interesting than that. A token count is a measurement of how surprising your text is.</p>\n<p>Start with a file where every byte is 1. Make it a gigabyte. How much information does it hold? Almost none. If you know it is all ones, you only need its length. Data and information are different things. Or take the sentence “I saw a fire, and it had flames.” Every fire has flames; the second half can go and nothing is lost. Information is surprise.</p>\n<p>A tokenizer works on that idea. Before a model reads anything, the text is compressed into tokens, pieces from a dictionary learned from the training data. Frequent pieces get a token of their own. Rare ones get spelled out in several. So the bill tracks surprise, not length. Try it:</p>\n<blockquote><p>Interactive figure (TokenMeter): open the web version at https://mrcll.me/stories/tokens-are-compression/</p></blockquote>\n<h2>The arithmetic of “euameiela”</h2>\n<p>Run the figure on “euameiela”. Nine characters, 72 bits on disk, and only about 21.8 bits of information, 2.419 bits per character. Roughly 70% of the space is wasted. A hand-built prefix code, one bit for the frequent “e”, longer codes for the rare letters, squeezes it to 22 bits. That is as low as it goes: bits are whole numbers and entropy is the floor.</p>\n<p>Byte-pair encoding is the same trick at scale. Instead of giving “e” a short code, it gives common chunks of text a single token. Then paste a UUID, a base64 blob or a minified JSON into the box above. Entropy per character climbs, the words stop looking like words, and the token count runs ahead of the character count.</p>\n<p>Language matters too. With mostly English training data, an English sentence usually costs fewer tokens than the same sentence in Portuguese. The two presets show it.</p>\n<p>What I do with this: I treat IDs, hashes and noisy JSON in prompts as the expensive part they are. I measure with the provider’s own tokenizer in every language we ship. And anything sent on every request gets cached, shortened or removed, because it is a line in the budget whether anyone reads it or not.</p>",
      "date_published": "2026-11-08T00:00:00.000Z",
      "date_modified": "2026-11-08T00:00:00.000Z",
      "tags": [
        "AI",
        "Fundamentals"
      ],
      "image": "https://mrcll.me/og/en/story-tokens-are-compression.png"
    },
    {
      "id": "https://mrcll.me/stories/q-learning-to-llm-agents/",
      "url": "https://mrcll.me/stories/q-learning-to-llm-agents/",
      "title": "From Q-learning to LLM agents",
      "summary": "From a Q-learning maze to LLM agents in production: in an agent, the reward is the design.",
      "language": "en",
      "content_html": "<p><em>The first agent I trained needed 799 episodes to escape a small maze. Production agents get no practice runs at all.</em></p>\n<p>The first agent I trained lived in a grid. One cell paid +1, one cost −1, one was a wall, and the rest was empty floor. It knew nothing. It learned from rewards alone, with the Bellman equation doing the bookkeeping:</p>\n<ul><li>at episode 0 it wandered;</li><li>at episode 199 it knew more, and still failed;</li><li>at episode 599 it was getting there;</li><li>by episode 799 it solved the maze every time.</li></ul>\n<p>Today “agent” almost always means a language model in a loop: read the state, pick a tool, look at the result, repeat. The vocabulary survived intact. Agent, environment, reward, policy. What did not survive is the training. Nobody gives you 800 episodes on real users before the agent has to behave.</p>\n<blockquote><p>Interactive figure (QMaze): open the web version at https://mrcll.me/stories/q-learning-to-llm-agents/</p></blockquote>\n<p>Train one episode at a time and watch the arrows spread backwards from the +1, like a stain. Set the step penalty to zero and the agent gets lazy: it still arrives, in no hurry at all. Set exploration to zero and it may settle for the first path that happened to work. The maze makes something obvious that is easy to forget with LLMs: the reward function is the design. Penalties are what make the agent finish quickly.</p>\n<p>With an LLM agent the policy arrives pre-trained, the model plus your prompt. The reward is still yours to write. It becomes the eval that scores the whole trajectory instead of only the final answer. The step penalty becomes the limit on steps, tokens and time. And one property of Q-learning carries over exactly: the values only stay right while the environment stays the same. Change a tool, an API or a schema and the old policy is stale until you run the evals again.</p>\n<p>799 episodes for a three-by-four grid. Your agent in production gets zero. Write the reward first.</p>",
      "date_published": "2026-11-07T00:00:00.000Z",
      "date_modified": "2026-11-07T00:00:00.000Z",
      "tags": [
        "AI"
      ],
      "image": "https://mrcll.me/og/en/story-q-learning-to-llm-agents.png"
    },
    {
      "id": "https://mrcll.me/stories/running-code-on-other-peoples-sites/",
      "url": "https://mrcll.me/stories/running-code-on-other-peoples-sites/",
      "title": "Running code on other people’s sites",
      "summary": "One tracking pixel, three sites, and what it takes to run your code as a guest on someone else’s website.",
      "language": "en",
      "content_html": "<p><em>I once built a script that lived inside our customers’ websites. The mechanism that tracks you across the web is the same one; the responsibility is what changes.</em></p>\n<p>At Social Miner I built and maintained URSA: a script our customers embedded through a pixel, the way you embed Google Tag Manager. It ran in a hostile environment, inside the browser sandbox of sites we did not control and could not test against in advance. Its onsite and offsite parts talked through postMessage, with a small pub/sub on top. Behavior events were batched in IndexedDB and shipped by a service worker, so nothing was lost when someone closed the tab. URSA also captured push-notification permission, which is what made our Push Notification product possible. I led a team of four on it and on the Channel Builder, built with .NET and React.</p>\n<p>Code in someone else’s page sees a lot. To understand how much, imagine a large company, call it MOZART, that wants to recognize the same person anywhere on the web without ever knowing who they are. It defines a person as a set of actions: visited the Palmeiras page on globoesporte at 6:30 pm, then the São Paulo state government site at 6:35 pm. From that it guesses: supports Palmeiras, lives in São Paulo, has free time around 6:30. To stitch the visits together it needs only two HTTP headers. The referer says where a request came from. A cookie, mozart.id, identifies the browser. Any site that loads mozart.com/external.png hands over both.</p>\n<blockquote><p>Interactive figure (TrackingPixel): open the web version at https://mrcll.me/stories/running-code-on-other-peoples-sites/</p></blockquote>\n<h2>A pixel is enough</h2>\n<p>Visit the three sites with everything allowed and watch the profile assemble. Then block third-party cookies: each request arrives anonymous and the visits stop linking. Trim the referer to the origin and “Palmeiras” disappears, because the page path no longer travels. Browsers have moved to that side, limiting third-party cookies and trimming cross-site referers by default.</p>\n<p>For code embedded in good faith that is not an obstacle. It is the contract. Every widget, SDK and embedded chat is a guest in someone else’s house, and a good guest:</p>\n<ul><li>stays isolated, in an iframe or Shadow DOM, with no globals and no leaking CSS;</li><li>relies only on what was offered, assumes no third-party cookies and a trimmed referer, and asks for anything else explicitly;</li><li>has a budget for weight and for failure, so that when it breaks the host page stays up.</li></ul>\n<p>A one-by-one image used to be enough to learn a team, a city and a time of day. What you do with what arrives is the whole difference.</p>",
      "date_published": "2026-11-06T00:00:00.000Z",
      "date_modified": "2026-11-06T00:00:00.000Z",
      "tags": [
        "Frontend",
        "Backend"
      ],
      "image": "https://mrcll.me/og/en/story-running-code-on-other-peoples-sites.png"
    },
    {
      "id": "https://mrcll.me/stories/progressive-enhancement-ai-ui/",
      "url": "https://mrcll.me/stories/progressive-enhancement-ai-ui/",
      "title": "Progressive enhancement for AI-generated UI",
      "summary": "Progressive enhancement for AI-generated UI: ship the core without the model and feature-detect the AI layer.",
      "language": "en",
      "content_html": "<p><em>Some people will not get the full experience. That was true of old browsers, and it is true again now that a model sits inside the interface.</em></p>\n<p>Does it really matter to use a feature that not every browser supports? We already accept different experiences on different devices. Why not on different browsers?</p>\n<p>The answer turns on one detail. People on an older browser may have no choice, or may not know how to upgrade. That is not their fault, so the core has to work everywhere and the rest comes on top: <code>@supports</code> to detect CSS features, Modernizr when you need more. Progressive enhancement is an old idea. It is about to be useful again.</p>\n<p>AI-generated UI adds a dependency that behaves worse than any browser ever did. It is sometimes slow, sometimes wrong, sometimes down, and sometimes switched off by the user. If the page only makes sense when the model answers, your provider’s worst day becomes your product’s worst day. The model is the least reliable layer you have ever shipped.</p>\n<p>Here is a card built in layers. Take them away one at a time.</p>\n<blockquote><p>Interactive figure (LayeredCard): open the web version at https://mrcll.me/stories/progressive-enhancement-ai-ui/</p></blockquote>\n<p>Turn JavaScript off and the facts are still there. Pretend the browser has no container queries and the filters stay, only stacked. Make the model fail and only the summary goes away; its reserved slot says so instead of collapsing the layout. Make the model slow and the text arrives in pieces, in a place that was already its own.</p>\n<p>Zero model calls are needed for the page to be useful. That is the whole design.</p>\n<p>The tools changed; the idea did not. Where <code>@supports</code> used to stand, there is now a timeout, a kill switch and a user setting. The core does not depend on the new layer, and the new layer announces itself when it arrives: streamed into a slot that was reserved for it, so nothing jumps, and labelled, so nobody mistakes generated text for the facts around it.</p>",
      "date_published": "2026-11-05T00:00:00.000Z",
      "date_modified": "2026-11-05T00:00:00.000Z",
      "tags": [
        "AI",
        "Frontend"
      ],
      "image": "https://mrcll.me/og/en/story-progressive-enhancement-ai-ui.png"
    },
    {
      "id": "https://mrcll.me/stories/why-i-wrote-my-own-renderer/",
      "url": "https://mrcll.me/stories/why-i-wrote-my-own-renderer/",
      "title": "Why I wrote my own renderer",
      "summary": "bradom: six rendering strategies written from scratch, from innerHTML to Virtual DOM and tagged templates, to make one tooling choice.",
      "language": "en",
      "content_html": "<p><em>Before picking the tooling for XP’s design system, I wanted to understand what it did inside. The result was bradom: six ways to render the same interface, written from scratch.</em></p>\n<p>When I had to choose the tools for the XP Inc. design system, none of them was a perfect fit. Every option had benefits and costs, and reading comparisons was not settling anything. So I raised a slightly cheeky possibility with the team: what if we built our own library? If others built one, why not us?</p>\n<p>That question became bradom, a study monorepo with six packages, each rendering the same interface a different way:</p>\n<ol><li>naive innerHTML;</li><li>Document Fragment;</li><li>a Virtual DOM without diffing;</li><li>a full Virtual DOM;</li><li>tagged template literals (left incomplete);</li><li>a React benchmark to keep me honest.</li></ol>\n<p>The code is on <a href=\"https://github.com/marcello0liveira/bradom\">GitHub</a>.</p>\n<h2>Where the time goes</h2>\n<p>The browser turns bytes into characters, tokens, nodes, DOM and CSSOM, combines them into the render tree, computes layout and paints. Changing the DOM can restart part of that path, and avoiding needless reflow is most of what rendering optimization means.</p>\n<p>The easiest approach, rewriting innerHTML, is also the worst. It throws away element state, like an input’s focus and the text in it, and it opens a door to XSS. There is a reason React calls its version dangerouslySetInnerHTML. Type into the filter below and watch what each strategy does to the list.</p>\n<blockquote><p>Interactive figure (RenderCost): open the web version at https://mrcll.me/stories/why-i-wrote-my-own-renderer/</p></blockquote>\n<p>Going from “ab” to “a”, innerHTML throws everything away and rebuilds it. The Virtual DOM lets you write code as if you were recreating everything, then compares the new tree with the old one and touches only what changed, so state survives. Its price is evaluating the whole tree on every change, including the parts that never change.</p>\n<h2>The cache nobody tells you about</h2>\n<p>Tagged templates have a quiet property. The first argument the tag function receives, the array of static strings, is the same object every time the same template runs. You can use it as a cache key. The HTML is already split into static parts and dynamic holes, so only the holes need re-evaluating. UI = f(state), recomputed only where the state changed.</p>\n<p>So did we ship bradom? No. For Soma, the design system, I chose a technology that was not my own. Writing the renderer was never about shipping it. It was about choosing with my hands instead of with blog posts, knowing exactly where the cost would be: in nodes evaluated and DOM operations, not in lines of code.</p>",
      "date_published": "2026-11-04T00:00:00.000Z",
      "date_modified": "2026-11-04T00:00:00.000Z",
      "tags": [
        "Frontend",
        "Fundamentals"
      ],
      "image": "https://mrcll.me/og/en/story-why-i-wrote-my-own-renderer.png"
    },
    {
      "id": "https://mrcll.me/stories/northeast-brasileirao/",
      "url": "https://mrcll.me/stories/northeast-brasileirao/",
      "title": "The Northeast and the Brasileirão",
      "summary": "Northeastern clubs in the Brasileirão: 44,006 km of travel for Fortaleza against 11,992 for São Paulo clubs in 2023.",
      "language": "en",
      "content_html": "<p><em>Two teams out of twenty. A share that was once half the table, and once zero.</em></p>\n<p>Five games in fifteen days. In 2022 Fortaleza hosted São Paulo, flew to play Vitória, Botafogo and Alianza Lima, and came home for Fluminense. That is 10,362 km in a straight line, and the flight is not always chartered.</p>\n<p>The 2023 Série A started with 2 Northeastern clubs out of 20, or 10%. It has been very different. In 1962, 9 of the clubs in the Taça Brasil, half the field, came from the Northeast. In 1967, in the Torneio Roberto Gomes Pedrosa, there were none. Today’s share sits closer to the region’s slice of GDP, 13.6%, than to its share of the population, 30%.</p>\n<p>There are many reasons for that, and distance weighs more than most, because the calendar never stops to let anyone recover. Here is what a season of travel looks like.</p>\n<blockquote><p>Interactive figure (TravelChart): open the web version at https://mrcll.me/stories/northeast-brasileirao/</p></blockquote>\n<p>To play the 2023 Série A, Fortaleza travels 44,006 km in a straight line. The São Paulo clubs travel 11,992.</p>\n<p>And still: Fortaleza became the first Northeastern club to play the Libertadores in consecutive years, 2022 and 2023, and in 2022 drew the two biggest crowds of the group stage, 50,429 paying fans against Colo-Colo and 48,867 against River Plate. Every league table hides a cost it does not show. In this one, it is the distance.</p>",
      "date_published": "2026-11-03T00:00:00.000Z",
      "date_modified": "2026-11-03T00:00:00.000Z",
      "tags": [
        "Off the code"
      ],
      "image": "https://mrcll.me/og/en/story-northeast-brasileirao.png"
    },
    {
      "id": "https://mrcll.me/stories/linear-recurrences-log-time/",
      "url": "https://mrcll.me/stories/linear-recurrences-log-time/",
      "title": "Fibonacci of 10¹⁸ in 84 multiplications",
      "summary": "Fibonacci of 10^18 in 84 matrix multiplications: linear recurrences in logarithmic time.",
      "language": "en",
      "content_html": "<p><em>Every linear recurrence can be written as matrix multiplication. And matrices can be raised to a power fast.</em></p>\n<p>What is the 10¹⁸th Fibonacci number, modulo something reasonable? A loop would still be running long after the contest ended. You get there with 84 multiplications.</p>\n<p>A linear recurrence is a sequence in which each term is a linear combination of the ones before it, and Fibonacci is the famous example. Any such step can be written as a matrix T times a vector of recent terms. One application of T moves the sequence forward by one. Two applications, T², move it by two. And T^(n−1), applied to f(0) and f(1), which you already know, lands on f(n).</p>\n<p>So the question becomes how fast you can raise a matrix to a power. You do not need n multiplications to reach xⁿ. From x you get x², from x² you get x⁴, and from x⁴ you get x⁸. Any exponent is a sum of those powers of two, read straight from its binary digits. Squaring turns n steps into about log n.</p>\n<blockquote><p>Interactive figure (Recurrence): open the web version at https://mrcll.me/stories/linear-recurrences-log-time/</p></blockquote>\n<p>The loop needs 10¹⁸ steps; the matrix needs 84 multiplications. When something grows one step at a time, look for the form that doubles. There almost always is one.</p>",
      "date_published": "2026-11-02T00:00:00.000Z",
      "date_modified": "2026-11-02T00:00:00.000Z",
      "tags": [
        "ICPC",
        "Fundamentals"
      ],
      "image": "https://mrcll.me/og/en/story-linear-recurrences-log-time.png"
    },
    {
      "id": "https://mrcll.me/stories/counting-inversions/",
      "url": "https://mrcll.me/stories/counting-inversions/",
      "title": "How many swaps until it is sorted?",
      "summary": "Counting inversions with Bubblesort and Mergesort, and why the same count compares two rankings.",
      "language": "en",
      "content_html": "<p><em>Counting inversions is a famous problem with many known solutions. Bubblesort counts by swapping; Mergesort counts without looking at every pair.</em></p>\n<p>Take [1,4,3,2] and run one pass of Bubblesort. Compare 1 and 4: leave them. Compare 4 and 3: swap. Compare 4 and 2: swap. You are left with [1,3,2,4], and one more swap finishes the job. Three swaps in total.</p>\n<p>That is not a coincidence. Every adjacent swap undoes exactly one inversion, a pair that is in the wrong order, so the number of swaps Bubblesort makes is the number of inversions. A counter next to the swap is the whole algorithm.</p>\n<p>It is also O(n²), which is fine for four numbers and useless for a million. Mergesort sorts in O(n log n), and it can count while it sorts. When the merge takes an element from the right half before what is left of the left half, that element jumps over every one of them, and each jump is an inversion. Add them up and you never look at a single pair directly.</p>\n<blockquote><p>Interactive figure (Inversions): open the web version at https://mrcll.me/stories/counting-inversions/</p></blockquote>\n<p>The count is more than a contest trick. It is a distance between two orderings, the number of pairs two lists disagree on. When a new reranker goes out and you want to know how far it moved from the old ranking, this is the number, and now you can compute it in O(n log n).</p>",
      "date_published": "2026-11-01T00:00:00.000Z",
      "date_modified": "2026-11-01T00:00:00.000Z",
      "tags": [
        "ICPC",
        "Fundamentals"
      ],
      "image": "https://mrcll.me/og/en/story-counting-inversions.png"
    },
    {
      "id": "https://mrcll.me/stories/digit-dp/",
      "url": "https://mrcll.me/stories/digit-dp/",
      "title": "Counting without enumerating",
      "summary": "Digit dynamic programming and the tight flag: counting numbers with a digit property without enumerating them.",
      "language": "en",
      "content_html": "<p><em>How many numbers between A and B have some property in their digits? Digit DP answers without looking at them one by one.</em></p>\n<p>How many numbers up to 1,000 have no digit 4 and never contain “13”? You could check all thousand. The answer is 712, and you can get it by visiting 9 states.</p>\n<p>First, a range is two prefixes: the count for [A, B] is the count for [0, B] minus the count for [0, A−1]. So you only ever count from zero up to some N.</p>\n<p>Inside [0, N], build the number from its most significant digit and carry one flag, tight, which says whether you are still pressed against the limit. If N is 723 and you put a 7 first, the next digit can only go up to 2. Put a 6 first and you are free: anything from 0 to 9 works from there on.</p>\n<blockquote><p>Interactive figure (DigitDP): open the web version at https://mrcll.me/stories/digit-dp/</p></blockquote>\n<p>Why does memoization pay off? Think of an 18-digit number that ends in 0123456789. Every possible prefix in front of that ending leads to the same question about the same remaining digits, 10⁸ identical calculations if you recompute it each time. Store it once and look it up.</p>\n<p>The state stays small: the position, whatever the question needs to remember (here, whether the last digit was a 1), and tight. The count of states depends on how many digits N has, not on how big N is. Before you enumerate anything, ask what the smallest state is that sums up everything that came before.</p>",
      "date_published": "2026-10-31T00:00:00.000Z",
      "date_modified": "2026-10-31T00:00:00.000Z",
      "tags": [
        "ICPC",
        "Fundamentals"
      ],
      "image": "https://mrcll.me/og/en/story-digit-dp.png"
    },
    {
      "id": "https://mrcll.me/stories/root-decomposition/",
      "url": "https://mrcll.me/stories/root-decomposition/",
      "title": "Cut it into √n pieces",
      "summary": "Square-root decomposition for range queries, from my first talk at the first Code Camp IFPB/ACM.",
      "language": "en",
      "content_html": "<p><em>The first talk I ever gave, at the first Code Camp IFPB/ACM, was about this.</em></p>\n<p>The first talk I ever gave was at the first Code Camp IFPB/ACM, and it was about a technique you can explain in one sentence: split the array into blocks of size √n and keep the sum of each block.</p>\n<p>That is it. A range-sum query in the simple loop costs up to n steps. With blocks, it costs the loose ends, at most 2√n single elements at the edges, plus the whole blocks in the middle, at most √n sums you already have. Updating one value means fixing one element and one block sum.</p>\n<blockquote><p>Interactive figure (SqrtDecomp): open the web version at https://mrcll.me/stories/root-decomposition/</p></blockquote>\n<p>For 800,000 elements, that is the gap between 800,000 steps and 2,798. Why √n and not some other size? Bigger blocks mean fewer of them but longer loose ends; smaller blocks, the opposite. √n is where the two costs meet.</p>\n<p>It is not the fastest structure there is; a segment tree does better. But square-root decomposition is the one you can write correctly in ten minutes, in a contest, under pressure, and that counts for a lot. The good-enough structure you fully understand beats the optimal one you copy.</p>",
      "date_published": "2026-10-30T00:00:00.000Z",
      "date_modified": "2026-10-30T00:00:00.000Z",
      "tags": [
        "ICPC",
        "Fundamentals"
      ],
      "image": "https://mrcll.me/og/en/story-root-decomposition.png"
    },
    {
      "id": "https://mrcll.me/stories/brasileirao-2023-titles/",
      "url": "https://mrcll.me/stories/brasileirao-2023-titles/",
      "title": "Brasileirão 2023, the most titles on the pitch",
      "summary": "The 2023 Brasileirão started with 65 national titles on the pitch, the most in the points era since 2003.",
      "language": "en",
      "content_html": "<p><em>Since the points era began in 2003, the Série A had never started with this much tradition.</em></p>\n<p>Add up the national titles of every club in a Série A season and you get a rough measure of how heavy the shirts are. The 2023 Brasileirão started with 65 of them, the most since the points era began in 2003. The year before it was 52. The previous high was 60.</p>\n<blockquote><p>Interactive figure (TitlesChart): open the web version at https://mrcll.me/stories/brasileirao-2023-titles/</p></blockquote>\n<p>Comparing seasons needs a footnote, though. In 2003 and 2004 the league had 24 teams, in 2005 it had 22, and only from 2006 did it settle at 20. More teams on the pitch, more possible titles. Two champions were missing from the count, too: Sport (1987) and Guarani (1978) were playing in Série B.</p>\n<p>At the other end, five clubs could lift the trophy for the first time: América-MG, Bragantino, Cuiabá, Fortaleza and Goiás.</p>\n<p>Do heavier shirts make a tighter race? Not necessarily. One of the seasons near the top of this chart ended with a standard deviation of 16.26 points in the final table, well off the usual balance. Tradition fills the pitch. It does not decide the table.</p>",
      "date_published": "2026-10-29T00:00:00.000Z",
      "date_modified": "2026-10-29T00:00:00.000Z",
      "tags": [
        "Off the code"
      ],
      "image": "https://mrcll.me/og/en/story-brasileirao-2023-titles.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/prompt-injection-is-our-xss/",
      "url": "https://mrcll.me/pt/stories/prompt-injection-is-our-xss/",
      "title": "Prompt injection é o XSS da nossa geração",
      "summary": "Não existe textContent para um modelo: o que XSS e XSRF ensinam sobre prompt injection, e por que a defesa sai da entrada e vai para o privilégio.",
      "language": "pt-BR",
      "content_html": "<p><em>XSS e XSRF são a mesma falha: um sistema que não separa dado de instrução, ou que age com uma autoridade que ninguém pediu. Os LLMs herdaram as duas.</em></p>\n<p>Imagine uma aplicação web que guarda um JWT no localStorage, válido por duas horas. Uma das páginas tem um input, e o que você digita aparece para todo mundo que visitar depois. Um atacante digita um script ali. Daí em diante o navegador de cada visitante roda esse script e entrega o token, e o atacante pode usar a conta dessas pessoas por até duas horas.</p>\n<p>A correção é quase sem graça: escrever o texto com textContent em vez de innerHTML, ou sanitizar antes de chegar ao DOM. O navegador tem um jeito confiável de dizer “isto é texto, não código”, e o buraco fecha.</p>\n<p>O primo dele, o XSRF, é sobre outro erro. Cookies vão junto com qualquer requisição para um site, mesmo quando outro site a dispara, e até por um iframe invisível que faz você clicar em algo que nunca viu. O navegador age com uma autoridade que o usuário não deu naquele clique.</p>\n<p>Agora olhe para um assistente com LLM. Ele lê texto de fora, tickets, e-mails, documentos, páginas web, e normalmente tem ferramentas: enviar e-mail, consultar dados, criar registros. Dado e instrução viajam no mesmo canal, e a autoridade vai junto sem ninguém pedir. É a mistura antiga, as duas falhas de uma vez.</p>\n<blockquote><p>Figura interativa (InjectionSandbox): use a versão web em https://mrcll.me/pt/stories/prompt-injection-is-our-xss/</p></blockquote>\n<p>Sem defesa nenhuma, o ticket vira ordem. Cercar o texto não confiável ajuda até o atacante fechar a cerca ele mesmo; teste o terceiro exemplo. O que segura de verdade é tirar a ferramenta da tarefa. Resumir não precisa de send_email, e qualquer efeito colateral espera uma pessoa confirmar.</p>\n<h2>Nada com que escapar</h2>\n<p>O XSS foi resolvido porque o navegador consegue traçar uma linha firme em volta do texto. Um modelo não consegue. Você pode pedir, cercar, repetir e avisar, e ainda assim não existe escape garantido. Então a defesa precisa sair da entrada e ir para o privilégio. O que o modelo pode fazer importa mais do que o que ele leu.</p>\n<p>Na minha demonstração de XSS, um token roubado durava duas horas. Para um agente com ferramentas, a janela é uma chamada. Trato tudo o que um modelo lê como não confiável, inclusive os nossos próprios documentos. Dou a cada tarefa o menor número de ferramentas de que ela precisa. E a saída do modelo é renderizada como texto, nunca jogada em innerHTML, SQL ou shell.</p>",
      "date_published": "2026-11-11T00:00:00.000Z",
      "date_modified": "2026-11-11T00:00:00.000Z",
      "tags": [
        "AI",
        "Backend",
        "Frontend"
      ],
      "image": "https://mrcll.me/og/pt/story-prompt-injection-is-our-xss.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/bad-intuitive-statisticians/",
      "url": "https://mrcll.me/pt/stories/bad-intuitive-statisticians/",
      "title": "Somos maus estatísticos intuitivos. Os LLMs também.",
      "summary": "Monty Hall, um hostel em Foz do Iguaçu e o que temperatura e top-p fazem de verdade com o próximo token de um LLM.",
      "language": "pt-BR",
      "content_html": "<p><em>Um hostel em Foz do Iguaçu, três portas e uma cabra me ensinaram mais sobre temperatura e top-p do que qualquer model card.</em></p>\n<p>Eu estava mochilando por Foz do Iguaçu com o <em>Rápido e Devagar</em>, do Kahneman, na mochila, quando dois outros mochileiros perguntaram do que o livro tratava. Contei a armadilha mais famosa dele: um rapaz é descrito como tímido e prestativo. Ele é bibliotecário ou fazendeiro?</p>\n<p>Os dois disseram bibliotecário. Todo mundo diz. Só que existem muito mais fazendeiros do que bibliotecários, então a resposta sem graça é a mais provável. A descrição ganha e a taxa de base perde. Não somos estatísticos intuitivos, e quem constrói com modelos de linguagem não é exceção.</p>\n<p>Um modelo não consulta uma resposta. A cada passo ele tem uma distribuição de probabilidade sobre o próximo token e sorteia dela. Temperatura e top-p não deixam o modelo mais criativo nem mais cuidadoso; eles mudam o formato dessa distribuição antes do sorteio. A maioria dos times ajusta esses valores do jeito que meus amigos do hostel chutaram: no feeling, a partir de uma demo.</p>\n<blockquote><p>Figura interativa (Sampler): use a versão web em https://mrcll.me/pt/stories/bad-intuitive-statisticians/</p></blockquote>\n<p>Desça a temperatura para 0,1 e “tests” ganha todas as vezes: determinístico e confiantemente sem graça. Suba para 2 e “moon” começa a aparecer no seu resumo de incidente. O top-p é o guarda-corpo. Ele corta a cauda antes do sorteio, então uma temperatura alta não alcança tokens que nunca foram plausíveis. Nada disso aparece numa resposta só. Aparece quando você sorteia mil vezes, que é exatamente o que seus usuários vão fazer por você.</p>\n<h2>Três portas</h2>\n<p>No hostel eu puxei o problema de Monty Hall. Três portas, um carro, duas cabras. Você escolhe uma porta. O apresentador, que sabe onde está o carro, abre outra e mostra uma cabra, e oferece a troca. Os dois disseram que tanto fazia: sobraram duas portas, cinquenta por cento. Jogue uma rodada e depois deixe a máquina jogar mil.</p>\n<blockquote><p>Figura interativa (MontyHall): use a versão web em https://mrcll.me/pt/stories/bad-intuitive-statisticians/</p></blockquote>\n<p>Trocar ganha duas vezes em três. Sua primeira escolha acerta um terço das vezes, então erra dois terços, e o apresentador acabou de mostrar onde não olhar. A escolha dele é evidência, e evidência muda a distribuição. Isso é Bayes, e é também o trabalho inteiro com modelos de linguagem: o prompt, os documentos recuperados e os exemplos são evidências que mexem naquilo de onde o modelo sorteia. A temperatura só decide até onde dessa distribuição você deixa ele ir.</p>\n<p>Teve mais uma armadilha naquela noite, e é a que mais vejo hoje em avaliações. Um atacante do Fluminense marca 1 de 3 chutes no Brasileirão e 95 de 97 na Libertadores. Um do Flamengo é melhor nos dois: 101 de 300 e 96 de 97. Some os totais e o Fluminense vence, 96% a 50%. Paradoxo de Simpson. Um modelo pode ganhar do baseline em todas as fatias da sua avaliação e perder no total, ou o contrário.</p>\n<p>Então eu escolho a temperatura por feature, a partir de amostras. Julgo mudanças de prompt em centenas de execuções, nunca na resposta única da demo. E quebro toda avaliação por segmento antes de acreditar no total.</p>",
      "date_published": "2026-11-10T00:00:00.000Z",
      "date_modified": "2026-11-10T00:00:00.000Z",
      "tags": [
        "AI",
        "Fundamentos"
      ],
      "image": "https://mrcll.me/og/pt/story-bad-intuitive-statisticians.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/floats-and-salaries/",
      "url": "https://mrcll.me/pt/stories/floats-and-salaries/",
      "title": "0.1 + 0.2 e o salário de alguém",
      "summary": "Por que dinheiro nunca é float: bits IEEE 754, desvio na folha e arredondamento de taxa, um contracheque por vez.",
      "language": "pt-BR",
      "content_html": "<p><em>Na maratona de programação, um épsilon faz o bug sumir. Na folha de pagamento, um épsilon é uma pessoa que recebeu um centavo a menos.</em></p>\n<p>Aqui está um programa que deveria imprimir true:</p>\n<pre><code>double phi = (1 + sqrt(5)) / 2;\nprintf(1 / phi == phi - 1 ? \"true\" : \"false\");   /* false */</code></pre>\n<p>A razão áurea satisfaz 1/φ = φ − 1 exatamente. O computador discorda na décima sexta casa decimal, porque os dois lados chegam ao mesmo número por arredondamentos diferentes. Na maratona de programação a correção é reflexo: comparar com tolerância, <code>fabs(x - y) &lt; 1e-9</code>, e ir para o próximo problema. Fiz isso por anos.</p>\n<p>Para entender o porquê, veja o que um double realmente guarda. 0,1 não tem forma binária exata, então a máquina guarda o valor mais próximo que consegue.</p>\n<blockquote><p>Figura interativa (FloatBits): use a versão web em https://mrcll.me/pt/stories/floats-and-salaries/</p></blockquote>\n<p>Aí os números viraram salários. Folha de pagamento entre países é soma, taxa e arredondamento, milhões de vezes por mês, em moedas que nem concordam sobre quantas casas decimais têm: o iene não tem nenhuma, o dinar kuwaitiano tem três. Cada arredondamento cai na conta de alguém, e os totais precisam fechar até a última unidade. O cenário abaixo é genérico, não é o sistema de nenhuma empresa. Some o mesmo valor a um total, contracheque após contracheque.</p>\n<blockquote><p>Figura interativa (PayrollDrift): use a versão web em https://mrcll.me/pt/stories/floats-and-salaries/</p></blockquote>\n<p>O desvio é uma fração minúscula de centavo. Arredonde o total e ele até parece certo, e essa é a armadilha. A checagem de igualdade falha, o job de conciliação dispara um alarme e alguém coloca um épsilon para silenciar. Daí em diante o épsilon vira política.</p>\n<h2>Para onde os centavos vão de verdade</h2>\n<p>O dinheiro que some de verdade vem de outro lugar: aplicar uma taxa, um câmbio ou um reajuste e arredondar linha a linha.</p>\n<blockquote><p>Figura interativa (RateRounding): use a versão web em https://mrcll.me/pt/stories/floats-and-salaries/</p></blockquote>\n<p>Com taxa 1,15, 2.376 de 100.000 linhas arredondam para o centavo errado com ponto flutuante. Com centavos inteiros e taxa decimal, nenhuma. Um épsilon não resolve isso, porque não existe “perto o bastante” para um centavo pago à pessoa errada.</p>\n<p>A correção é uma representação, não uma comparação:</p>\n<ol><li>Guarde dinheiro como uma contagem inteira de unidades menores mais uma moeda, e tire o número de casas decimais da moeda (ISO 4217), nunca de um “2” global.</li><li>Faça a conta de taxa num tipo decimal, escolha uma regra de arredondamento (half-up ou half-even), escreva, aplique uma vez num passo definido e teste por propriedade contra aritmética exata.</li><li>Trate conciliação como igualdade. Se os totais só batem com épsilon, o bug está antes.</li></ol>",
      "date_published": "2026-11-09T00:00:00.000Z",
      "date_modified": "2026-11-09T00:00:00.000Z",
      "tags": [
        "Backend",
        "Fundamentos"
      ],
      "image": "https://mrcll.me/og/pt/story-floats-and-salaries.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/tokens-are-compression/",
      "url": "https://mrcll.me/pt/stories/tokens-are-compression/",
      "title": "Tokens são compressão: o que Shannon explica sobre a sua conta de IA",
      "summary": "Entropia de Shannon, tokenizadores e a sua conta de IA: digite qualquer texto e veja informação, tokens e custo se mexerem juntos.",
      "language": "pt-BR",
      "content_html": "<p><em>Por que alguns prompts custam o dobro de outros, explicado por um gigabyte de 1s e uma frase sobre um incêndio.</em></p>\n<p>Toda fatura de LLM é uma contagem de tokens, de entrada e de saída. Os times olham para ela como olham para a conta de celular: um número que sobe. É mais interessante do que isso. Uma contagem de tokens mede o quanto o seu texto é surpreendente.</p>\n<p>Comece com um arquivo em que todo byte é 1. Faça ele ter um gigabyte. Quanta informação tem ali? Quase nenhuma. Se você sabe que é tudo 1, só precisa do tamanho. Dado e informação são coisas diferentes. Ou pegue a frase “Vi um incêndio e tinha fogo.” Todo incêndio tem fogo; a segunda metade pode sair e nada se perde. Informação é surpresa.</p>\n<p>Um tokenizador trabalha em cima dessa ideia. Antes de o modelo ler qualquer coisa, o texto é comprimido em tokens, pedaços de um dicionário aprendido com os dados de treino. Pedaços frequentes ganham um token só. Os raros são soletrados em vários. Por isso a conta acompanha a surpresa, não o tamanho. Experimente:</p>\n<blockquote><p>Figura interativa (TokenMeter): use a versão web em https://mrcll.me/pt/stories/tokens-are-compression/</p></blockquote>\n<h2>A aritmética de “euameiela”</h2>\n<p>Rode a figura com “euameiela”. Nove caracteres, 72 bits em disco e só uns 21,8 bits de informação, 2,419 bits por caractere. Cerca de 70% do espaço desperdiçado. Um código de prefixo feito à mão, um bit para o “e” frequente e códigos maiores para as letras raras, espreme tudo em 22 bits. Não dá para ir abaixo disso: bits são inteiros e a entropia é o piso.</p>\n<p>O byte-pair encoding é o mesmo truque em escala. Em vez de dar ao “e” um código curto, ele dá a pedaços comuns de texto um token só. Agora cole um UUID, um base64 ou um JSON minificado na caixa acima. A entropia por caractere sobe, as palavras deixam de parecer palavras e a contagem de tokens passa na frente da de caracteres.</p>\n<p>O idioma também pesa. Com dados de treino majoritariamente em inglês, uma frase em inglês costuma custar menos tokens do que a mesma frase em português. Os dois exemplos mostram isso.</p>\n<p>O que eu faço com isso: trato IDs, hashes e JSON ruidoso nos prompts como a parte cara que eles são. Meço com o tokenizador do próprio provedor em cada idioma que atendemos. E tudo o que vai em toda requisição é cacheado, encurtado ou removido, porque é uma linha no orçamento, leia alguém ou não.</p>",
      "date_published": "2026-11-08T00:00:00.000Z",
      "date_modified": "2026-11-08T00:00:00.000Z",
      "tags": [
        "AI",
        "Fundamentos"
      ],
      "image": "https://mrcll.me/og/pt/story-tokens-are-compression.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/q-learning-to-llm-agents/",
      "url": "https://mrcll.me/pt/stories/q-learning-to-llm-agents/",
      "title": "De Q-learning a agentes com LLM",
      "summary": "De um labirinto com Q-learning a agentes com LLM em produção: num agente, a recompensa é o design.",
      "language": "pt-BR",
      "content_html": "<p><em>O primeiro agente que treinei precisou de 799 episódios para sair de um labirinto pequeno. Agentes em produção não ganham nenhum ensaio.</em></p>\n<p>O primeiro agente que treinei vivia numa grade. Uma casa valia +1, uma custava −1, uma era parede e o resto era chão vazio. Ele não sabia nada. Aprendia só com recompensas, com a equação de Bellman fazendo a contabilidade:</p>\n<ul><li>no episódio 0, ele vagava;</li><li>no 199, sabia mais e ainda falhava;</li><li>no 599, estava chegando lá;</li><li>no 799, resolvia o labirinto todas as vezes.</li></ul>\n<p>Hoje “agente” quase sempre quer dizer um modelo de linguagem num loop: lê o estado, escolhe uma ferramenta, olha o resultado, repete. O vocabulário sobreviveu inteiro. Agente, ambiente, recompensa, política. O que não sobreviveu foi o treino. Ninguém te dá 800 episódios com usuários reais antes de o agente precisar se comportar.</p>\n<blockquote><p>Figura interativa (QMaze): use a versão web em https://mrcll.me/pt/stories/q-learning-to-llm-agents/</p></blockquote>\n<p>Treine um episódio de cada vez e veja as setas se espalharem para trás a partir do +1, como uma mancha. Zere a penalidade por passo e o agente fica preguiçoso: continua chegando, sem pressa nenhuma. Zere a exploração e ele pode se contentar com o primeiro caminho que funcionou. O labirinto deixa óbvio algo fácil de esquecer com LLMs: a função de recompensa é o design. São as penalidades que fazem o agente terminar rápido.</p>\n<p>Num agente com LLM a política já chega treinada: o modelo mais o seu prompt. A recompensa continua sendo sua. Ela vira a avaliação que pontua a trajetória inteira, e não só a resposta final. A penalidade por passo vira o limite de passos, de tokens e de tempo. E uma propriedade do Q-learning vale igualzinho: os valores só continuam certos enquanto o ambiente continua o mesmo. Mudou uma ferramenta, uma API ou um schema, a política antiga ficou velha até você rodar as avaliações de novo.</p>\n<p>799 episódios para uma grade de três por quatro. Seu agente em produção ganha zero. Escreva a recompensa primeiro.</p>",
      "date_published": "2026-11-07T00:00:00.000Z",
      "date_modified": "2026-11-07T00:00:00.000Z",
      "tags": [
        "AI"
      ],
      "image": "https://mrcll.me/og/pt/story-q-learning-to-llm-agents.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/running-code-on-other-peoples-sites/",
      "url": "https://mrcll.me/pt/stories/running-code-on-other-peoples-sites/",
      "title": "Rodar código no site dos outros",
      "summary": "Um pixel de rastreamento, três sites e o que é preciso para rodar seu código como visita no site dos outros.",
      "language": "pt-BR",
      "content_html": "<p><em>Já construí um script que vivia dentro do site dos nossos clientes. O mecanismo que te rastreia pela internet é o mesmo; o que muda é a responsabilidade.</em></p>\n<p>Na Social Miner, construí e mantive o URSA: um script que nossos clientes embutiam por um pixel, do jeito que se embute o Google Tag Manager. Ele rodava num ambiente hostil, dentro do sandbox do navegador em sites que não eram nossos e que não dava para testar antes. As partes onsite e offsite conversavam por postMessage, com um pequeno pub/sub por cima. Eventos de comportamento iam em lote para o IndexedDB e eram enviados por um service worker, para nada se perder quando alguém fechava a aba. O URSA também capturava a permissão de push, e foi isso que viabilizou nosso produto de Push Notification. Liderei um time de quatro pessoas nele e no Channel Builder, feito com .NET e React.</p>\n<p>Código na página dos outros enxerga muita coisa. Para entender quanto, imagine uma empresa grande, a MOZART, que quer reconhecer a mesma pessoa em qualquer lugar da internet sem nunca saber quem ela é. Ela define a pessoa como um conjunto de ações: visitou a página do Palmeiras no globoesporte às 18h30, depois o site do governo de São Paulo às 18h35. Daí deduz: torce pro Palmeiras, mora em São Paulo, tem tempo livre perto das 18h30. Para costurar as visitas bastam dois headers HTTP. O referer diz de onde veio a requisição. Um cookie, mozart.id, identifica o navegador. Qualquer site que carregue mozart.com/external.png entrega os dois.</p>\n<blockquote><p>Figura interativa (TrackingPixel): use a versão web em https://mrcll.me/pt/stories/running-code-on-other-peoples-sites/</p></blockquote>\n<h2>Um pixel basta</h2>\n<p>Visite os três sites com tudo liberado e veja o perfil se montar. Depois bloqueie cookies de terceiros: cada requisição chega anônima e as visitas deixam de se ligar. Corte o referer para a origem e o “Palmeiras” some, porque o caminho da página não viaja mais. Os navegadores foram para esse lado, limitando cookies de terceiros e cortando o referer entre sites por padrão.</p>\n<p>Para código embutido de boa-fé isso não é obstáculo. É o contrato. Todo widget, SDK e chat embutido é visita na casa de outra pessoa, e uma boa visita:</p>\n<ul><li>fica isolada, num iframe ou em Shadow DOM, sem variáveis globais e sem CSS vazando;</li><li>depende só do que foi oferecido, presume que não há cookies de terceiros e que o referer vem cortado, e pede qualquer outra coisa de forma explícita;</li><li>tem orçamento de peso e de falha, para que, quando quebrar, a página do anfitrião continue de pé.</li></ul>\n<p>Uma imagem de um por um já bastou para descobrir um time, uma cidade e um horário. O que você faz com o que chega é toda a diferença.</p>",
      "date_published": "2026-11-06T00:00:00.000Z",
      "date_modified": "2026-11-06T00:00:00.000Z",
      "tags": [
        "Frontend",
        "Backend"
      ],
      "image": "https://mrcll.me/og/pt/story-running-code-on-other-peoples-sites.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/progressive-enhancement-ai-ui/",
      "url": "https://mrcll.me/pt/stories/progressive-enhancement-ai-ui/",
      "title": "Progressive enhancement para interfaces geradas por IA",
      "summary": "Progressive enhancement para interfaces geradas por IA: entregue o essencial sem o modelo e detecte a camada de IA.",
      "language": "pt-BR",
      "content_html": "<p><em>Parte do público não vai receber a experiência completa. Era verdade com navegadores antigos e voltou a ser agora que existe um modelo dentro da interface.</em></p>\n<p>Vale mesmo usar um recurso que nem todo navegador suporta? A gente já aceita experiências diferentes em dispositivos diferentes. Por que não em navegadores diferentes?</p>\n<p>A resposta depende de um detalhe. Quem está num navegador antigo pode não ter escolha, ou não saber atualizar. A culpa não é dessa pessoa, então o essencial tem que funcionar em todo lugar e o resto vem por cima: <code>@supports</code> para detectar recursos de CSS, Modernizr quando precisar de mais. Progressive enhancement é uma ideia antiga. E está prestes a ser útil de novo.</p>\n<p>Interface gerada por IA traz uma dependência que se comporta pior do que qualquer navegador já se comportou. Às vezes é lenta, às vezes erra, às vezes cai, às vezes o usuário desliga. Se a página só faz sentido quando o modelo responde, o pior dia do seu provedor vira o pior dia do seu produto. O modelo é a camada menos confiável que você já colocou em produção.</p>\n<p>Aqui está um card construído em camadas. Tire uma de cada vez.</p>\n<blockquote><p>Figura interativa (LayeredCard): use a versão web em https://mrcll.me/pt/stories/progressive-enhancement-ai-ui/</p></blockquote>\n<p>Desligue o JavaScript e os fatos continuam lá. Finja que o navegador não tem container queries e os filtros ficam, só que empilhados. Faça o modelo falhar e só o resumo vai embora; o espaço reservado avisa em vez de quebrar o layout. Deixe o modelo lento e o texto chega aos pedaços, num lugar que já era dele.</p>\n<p>Nenhuma chamada ao modelo é necessária para a página ser útil. Esse é o design inteiro.</p>\n<p>As ferramentas mudaram; a ideia não. Onde antes ficava o <code>@supports</code>, agora ficam um timeout, um kill switch e uma configuração do usuário. O essencial não depende da camada nova, e a camada nova se anuncia quando chega: em streaming num espaço reservado para ela, para nada pular, e identificada, para ninguém confundir texto gerado com os fatos em volta.</p>",
      "date_published": "2026-11-05T00:00:00.000Z",
      "date_modified": "2026-11-05T00:00:00.000Z",
      "tags": [
        "AI",
        "Frontend"
      ],
      "image": "https://mrcll.me/og/pt/story-progressive-enhancement-ai-ui.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/why-i-wrote-my-own-renderer/",
      "url": "https://mrcll.me/pt/stories/why-i-wrote-my-own-renderer/",
      "title": "Por que eu escrevi meu próprio renderizador",
      "summary": "bradom: seis estratégias de renderização escritas do zero, de innerHTML a Virtual DOM e tagged templates, para tomar uma decisão.",
      "language": "pt-BR",
      "content_html": "<p><em>Antes de escolher o ferramental do design system da XP, eu quis entender o que ele fazia por dentro. O resultado foi o bradom: seis jeitos de renderizar a mesma interface, escritos do zero.</em></p>\n<p>Quando precisei escolher as ferramentas do design system da XP Inc., nenhuma servia perfeitamente. Toda opção tinha benefícios e custos, e ler comparações não resolvia nada. Então levantei uma possibilidade meio atrevida com o time: e se a gente construísse a nossa própria biblioteca? Se outros construíram, por que não nós?</p>\n<p>Essa pergunta virou o bradom, um monorepo de estudo com seis pacotes, cada um renderizando a mesma interface de um jeito:</p>\n<ol><li>innerHTML ingênuo;</li><li>Document Fragment;</li><li>Virtual DOM sem diff;</li><li>Virtual DOM completo;</li><li>tagged template literals (incompleto);</li><li>um benchmark com React para me manter honesto.</li></ol>\n<p>O código está no <a href=\"https://github.com/marcello0liveira/bradom\">GitHub</a>.</p>\n<h2>Onde o tempo vai</h2>\n<p>O navegador transforma bytes em caracteres, tokens, nós, DOM e CSSOM, junta tudo na render tree, calcula o layout e pinta. Mexer no DOM pode reiniciar parte desse caminho, e evitar reflow desnecessário é quase tudo o que otimização de renderização quer dizer.</p>\n<p>O jeito mais fácil, reescrever o innerHTML, também é o pior. Ele joga fora o estado dos elementos, como o foco de um input e o texto dentro dele, e abre uma porta para XSS. Não é à toa que o React chama a versão dele de dangerouslySetInnerHTML. Digite no filtro abaixo e veja o que cada estratégia faz com a lista.</p>\n<blockquote><p>Figura interativa (RenderCost): use a versão web em https://mrcll.me/pt/stories/why-i-wrote-my-own-renderer/</p></blockquote>\n<p>Indo de “ab” para “a”, o innerHTML joga tudo fora e reconstrói. O Virtual DOM deixa você escrever o código como se recriasse tudo, depois compara a árvore nova com a antiga e só mexe no que mudou, e o estado sobrevive. O preço é avaliar a árvore inteira a cada mudança, inclusive as partes que nunca mudam.</p>\n<h2>O cache que ninguém te conta</h2>\n<p>Tagged templates têm uma propriedade discreta. O primeiro argumento que a função recebe, o array de strings estáticas, é o mesmo objeto toda vez que o mesmo template roda. Dá para usar como chave de cache. O HTML já vem separado em partes estáticas e buracos dinâmicos, então só os buracos precisam ser reavaliados. UI = f(estado), recalculada só onde o estado mudou.</p>\n<p>E a gente colocou o bradom em produção? Não. Para o Soma, o design system, escolhi uma tecnologia que não era minha. Escrever o renderizador nunca foi para entregar. Foi para escolher com as mãos e não com posts de blog, sabendo exatamente onde estaria o custo: em nós avaliados e operações no DOM, não em linhas de código.</p>",
      "date_published": "2026-11-04T00:00:00.000Z",
      "date_modified": "2026-11-04T00:00:00.000Z",
      "tags": [
        "Frontend",
        "Fundamentos"
      ],
      "image": "https://mrcll.me/og/pt/story-why-i-wrote-my-own-renderer.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/northeast-brasileirao/",
      "url": "https://mrcll.me/pt/stories/northeast-brasileirao/",
      "title": "O Nordeste e o Brasileirão",
      "summary": "Clubes nordestinos no Brasileirão: 44.006 km de viagem para o Fortaleza contra 11.992 para os clubes paulistas em 2023.",
      "language": "pt-BR",
      "content_html": "<p><em>Dois times em vinte. Uma fatia que já foi metade da tabela, e já foi zero.</em></p>\n<p>Cinco jogos em quinze dias. Em 2022 o Fortaleza recebeu o São Paulo, viajou para enfrentar Vitória, Botafogo e Alianza Lima e voltou para casa para pegar o Fluminense. São 10.362 km em linha reta, e o voo nem sempre é fretado.</p>\n<p>A Série A de 2023 começou com 2 clubes nordestinos em 20, ou 10%. Já foi bem diferente. Em 1962, 9 dos clubes da Taça Brasil, metade dos participantes, eram do Nordeste. Em 1967, no Torneio Roberto Gomes Pedrosa, não havia nenhum. A fatia de hoje fica mais perto da participação da região no PIB, 13,6%, do que na população, 30%.</p>\n<p>Os motivos são muitos, e a distância pesa mais do que a maioria, porque o calendário nunca para para ninguém se recuperar. Assim é uma temporada de viagens.</p>\n<blockquote><p>Figura interativa (TravelChart): use a versão web em https://mrcll.me/pt/stories/northeast-brasileirao/</p></blockquote>\n<p>Para jogar a Série A de 2023, o Fortaleza viaja 44.006 km em linha reta. Os clubes paulistas viajam 11.992.</p>\n<p>E mesmo assim: o Fortaleza virou o primeiro clube nordestino a jogar a Libertadores em anos seguidos, 2022 e 2023, e em 2022 levou os dois maiores públicos da fase de grupos, 50.429 pagantes contra o Colo-Colo e 48.867 contra o River Plate. Toda tabela esconde um custo que não mostra. Nesta, é a distância.</p>",
      "date_published": "2026-11-03T00:00:00.000Z",
      "date_modified": "2026-11-03T00:00:00.000Z",
      "tags": [
        "Off the code"
      ],
      "image": "https://mrcll.me/og/pt/story-northeast-brasileirao.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/linear-recurrences-log-time/",
      "url": "https://mrcll.me/pt/stories/linear-recurrences-log-time/",
      "title": "Fibonacci de 10¹⁸ em 84 multiplicações",
      "summary": "Fibonacci de 10^18 em 84 multiplicações de matrizes: recorrências lineares em tempo logarítmico.",
      "language": "pt-BR",
      "content_html": "<p><em>Toda recorrência linear pode ser escrita como multiplicação de matrizes. E matrizes podem ser elevadas a uma potência rápido.</em></p>\n<p>Qual é o 10¹⁸-ésimo número de Fibonacci, módulo algo razoável? Um loop ainda estaria rodando muito depois do fim da maratona. Dá para chegar lá com 84 multiplicações.</p>\n<p>Uma recorrência linear é uma sequência em que cada termo é uma combinação linear dos anteriores, e Fibonacci é o exemplo famoso. Qualquer passo assim pode ser escrito como uma matriz T vezes um vetor com os termos recentes. Aplicar T uma vez anda a sequência um passo. Aplicar duas, T², anda dois. E T^(n−1), aplicada a f(0) e f(1), que você já conhece, chega em f(n).</p>\n<p>Então a pergunta vira: quão rápido dá para elevar uma matriz a uma potência? Você não precisa de n multiplicações para chegar a xⁿ. De x sai x², de x² sai x⁴, de x⁴ sai x⁸. Qualquer expoente é uma soma dessas potências de dois, lida direto dos seus dígitos binários. Elevar ao quadrado transforma n passos em uns log n.</p>\n<blockquote><p>Figura interativa (Recurrence): use a versão web em https://mrcll.me/pt/stories/linear-recurrences-log-time/</p></blockquote>\n<p>O loop precisa de 10¹⁸ passos; a matriz precisa de 84 multiplicações. Quando algo cresce um passo de cada vez, procure a forma que dobra. Quase sempre existe uma.</p>",
      "date_published": "2026-11-02T00:00:00.000Z",
      "date_modified": "2026-11-02T00:00:00.000Z",
      "tags": [
        "ICPC",
        "Fundamentos"
      ],
      "image": "https://mrcll.me/og/pt/story-linear-recurrences-log-time.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/counting-inversions/",
      "url": "https://mrcll.me/pt/stories/counting-inversions/",
      "title": "Quantas trocas até ficar ordenado?",
      "summary": "Contando inversões com Bubblesort e Mergesort, e por que a mesma contagem compara dois rankings.",
      "language": "pt-BR",
      "content_html": "<p><em>Contar inversões é um problema famoso com muitas soluções conhecidas. O Bubblesort conta trocando; o Mergesort conta sem olhar cada par.</em></p>\n<p>Pegue [1,4,3,2] e rode uma passada de Bubblesort. Compare 1 e 4: deixa. Compare 4 e 3: troca. Compare 4 e 2: troca. Sobra [1,3,2,4], e mais uma troca termina o serviço. Três trocas no total.</p>\n<p>Não é coincidência. Cada troca de vizinhos desfaz exatamente uma inversão, um par fora de ordem, então o número de trocas do Bubblesort é o número de inversões. Um contador ao lado da troca é o algoritmo inteiro.</p>\n<p>Também é O(n²), o que serve para quatro números e não serve para um milhão. O Mergesort ordena em O(n log n) e consegue contar enquanto ordena. Quando o merge pega um elemento da metade direita antes do que sobrou da metade esquerda, esse elemento pula todos eles, e cada pulo é uma inversão. Some tudo e você nunca olha um par diretamente.</p>\n<blockquote><p>Figura interativa (Inversions): use a versão web em https://mrcll.me/pt/stories/counting-inversions/</p></blockquote>\n<p>A contagem é mais do que truque de maratona. É uma distância entre duas ordenações, o número de pares em que duas listas discordam. Quando um reranker novo vai para produção e você quer saber o quanto ele se afastou do ranking antigo, o número é esse, e agora dá para calcular em O(n log n).</p>",
      "date_published": "2026-11-01T00:00:00.000Z",
      "date_modified": "2026-11-01T00:00:00.000Z",
      "tags": [
        "ICPC",
        "Fundamentos"
      ],
      "image": "https://mrcll.me/og/pt/story-counting-inversions.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/digit-dp/",
      "url": "https://mrcll.me/pt/stories/digit-dp/",
      "title": "Contar sem enumerar",
      "summary": "Programação dinâmica de dígitos e a flag tight: contando números com uma propriedade nos dígitos sem enumerá-los.",
      "language": "pt-BR",
      "content_html": "<p><em>Quantos números entre A e B têm alguma propriedade nos dígitos? A DP de dígitos responde sem olhar um por um.</em></p>\n<p>Quantos números até 1.000 não têm o dígito 4 e nunca contêm “13”? Dá para conferir os mil. A resposta é 712, e você chega nela visitando 9 estados.</p>\n<p>Primeiro, um intervalo são dois prefixos: a contagem de [A, B] é a de [0, B] menos a de [0, A−1]. Então você só precisa contar de zero até algum N.</p>\n<p>Dentro de [0, N], monte o número a partir do dígito mais significativo e carregue uma flag, tight, que diz se você ainda está colado no limite. Se N é 723 e você coloca 7 primeiro, o próximo dígito só vai até 2. Coloque 6 primeiro e você está livre: daí em diante vale qualquer coisa de 0 a 9.</p>\n<blockquote><p>Figura interativa (DigitDP): use a versão web em https://mrcll.me/pt/stories/digit-dp/</p></blockquote>\n<p>Por que a memorização compensa? Pense num número de 18 dígitos que termina em 0123456789. Todo prefixo possível na frente desse final leva à mesma pergunta sobre os mesmos dígitos restantes, 10⁸ cálculos idênticos se você refizer toda vez. Guarde uma vez e consulte.</p>\n<p>O estado continua pequeno: a posição, o que a pergunta precisa lembrar (aqui, se o último dígito foi 1) e o tight. A quantidade de estados depende de quantos dígitos N tem, não do tamanho de N. Antes de enumerar qualquer coisa, pergunte qual é o menor estado que resume tudo o que veio antes.</p>",
      "date_published": "2026-10-31T00:00:00.000Z",
      "date_modified": "2026-10-31T00:00:00.000Z",
      "tags": [
        "ICPC",
        "Fundamentos"
      ],
      "image": "https://mrcll.me/og/pt/story-digit-dp.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/root-decomposition/",
      "url": "https://mrcll.me/pt/stories/root-decomposition/",
      "title": "Corte em √n pedaços",
      "summary": "Decomposição em raiz quadrada para consultas em intervalos, da minha primeira palestra, no primeiro Code Camp IFPB/ACM.",
      "language": "pt-BR",
      "content_html": "<p><em>A primeira palestra que dei, no primeiro Code Camp IFPB/ACM, foi sobre isso.</em></p>\n<p>A primeira palestra que dei foi no primeiro Code Camp IFPB/ACM, sobre uma técnica que cabe numa frase: divida o vetor em blocos de tamanho √n e guarde a soma de cada bloco.</p>\n<p>É isso. Uma consulta de soma num intervalo, no loop simples, custa até n passos. Com blocos, custa as pontas soltas, no máximo 2√n elementos nas bordas, mais os blocos inteiros do meio, no máximo √n somas que você já tem. Atualizar um valor é corrigir um elemento e a soma de um bloco.</p>\n<blockquote><p>Figura interativa (SqrtDecomp): use a versão web em https://mrcll.me/pt/stories/root-decomposition/</p></blockquote>\n<p>Para 800.000 elementos, é a diferença entre 800.000 passos e 2.798. Por que √n e não outro tamanho? Blocos maiores dão menos blocos e pontas mais longas; blocos menores, o contrário. √n é onde os dois custos se encontram.</p>\n<p>Não é a estrutura mais rápida que existe; uma segment tree faz melhor. Mas a decomposição em raiz é a que você escreve certo em dez minutos, numa maratona, sob pressão, e isso conta muito. A estrutura boa o bastante que você entende por inteiro ganha da ótima que você copia.</p>",
      "date_published": "2026-10-30T00:00:00.000Z",
      "date_modified": "2026-10-30T00:00:00.000Z",
      "tags": [
        "ICPC",
        "Fundamentos"
      ],
      "image": "https://mrcll.me/og/pt/story-root-decomposition.png"
    },
    {
      "id": "https://mrcll.me/pt/stories/brasileirao-2023-titles/",
      "url": "https://mrcll.me/pt/stories/brasileirao-2023-titles/",
      "title": "Brasileirão 2023, o maior número de títulos em campo",
      "summary": "O Brasileirão 2023 começou com 65 títulos nacionais em campo, o maior número da era dos pontos corridos, desde 2003.",
      "language": "pt-BR",
      "content_html": "<p><em>Desde o início dos pontos corridos, em 2003, a Série A nunca tinha começado com tanta tradição.</em></p>\n<p>Some os títulos nacionais de todos os clubes de uma Série A e você tem uma medida grosseira do peso das camisas. O Brasileirão 2023 começou com 65, o maior número desde o início dos pontos corridos, em 2003. No ano anterior eram 52. O recorde anterior era 60.</p>\n<blockquote><p>Figura interativa (TitlesChart): use a versão web em https://mrcll.me/pt/stories/brasileirao-2023-titles/</p></blockquote>\n<p>Comparar temporadas pede uma nota de rodapé. Em 2003 e 2004 o campeonato tinha 24 times, em 2005 tinha 22, e só a partir de 2006 ficou em 20. Mais times em campo, mais títulos possíveis. Dois campeões também ficaram de fora da conta: Sport (1987) e Guarani (1978) estavam na Série B.</p>\n<p>Na outra ponta, cinco clubes podiam levantar a taça pela primeira vez: América-MG, Bragantino, Cuiabá, Fortaleza e Goiás.</p>\n<p>Camisas mais pesadas fazem uma disputa mais equilibrada? Não necessariamente. Uma das temporadas perto do topo deste gráfico terminou com desvio padrão de 16,26 pontos na tabela final, bem longe do equilíbrio de costume. Tradição enche o campo. Não decide a tabela.</p>",
      "date_published": "2026-10-29T00:00:00.000Z",
      "date_modified": "2026-10-29T00:00:00.000Z",
      "tags": [
        "Off the code"
      ],
      "image": "https://mrcll.me/og/pt/story-brasileirao-2023-titles.png"
    }
  ]
}