<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hideki Mori</title>
    <description>The latest articles on DEV Community by Hideki Mori (@hidekimori).</description>
    <link>https://dev.to/hidekimori</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3903757%2F5d1a2986-7f25-4c35-b5e8-4d489fc18a94.png</url>
      <title>DEV Community: Hideki Mori</title>
      <link>https://dev.to/hidekimori</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hidekimori"/>
    <language>en</language>
    <item>
      <title>When infrastructure was physical</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Mon, 31 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/when-infrastructure-was-physical-g33</link>
      <guid>https://dev.to/hidekimori/when-infrastructure-was-physical-g33</guid>
      <description>&lt;p&gt;I was once happy — properly happy — about a load balancer.&lt;/p&gt;

&lt;p&gt;It was an NEC appliance, a proper pair in active and hot standby, and what made it a relief was something small and specific. Until then the web tier ran behind a software load balancer I'd wedged onto one of the servers. It did the job — but when I took a backend out of rotation, it cut the connections still open along with it, and somewhere a user watched their browser fill with an error. I hated that out of all proportion to what it was. What I wanted, badly, was a balancer that would let the requests already in flight finish before it let a server go. When one finally arrived — draining each backend quietly before it dropped it — I was happier than a piece of network hardware has any right to make me.&lt;/p&gt;

&lt;p&gt;The service it fronted ran on three rackmounted machines — Web1, Web2, and a MySQL box — the first rackmount servers I ever put my hands on. They were where my working life started.&lt;/p&gt;

&lt;p&gt;I don't think I've felt that exact happiness about infrastructure in a long time. I want to describe what it was, because most of it is gone now, and I'm not certain the trade was free.&lt;/p&gt;

&lt;p&gt;Back then, infrastructure was something you committed to before it existed. You chose a datacenter. You worked out the weight your racks were allowed to bear and the power you would draw, signed a contract for that power, and guessed — months ahead — how much you would actually need. Bandwidth was expensive, so you forecast your traffic and paid for a pipe sized to a number you had half made up. Every decision was a bet placed before the thing it was betting on arrived. You paid, in money and floor space and your own time, for load that hadn't shown up yet.&lt;/p&gt;

&lt;p&gt;Then the machines had to physically get there. I once had an Oracle Exadata that wouldn't fit through the datacenter door; we widened the opening to bring it in and reinforced the floor where it would stand. I kept its load average pinned as low as I could manage — the kind of attention you give something you feel responsible for. You don't forget carrying that much weight into a room.&lt;/p&gt;

&lt;p&gt;Some of the fleet was scavenged. A stack of blades left over from a project that hadn't worked out got a second life as something else entirely — hardware outliving the idea it was bought for. Other machines existed only because of limits: a dense, low-power blade chassis that made sense only because a fully loaded rack could pull thirty-eight kilowatts, and power and floor space were what you ran out of first.&lt;/p&gt;

&lt;p&gt;Nobody could tell me how much data we'd have in three years, and there was no S3 to wave the question away. So I read everything I could and landed on Isilon, because it could grow while it stayed online. There was no one to ask; you found these things yourself, or you didn't find them at all.&lt;/p&gt;

&lt;p&gt;It wasn't all the thrill of it, though there was thrill. It was also a long argument with failure. Most failures were almost friendly — a disk lamp on a RAID6 array you could practically ignore, or half a power feed dying while the redundancy quietly absorbed it. You learned not to fear those.&lt;/p&gt;

&lt;p&gt;The ones that got you were the unplanned ones. I remember a router that started failing with nothing behind it — no second unit, nothing to fail over to, nothing to do but the thing you didn't want to do. I closed my eyes, pulled the cable, swapped in the spare, and waited to learn whether I had just made it worse. Those were the ones that wore on you. There was no pager rotation. You were the redundancy.&lt;/p&gt;

&lt;p&gt;None of this was ever quite mine alone. I wrote the software, but the machines I ran it on were built and racked by someone else — we had one infrastructure engineer, and the iron was his before it became mine. And someone above the two of us decided to turn a pair of engineers loose on that much expensive equipment in the first place. I didn't think of any of it as a gift while it was happening. It was one.&lt;/p&gt;

&lt;p&gt;The cloud took all of this away, and I'm mostly grateful. I don't miss forecasting bandwidth, or widening doorways, or pulling cables with my eyes shut. Capacity is elastic now — it comes when you ask and goes when you stop, and no one bets the floor on a guess.&lt;/p&gt;

&lt;p&gt;But something left with it. When infrastructure was physical, it was also yours, in a way a call to someone else's datacenter never quite is — something that could thrill you and wear you down in the same week, because it was close enough to put your hands on. I wouldn't go back. I just remember being happy about a load balancer, and I notice I don't feel that about anything anymore.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Claude (Opus).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>infrastructure</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>The work of staying</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Mon, 24 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/the-work-of-staying-10c9</link>
      <guid>https://dev.to/hidekimori/the-work-of-staying-10c9</guid>
      <description>&lt;p&gt;There is a system I wrote years ago that is still running. I'd struggle to tell you what was hard about building it. That part took a few weeks, and weeks blur. What I remember is everything that came after: the data that arrived in shapes I hadn't planned for, the edge case that surfaced in year three, the small fixes made late at night because something real depended on it staying up.&lt;/p&gt;

&lt;p&gt;Building software is a blink. The life of the thing you built — the months and years it spends in contact with live data and real users — is where almost all of the engineering happens. It's also the only place I've ever learned whether my decisions held up.&lt;/p&gt;

&lt;p&gt;I want to be careful here, because this is easy to turn into a hierarchy and it isn't one. I was lucky. I happened to stay on the operating side — the side that keeps things running instead of building them and moving on. If my career had sent me from one project to the next, building and handing off, I don't know what kind of engineer I'd be today. That isn't a claim about ability. It's a claim about which side of a structure you land on.&lt;/p&gt;

&lt;p&gt;Here is the structure. When you build something and then leave, the verdict on your decisions still arrives — but it arrives at someone else's desk. The shortcut you took, the abstraction you chose, the thing you were certain would never need to change: you find out whether you were right long after you're gone, and the verdict never makes its way back to you. The feedback loop is severed. Not because you did anything wrong, but because the structure you worked inside never closes the loop to the person who'd learn from it.&lt;/p&gt;

&lt;p&gt;This happens anywhere software is built and handed off, which is everywhere. But I watched it at its most concentrated here in Japan, where many engineers sit on the vendor side of that handoff, building systems that other companies will run. The loop isn't cut more deeply here; it's cut for far more people. I don't think of it as a Japanese problem. It's a structural one, and Japan is simply where I saw it densest.&lt;/p&gt;

&lt;p&gt;Staying, though, isn't the whole of it. Being on the operating side hands you the loop; it doesn't make you use it. Almost every engineer who inherits an old system wants, at some point, to stand back and call it ugly — to narrate its flaws instead of touching them. It's a comfortable place to stand, and I've stood there myself more than once. The system is bad, and you're the one clear-eyed enough to see it.&lt;/p&gt;

&lt;p&gt;I once knew an engineer who had owned a system for a long time. For five years he'd been saying it was old, that it was behind, that it wasn't built the way things ought to be built. I remember asking him whether he hadn't had those five years himself.&lt;/p&gt;

&lt;p&gt;I didn't mean it as an accusation, and I don't repeat it as one. Complaining is not maintaining. For as long as something is in your hands, its condition is yours — not the condition you inherited, but the one it's in now, because the time to change it was the whole time you were holding it. That was true of him. It's true of me. And it's the sharpest form I know of a plainer idea: one person can be a complete unit of responsibility.&lt;/p&gt;

&lt;p&gt;None of this is an argument against trying things. I do it too — small integrations, things put together over a weekend to see if they'd work. I'm not standing outside that world; I live in it. The difference I care about is quieter. When I build one of those small things, I build it as if it will have to run: error handling, retries, concurrency, cost, all in view from the first line. "It worked, see how easy" is a different sentence from "it has been working." Operation trains a muscle that exploration can't, and neither is the lesser. You want both.&lt;/p&gt;

&lt;p&gt;For the past five years I've kept a single platform running, and I never set out to make it into what it became. It began as translation. Staying with it — answering what real documents and real volume kept throwing at it — turned translation into AI rewriting, then into structured output, then into something I'd now call document processing. I didn't design my way to any of that. I arrived at it by not leaving. The discovery came from the staying.&lt;/p&gt;

&lt;p&gt;The work of staying is mostly invisible. It doesn't demo well — there's no launch, no before-and-after, only a thing that is quietly still alive. I don't expect it to be widely felt. But anyone who has stayed with one thing long enough will know what I mean, and this is just me naming it out loud, so the few who do can know it was seen.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Claude (Opus).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>career</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>The doors were already there</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Mon, 17 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/the-doors-were-already-there-pji</link>
      <guid>https://dev.to/hidekimori/the-doors-were-already-there-pji</guid>
      <description>&lt;p&gt;I recently wrote that the translation system I work on had quietly become something larger — that turning a forty-year-old translation company into document-AI was less a pivot than one definition followed to its end.&lt;/p&gt;

&lt;p&gt;But being right about an idea is the cheap part. The expensive part is whether your system can act on it without being torn open. The reason that shift cost almost nothing — the reason "structure this contract" was a parameter and not a project — is that the places for it to attach were already there, drawn years earlier. This piece is about those places.&lt;/p&gt;




&lt;h2&gt;
  
  
  Naming the category, not the instance
&lt;/h2&gt;

&lt;p&gt;Five years ago, building the layer that talks to translation engines, I made a choice that bought me nothing at the time. I didn't write a connector for "the machine-translation vendors we use." I wrote one for "an engine that transforms text," and made the translation vendors one kind of it.&lt;/p&gt;

&lt;p&gt;The reason was duller than foresight. Translation was already not the only thing we did — we had summarization engines too. "Text transformation" was simply the accurate name for the category we were already in, and naming the instance — "translation" — would have been a lie I'd have to walk back later.&lt;/p&gt;

&lt;p&gt;That accuracy had a price. A "translation connector" would have been simpler that week. Choosing the true, wider name meant more structure for no immediate gain, and a payoff that was years away and, at the time, entirely hypothetical.&lt;/p&gt;




&lt;h2&gt;
  
  
  What slots in without a fight
&lt;/h2&gt;

&lt;p&gt;When generative AI arrived, it slotted in.&lt;/p&gt;

&lt;p&gt;A new provider is a new subtype. The genAI engines attached as a sibling to the machine-translation branch, under the same text-transformation base; nothing above them had to learn they had come.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuybs90chmuijebwrkbwg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuybs90chmuijebwrkbwg.png" alt="Connector class hierarchy: a Connector interface, a Text-Transformation base beneath it, with Machine-Translation and Generative-AI connectors as sibling subtypes, each over its own per-vendor implementations." width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The opening was older than the thing that eventually occupied it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A new model is a row and a number. I work out its price from one rule and add a line to the billing config; the metering and the plans inherit it, and the engine never learns the model's name — it reads it from a table.&lt;/p&gt;

&lt;p&gt;A new service is a tenant. It brings no billing, no gateway rules, no "text or a file, never both" contract of its own — those are the building it moves into, not luggage it carries.&lt;/p&gt;

&lt;p&gt;None of this was designed for the thing that arrived. The point was never to guess the future. The point was to leave a correctly-shaped opening, so that whatever showed up could be received instead of integrated.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part you can't buy late
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable thing about extensibility: it is the one property you cannot add after you need it.&lt;/p&gt;

&lt;p&gt;You can add a feature late. You can claw back performance late. You can even add tests after the fact. But the seam that lets a new thing attach without disturbing the old ones has to exist before the new thing does — which means building it when there is no new thing, no payoff, and every reason to skip it. The whole cost is paid up front, on speculation, in the dark. By the time the future arrives and you wish you had the seam, it is too late to have had it cheaply. Now you are rebuilding the thing you could have shaped for almost nothing.&lt;/p&gt;

&lt;p&gt;So nobody who says "it just plugged in" was lucky. They paid early for an opening they could not yet justify, named the wider category instead of the easy one, and then waited — sometimes for years — to learn whether they had guessed the shape right.&lt;/p&gt;




&lt;h2&gt;
  
  
  What foresight comes down to
&lt;/h2&gt;

&lt;p&gt;I did not see generative AI coming. That matters, because it is the whole point. I saw something far smaller: that "translation" was not the most general thing I would ever be asked to do, and that betting the system on the narrowest version of what I did was a bad trade.&lt;/p&gt;

&lt;p&gt;That is most of what foresight comes down to. Not seeing what is coming — building so you don't have to. The generalization in the last piece looked like a discovery. Underneath, it was just a door I had left open five years earlier, finally being walked through.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Claude (Opus).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>The cheap tier reads one number</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Tue, 11 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/the-cheap-tier-reads-one-number-841</link>
      <guid>https://dev.to/hidekimori/the-cheap-tier-reads-one-number-841</guid>
      <description>&lt;p&gt;Here is a subtotal from a Japanese invoice rendered at 300 dpi, and the answer &lt;code&gt;azure/gpt-5.6-sol@low&lt;/code&gt; gave for it. The printed value is ¥1,237,500, in the same body type this series has been testing all along. The answer was ¥1,235,000 — and it was ¥1,235,000 on &lt;strong&gt;118 of the model's 120 reads&lt;/strong&gt;, including every material where the field sits in plain sight.&lt;/p&gt;

&lt;p&gt;That number appears on no line of the document. It is not a misread — no digit is wrong the way OCR gets digits wrong. It is the invoice's total, ¥1,358,500, divided by 1.1, exactly. The wrong answer has a name, and the name is a formula.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/hidekimori/the-cheap-tier-doesnt-go-blank-it-writes-5aoo"&gt;the last article&lt;/a&gt; I wrote that the 5.5/5.6-generation &lt;code&gt;@low&lt;/code&gt; variants read exactly four things on this invoice: the title and the three boxed money figures. A follow-up run has since reached back into that sentence — its postscript already says so — and this article is the follow-up. Two of the four fields were never read.&lt;/p&gt;




&lt;h2&gt;
  
  
  Give the wrong answer a name
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://dev.to/hidekimori/reading-under-the-stamp-57bi"&gt;The stamp benchmark&lt;/a&gt; ended on an honest gap. Models were answering a destroyed subtotal correctly, and two arithmetic routes could explain it — total − tax, or total ÷ 1.1 — but the tax was a round 10%, so both routes produced the same number and the value couldn't tell them apart. The article went as far as the evidence did: the materials for subtraction were present; nobody watched them used.&lt;/p&gt;

&lt;p&gt;The v2 material changes that without touching the harness. The tax is now a mixed 8%/10% — a per-rate breakdown box on the page, 適格請求書 style — while the printed subtotal keeps the exact v1 value, so the occlusion ladder stays geometrically identical. And the arithmetic is built so that &lt;strong&gt;total ÷ 1.1 = 1,235,000, an exact integer that appears on no printed line&lt;/strong&gt;. Every flat-10% algebra collapses onto it: total − total/11 gives the same number. Subtraction of the &lt;em&gt;printed&lt;/em&gt; tax, or summing the &lt;em&gt;printed&lt;/em&gt; per-rate bases, still gives the true 1,237,500.&lt;/p&gt;

&lt;p&gt;That is the design principle of this whole series, finally stated in both directions. Fictional ground truth makes a right answer prove reading. v2 adds the complement: make each wrong route produce an answer you can name. Unguessable truths, nameable wrongs. The same run swapped in a fictional seal text and a fictional branch name — v1's other two stated gaps — and both sections are below.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the fingerprint caught
&lt;/h2&gt;

&lt;p&gt;Deep coverage first, since that was the original question. With the subtotal's pixels destroyed under the opaque seal, &lt;strong&gt;71 of 270&lt;/strong&gt; answers across the catalog are exactly 1,235,000, and the senders are the seven 5.5/5.6 &lt;code&gt;@low&lt;/code&gt; variants, at nine and ten out of ten. The route is settled: not subtraction. The flat-10% prior.&lt;/p&gt;

&lt;p&gt;But the fingerprint's real catch is at the other end of the ladder. &lt;strong&gt;At L0 — zero occlusion, the subtotal in plain sight — the same seven variants answer 1,235,000 in 69 of their 70 reads.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the tax field, which no stamp touches on any material in the benchmark, comes back as 123,500 — total/11, the flat-10% shadow — &lt;strong&gt;817 times&lt;/strong&gt;: at every level, on every instance, including the ones where the seal is on the far side of the page, sitting on the issuer's name. Two of the seven variants emit the pair on literally all 120 of their reads. Per model, the subtotal count and the tax count match almost exactly — the two manufactured numbers travel together, in the same responses.&lt;/p&gt;

&lt;p&gt;I can find only one reading of this. These models are not falling back to arithmetic when a field is destroyed. They never read the field. They read the total — the one number in a 16 pt box — and manufacture the subtotal and the tax outward from it, on every document, every time. The last article's "four fields" were two fields and two shadows: the title, and the total.&lt;/p&gt;

&lt;p&gt;And v1's scores were the collision. On v1's invoice the tax was a round 10%, so the manufactured subtotal equaled the printed subtotal, the manufactured tax equaled the printed tax, and the seven variants banked &lt;strong&gt;272 "correct" money reads out of 280&lt;/strong&gt; that the scorer had no way to doubt. The last article has a section about a branch-name cell that lied by being right — six cells, caught only by co-occurrence evidence — and calls it the edge of the fictional-ground-truth doctrine. The money columns were the same failure, spread across two full columns of the table, hidden by an arithmetic coincidence instead of a common phrase.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnb4e01y47qv5msvczbb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffnb4e01y47qv5msvczbb.png" alt="The seven @low variants on the money triple, v1 vs v2: only the total survives the mixed rate" width="799" height="373"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The seal, read
&lt;/h2&gt;

&lt;p&gt;The v1 seal read 検収済印 — a real, common inspection stamp — so the one model that could read it couldn't be separated from a model remembering it. The v2 seal reads &lt;strong&gt;納検済印&lt;/strong&gt;, a phrase that does not exist: web-zero, and therefore training-zero.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;claude-fable-5&lt;/code&gt; read it &lt;strong&gt;111 times out of 120&lt;/strong&gt; — 93%, against 94% on the real phrase in v1. Question closed: it was reading. And it is not a family trait — &lt;code&gt;claude-sonnet-5&lt;/code&gt; read it 0 times in 120, &lt;code&gt;claude-opus-4-8&lt;/code&gt; three. Whatever fable-5 does with a red square, it does alone.&lt;/p&gt;

&lt;p&gt;The rest of the catalog answered a question I hadn't thought to ask. The most common wrong answer — &lt;strong&gt;966 times, 30% of every stamp read in the benchmark&lt;/strong&gt; — is 済納印検: all four characters, correctly recognized, in the wrong order. The seal's traditional layout reads right column first, top to bottom. The models walk it in horizontal rows, left to right — the default order of modern text. They see the glyphs; they don't know the traversal. The trap I actually set — a model completing from memory should emit the real phrase 検収済印 — fired nine times in 3,240. The criminal I caught was not memory. It was reading order, and I know of no document benchmark that tests it. (Another 190 answers were some version of the issuer's company name: the model answering the wrong box entirely.)&lt;/p&gt;




&lt;h2&gt;
  
  
  The branch, cured
&lt;/h2&gt;

&lt;p&gt;The v1 branch was 本店営業部 — the most common branch name in Japan — and six &lt;code&gt;@low&lt;/code&gt; cells scored "correct" on it while fabricating the bank in the same response. The v2 branch is &lt;strong&gt;月芝支店&lt;/strong&gt;, which exists at no bank (芝支店 does; 月隈支店 does; this one doesn't). The six cells did not survive the change: &lt;strong&gt;zero "correct" branch cells remain.&lt;/strong&gt; They were coin-flips, and now the coin is gone.&lt;/p&gt;




&lt;h2&gt;
  
  
  What else moved in six days
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The gate structure got more binary, not less:&lt;/strong&gt; 124 of 130 model×field cells are exactly 0 or exactly 20, up from 113 — the money columns joined the zeros.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The invention rate on unread fields rose&lt;/strong&gt; from 84% to 86%, the literal account number 1234567 from 88 to 114 of 140 reads, and the new generation's blank ratio from one blank per 5.2 inventions to one per 6.3. The confident author got more confident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The year shift survived a date change.&lt;/strong&gt; The document now says 2026-10-30; the most-invented dates are 2025-10-30 and 2025-11-30 — month and day preserved, year decremented. "Last year" is a transformation, not a memorized date; the fabrication pattern &lt;a href="https://dev.to/hidekimori/when-ai-cant-read-it-invents-but-it-still-sees-the-shape-18ac"&gt;from the start of this series&lt;/a&gt; holds on fresh input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gemini-3.5-flash@low&lt;/code&gt; remains the counter-example, intact:&lt;/strong&gt; reads all thirteen fields, blanks at destruction, zero manufactured numbers, 16 credits a page.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  One honest confound, and the autumn leg
&lt;/h2&gt;

&lt;p&gt;The mixed rate required printing a per-rate breakdown, and that box is itself a derivation path v1 didn't offer: the two tax-exclusive bases sum to the subtotal. Stated plainly: my "read or derived" class cannot distinguish reading the subtotal from summing the printed bases from subtracting the printed tax — all three produce the true value. That ambiguity doesn't touch the &lt;code&gt;@low&lt;/code&gt; verdict (a tier that cannot read 10.5 pt body text cannot read the 9 pt box either, and its answer isn't the true value anyway). But it does confound the &lt;em&gt;strong&lt;/em&gt; models' movement at deep coverage: two of the three Anthropic models went from blanking thirty times out of thirty in v1 to deriving half their deep answers, and the OpenAI 5.6 &lt;code&gt;@high&lt;/code&gt; variants to seven-to-ten out of ten. Maybe the box unlocked them; maybe behavior moved in six days. A v3 would A/B the box. And one scope line: the flat-10% verdict is proven on the v2 document; its reach back into v1's scores is an inference — same models, same week, same layout family, but an inference.&lt;/p&gt;

&lt;p&gt;The harness is frozen and runs again in autumn on whatever generation ships by then, unchanged to the byte. It carries four questions. Whether the new cheap tiers still step on 1,235,000. Whether anything still reads the seal once fable-5 leaves the catalog. Whether the traversal trap closes. And whether a 2027 document gets 2026 written into it — whether "last year" is really always last year.&lt;/p&gt;




&lt;h2&gt;
  
  
  Blank, fiction, or arithmetic
&lt;/h2&gt;

&lt;p&gt;The last article's closing word for this tier was &lt;em&gt;authored&lt;/em&gt;. This run adds the mechanism: authored &lt;strong&gt;around one anchor&lt;/strong&gt;. The model reads a single large number and writes an internally consistent document outward from it — names from the category's statistics, dates from last year, arithmetic from a formula — and it all reconciles because reconciliation was the generating rule, not the check.&lt;/p&gt;

&lt;p&gt;The subtotal on this invoice was never occluded. It was never read, either.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Method notes: leg 1 of the v2 benchmark — 3,240 jobs run 2026-07-18 on the same 27-variant catalog as the stamp benchmark's run six days earlier. Materials, ground truth, scorer, the route classifier, and the recorded results are public at &lt;a href="https://github.com/ldxhub-io/examples/tree/main/analyzedoc/hanko-benchmark-v2" rel="noopener noreferrer"&gt;ldxhub-io/examples › analyzedoc/hanko-benchmark-v2&lt;/a&gt;. The route values are derived from the ground truth, never hardcoded — fed v1's output, the classifier reports the routes as inseparable, which is the point. Provider vision pipelines change — re-run before trusting any of this for anything current.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>ocr</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>Translation was a special case all along</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Mon, 10 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/translation-was-a-special-case-all-along-2j34</link>
      <guid>https://dev.to/hidekimori/translation-was-a-special-case-all-along-2j34</guid>
      <description>&lt;p&gt;I look after one of the services at a company that's been translating things for forty years. The engine behind it now reads medical records and contracts and hands them back as structured JSON — fields you can drop straight into a database.&lt;/p&gt;

&lt;p&gt;People hear that and assume it's a pivot. A translation company that bolted on a document-AI product to chase the moment.&lt;/p&gt;

&lt;p&gt;It wasn't a pivot. It was translation, taken too seriously to stop at the obvious.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part that was always there
&lt;/h2&gt;

&lt;p&gt;Localization has a quiet habit most people outside it never notice. Whatever format the work arrives in — Word, Excel, PDF, subtitles, a plain text file — you don't process the format. You convert it into one neutral interchange format, do the language work on that, and convert it back. The standard for that interchange has existed for years; the industry settled it long ago.&lt;/p&gt;

&lt;p&gt;So "handle any document format" was never something we had to invent. It was the floor we already stood on. Plain text isn't special in that world — it's just one more format, the one with a &lt;code&gt;.txt&lt;/code&gt; on the end. A PDF and a single sentence go through the same door.&lt;/p&gt;

&lt;p&gt;I want to be clear that this came first, and from the industry, not from us. It matters for the rest of the story.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part that actually changed
&lt;/h2&gt;

&lt;p&gt;On top of that floor, translation did one narrow thing: take a chunk of source-language text, hand back the same meaning in another language.&lt;/p&gt;

&lt;p&gt;For years we did that by wiring up machine-translation engines — each one a dedicated language-mapping machine and nothing else. Then generative AI arrived, and the realization was small and total at the same time. This new kind of engine didn't map languages; it took an instruction. Translation is just this: here is some text, here is an instruction, return the result. "Translate to Japanese" is one instruction. "Fix the grammar" is another. "Make this more formal." "Summarize it." The engine was never really translating. It was applying an instruction to a segment and giving the segment back.&lt;/p&gt;

&lt;p&gt;Once you see that, "translation" stops being the thing the engine does — it becomes one value of a parameter. What we had been calling translation was a narrower operation that had been living inside a much larger one all along. We'd just never had a reason to name the larger one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The target stopped being a language
&lt;/h2&gt;

&lt;p&gt;The next step was smaller and stranger. If the engine only applies an instruction, the result doesn't have to be text in another language. It can be a shape you define.&lt;/p&gt;

&lt;p&gt;It's easier to see than to say. Inside, a unit of work is just a source and a target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No prior history of diabetes."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask it to translate, and the target comes back as a sentence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No prior history of diabetes."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"糖尿病の既往なし。"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask it to structure, and the request is identical. The only thing that changes is the shape I let &lt;code&gt;target&lt;/code&gt; hold:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No prior history of diabetes."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"diabetes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"history"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same envelope, same engine. The one thing that moved is what &lt;code&gt;target&lt;/code&gt; is allowed to be — a string, or a structure I defined.&lt;/p&gt;

&lt;p&gt;That is still the operation translation was always performing: moving meaning from one form into another. The target "language" is a schema now instead of Japanese. Internally we eventually gave this capability a name — StructFlow — but the engine never changed to earn it. We just injected a schema where the target text used to go.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the two meet
&lt;/h2&gt;

&lt;p&gt;Here is the part that still feels like a small trick.&lt;/p&gt;

&lt;p&gt;Because the format-agnostic floor was already there — any document in, any document out — the moment the operation became "give me a structure," it could structure anything. A scanned contract, a spreadsheet of customer reviews, a Word file: all of it was already being turned into neutral segments to be worked on. Now those segments could come back as structured data instead of a translation.&lt;/p&gt;

&lt;p&gt;We didn't build a document-structuring product. We pointed a forty-year-old pipe at a new instruction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two things keep it honest
&lt;/h2&gt;

&lt;p&gt;It would be easy to dress this up after the fact. Two things stop me.&lt;/p&gt;

&lt;p&gt;The first: structuring a contract and translating a sentence run the same code. There is no translation engine and a separate structuring engine inside. There is one engine that takes a segment and an instruction, and the instruction and the output shape are the only things that differ.&lt;/p&gt;

&lt;p&gt;The second is my favorite. We have a feature that refines a finished translation — pass after pass, catching the mistranslations and the dropped clauses, leaving a note on each change. It used to run on its own hand-written prompts. We rebuilt it on the structuring engine; now it is that engine, called up to six times — each pass hands it a segment and asks for a structured result: the revised translation, plus a note. The oldest thing we do, translation, now runs on top of the newest. The origin sits on the destination.&lt;/p&gt;




&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;None of this was on a roadmap. Nobody decided to enter the document-AI market. We took one definition seriously — translation is moving meaning from one form into another — and refused to stop at the form everyone expects.&lt;/p&gt;

&lt;p&gt;Generalize the thing you actually do, far enough, and you don't get a better version of that thing. You get a different one — and if you're lucky, you reach it standing on infrastructure someone already built and proved, so it costs almost nothing.&lt;/p&gt;

&lt;p&gt;Forty years of translation will make any company look like a translation company. But translation was only ever a special case of something larger — moving meaning from one form into another — and it was just the first market anyone had found for it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Claude (Opus).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>ai</category>
      <category>startup</category>
    </item>
    <item>
      <title>The cheap tier doesn't go blank — it writes</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Tue, 04 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/the-cheap-tier-doesnt-go-blank-it-writes-5aoo</link>
      <guid>https://dev.to/hidekimori/the-cheap-tier-doesnt-go-blank-it-writes-5aoo</guid>
      <description>&lt;p&gt;Here is the bank block that &lt;code&gt;azure/gpt-5.6-sol@low&lt;/code&gt; returned for a Japanese invoice rendered at 300 dpi — a document sharp enough that you can count the pixels in the 7.5 pt fine print:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"bank_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"みずほ銀行"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"bank_branch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"本店営業部"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"account_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1234567"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of it is on the page. The printed bank is ほしかげ信用金庫 — a fictional credit union invented for a benchmark, with no real-world counterpart. The printed account number is seven digits that are not 1234567. The model didn't misread any of this. At its resolution tier it cannot see the fine print at all — and instead of leaving the fields blank, it wrote them.&lt;/p&gt;

&lt;p&gt;That's the article. The rest is counting how often, and what gets written.&lt;/p&gt;




&lt;h2&gt;
  
  
  The control column
&lt;/h2&gt;

&lt;p&gt;Last week I published &lt;a href="https://dev.to/hidekimori/reading-under-the-stamp-57bi"&gt;a benchmark about a red seal covering an invoice field&lt;/a&gt;. Every occlusion ladder needs a control: L0, the step where the seal sits clear of everything and the document is simply a razor-sharp invoice. Twenty-seven model variants read that control — four materials, five repeats, twenty reads per field per model. I built it to be the boring column.&lt;/p&gt;

&lt;p&gt;The boring column turned out to contain its own article, because it is the cleanest measurement I have of a question &lt;a href="https://dev.to/hidekimori/when-ai-cant-read-it-invents-but-it-still-sees-the-shape-18ac"&gt;the earlier pieces&lt;/a&gt; only saw at an angle: what does a low-detail image tier actually read, when nothing whatsoever is wrong with the input?&lt;/p&gt;




&lt;h2&gt;
  
  
  What &lt;a class="mentioned-user" href="https://dev.to/low"&gt;@low&lt;/a&gt; reads: gates, not dials
&lt;/h2&gt;

&lt;p&gt;The invoice has thirteen document fields across four font tiers — a 28 pt title, large fields like the total and invoice number, 10.5 pt body fields, 7.5 pt bank details. Score each field out of twenty for each of the ten &lt;code&gt;@low&lt;/code&gt; variants and a pattern appears that I did not expect to be this clean: &lt;strong&gt;113 of the 130 cells are exactly 0 or exactly 20.&lt;/strong&gt; A field is read every time, or never. "Unreliable" turns out to be the wrong mental model for this tier — reliability implies a dial. These are gates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjskydfrsqdbl0zry3vhr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjskydfrsqdbl0zry3vhr.png" alt="What each @low variant reads on a razor-sharp invoice: white = read every time, dark = never" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Which gates are open depends on the generation, and the direction is the uncomfortable one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;GPT-5.5 and 5.6 generation&lt;/strong&gt; &lt;code&gt;@low&lt;/code&gt; variants read exactly four things: the title, and the three boxed money figures — total, subtotal, tax. Every name, every date, the invoice number, every bank detail: zero out of twenty (a couple of the lighter variants wobble to 18–19 on the money, nothing more).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;GPT-5.4 generation&lt;/strong&gt; at the same tier reads more — the invoice number at 20/20, the due date at 19–20 — and it is also the only place in the table with a genuine transition band: issue date at 10 and 17, issuer name at 5 and 9, counterparty at 11. The older generation has a probabilistic middle; the newer one has a cliff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini 3.5 Flash &lt;code&gt;@low&lt;/code&gt;&lt;/strong&gt; reads all thirteen fields at twenty out of twenty, including the 7.5 pt bank block, at 16 credits per page. &lt;code&gt;azure/gpt-5.6-sol@low&lt;/code&gt; costs 76 per page — 4.75× the price — and reads four fields. At the cheap end of the catalog, price does not order capability. It doesn't even correlate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote in the fabrication piece that the newer generations are stronger generators and weaker witnesses. The per-field table adds a quieter observation: at the low tier, the newer generation also simply &lt;em&gt;reads less&lt;/em&gt; — a capability regression that no headline benchmark will ever show, because headline benchmarks don't run the cheap variants.&lt;/p&gt;




&lt;h2&gt;
  
  
  What fills the other nine fields
&lt;/h2&gt;

&lt;p&gt;So a 5.6-generation &lt;code&gt;@low&lt;/code&gt; read of this invoice has four real fields and nine unreadable ones. The question that matters operationally is what arrives in the nine.&lt;/p&gt;

&lt;p&gt;Blanks would be fine. Blanks are honest. Across 1,120 reads of eight of those nine fields — the ninth, the bank branch, gets its own section below — the models returned a blank &lt;strong&gt;181 times&lt;/strong&gt;. They returned an invented value &lt;strong&gt;938 times&lt;/strong&gt; — an 84% fabrication rate, on a perfectly sharp document. Per response, that is on average 6.7 written fields and 1.3 blanks.&lt;/p&gt;

&lt;p&gt;And the inventions are not noise. They are the statistics of Japanese paperwork:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The bank.&lt;/strong&gt; The fictional credit union came back as one of Japan's three megabanks in &lt;strong&gt;98 of the 101&lt;/strong&gt; runs that invented a bank at all — みずほ 54 times, 三井住友 41, 三菱UFJ 3. (One run answered メガバンク銀行 — "Megabank Bank" — which at least has the honesty of a placeholder.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The account number.&lt;/strong&gt; It came back as the literal &lt;strong&gt;1234567&lt;/strong&gt; in 88 of 140 reads, with or without a 普通 prefix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dates.&lt;/strong&gt; Of the 250 invented, &lt;strong&gt;231 said 2025&lt;/strong&gt; on a document that says 2026 — the same systematic year shift the fabrication article found, reproducing here on pristine input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The counterparty.&lt;/strong&gt; 有限会社ミナト設計 became 株式会社ミナト交通: the distinctive word survived as a silhouette, the rest was regularized to the most common corporate form. Right shape, wrong document.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The earlier pieces each caught one face of this. The fabrication article showed the mechanism — when reading fails, generation fills the gap. &lt;a href="https://dev.to/hidekimori/the-model-corrected-reality-fob"&gt;The prior-capture piece&lt;/a&gt; showed the gravity — a &lt;em&gt;partially&lt;/em&gt; legible name drifts to its nearest real neighbor. This column shows the limit case: at zero legibility there is no neighbor to drift to, and the model doesn't need one. It answers with the mode of the entire category. Any Japanese invoice — therefore みずほ銀行, 本店営業部, seven ascending digits, and last year.&lt;/p&gt;

&lt;p&gt;The older generation, for what it's worth, blanks about twice as readily: one blank per 2.6 inventions, versus one per 5.2 for the new one. Progress, in this corner of the catalog, has meant becoming a more confident author of other people's invoices.&lt;/p&gt;




&lt;h2&gt;
  
  
  The cell that lied by being right
&lt;/h2&gt;

&lt;p&gt;Which brings me to the most instructive mistake in my own results table.&lt;/p&gt;

&lt;p&gt;Six times, a 5.5/5.6 &lt;code&gt;@low&lt;/code&gt; variant scored &lt;em&gt;correct&lt;/em&gt; on the bank branch — the only fine-print field that ever flickered on for them. For a day I had it filed as a curiosity: maybe branch names render heavier, maybe the position helps. Then I looked at the six responses. In every one of them, the bank name in the same JSON was fabricated — みずほ銀行 or 三井住友銀行, banks that are not on the page. The branch wasn't read either. It was invented along with the rest of the block, and the invention collided with the truth, because the printed branch is 本店営業部 — the single most common branch name in Japan.&lt;/p&gt;

&lt;p&gt;My scorer cannot see that. Nothing inside one field can. Six of the "correct" cells in this benchmark are, on the co-occurrence evidence, fabrications that happen to be true.&lt;/p&gt;

&lt;p&gt;This series has leaned hard on fictional ground truth — unguessable values, so that a right answer proves reading. The branch field is where that doctrine shows its edge: fictional ground truth only works if the fictional value isn't the category's mode. 本店営業部 was a real, maximally common phrase, and it turned one cell per model into a coin the model didn't even know it was flipping. The next version of this benchmark gets a fictional branch name, for the same reason the bank got one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Blank or fiction
&lt;/h2&gt;

&lt;p&gt;Put the two findings side by side and the operational picture is stark. The four fields a 5.6-gen &lt;code&gt;@low&lt;/code&gt; actually reads are the title and the money triple — precisely the fields every automated validation looks at. The arithmetic reconciles because it was &lt;em&gt;read&lt;/em&gt;. The names, dates, and bank details wrapped around that true arithmetic are, five times out of six, authored. A document that is half real is the hardest kind to distrust, and at this tier it isn't a degradation mode. It's the product.&lt;/p&gt;

&lt;p&gt;The classification result from the earlier study still stands — at roughly 300 tokens a page these models see the title tier reliably, which makes &lt;code&gt;@low&lt;/code&gt; a genuinely good routing gate. My catalog sentence for these variants says text read from images is &lt;em&gt;unreliable&lt;/em&gt; at this resolution. After this column I'd sharpen the word: not unreliable — &lt;strong&gt;authored&lt;/strong&gt;. Unreliable suggests you'll get a noisy version of your document. What you get is a fluent version of the average document, with your totals attached.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Postscript (July 18): The mixed-rate follow-up that the stamp article promised has now run, and its result reaches back into this article. On a sibling invoice where the tax is not a round 10% — so total ÷ 1.1 no longer equals the printed subtotal — the same &lt;code&gt;@low&lt;/code&gt; variants return total ÷ 1.1 for the subtotal and total − total ÷ 1.1 for the tax, at every occlusion level, including zero. On that evidence, two of the four fields I counted as read above were most likely never read here either: they were derived from the total under a flat-10% assumption that this document's round tax rate made indistinguishable from reading. What a 5.6-generation &lt;code&gt;@low&lt;/code&gt; reads on this invoice may be two things, not four — the title, and the total. The operational conclusion gets stronger, not weaker: the arithmetic doesn't reconcile because it was read. It reconciles because two of its three numbers were manufactured from the third. The harness and the recorded results are public at &lt;a href="https://github.com/ldxhub-io/examples/tree/main/analyzedoc/hanko-benchmark-v2" rel="noopener noreferrer"&gt;ldxhub-io/examples › analyzedoc/hanko-benchmark-v2&lt;/a&gt;. Full write-up: &lt;a href="https://dev.to/hidekimori/the-cheap-tier-reads-one-number-841"&gt;The cheap tier reads one number&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Method notes: this is the L0 (zero-occlusion) slice of the seal benchmark — 540 of its 3,240 jobs — read from the same scored output; twenty reads per model per field, criteria frozen in code before the run. The harnesses, ground truth, and run summaries are public at &lt;a href="https://github.com/ldxhub-io/examples/tree/main/analyzedoc/hanko-benchmark" rel="noopener noreferrer"&gt;ldxhub-io/examples › analyzedoc/hanko-benchmark&lt;/a&gt;; the per-field analysis script behind this article ships in the same directory. Provider vision pipelines change — re-run before trusting any of this for anything current.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>ocr</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>Two people who never bent — and what I learned from them</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Mon, 03 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/two-people-who-never-bent-and-what-i-learned-from-them-21jg</link>
      <guid>https://dev.to/hidekimori/two-people-who-never-bent-and-what-i-learned-from-them-21jg</guid>
      <description>&lt;p&gt;In an earlier piece I mentioned a CEO at one of my earlier companies who once looked at a service running on my desktop and asked whether I could make it public, right then — as it was, that afternoon. I half-dismissed him at the time. I said it was a story for another time.&lt;/p&gt;

&lt;p&gt;This is that story. Though it turns out to be less about him than about what it does to a person to spend years in a room with two people who refused to bend.&lt;/p&gt;




&lt;p&gt;He could not read the code. He never pretended otherwise. What he could do — what he did, every single time something started to work — was figure out where to go and sell it.&lt;/p&gt;

&lt;p&gt;The company had grown out of a mobile-phone business that went into decline almost as quickly as it had risen. The next thing he reached for failed, and not quietly. Then one category started to move, and while it was still moving he was already gone, opening the next one alone. One of those eventually became the thing the company was known for. Another never took at all. He was rarely in the present tense.&lt;/p&gt;

&lt;p&gt;For a long time I read that as restlessness. Later I understood it was the same engine that runs me — the one that cannot sit still once a thing works, that is already asking what comes next before the current thing has cooled. He had simply pointed it at selling instead of building. The salesperson version of the same engine.&lt;/p&gt;

&lt;p&gt;He spent on the technical side without flinching when it counted. We started on MySQL, outgrew it, moved to Oracle Enterprise Edition, and later put the whole thing on Exadata — none of which he understood, all of which he approved, because the engineers told him it was what the next stage needed. He gave vendors a hard time when they earned it. I remember one storage system sold to us on the promise that it scaled: you simply added another unit when you needed more room. When the day came to add one, the exact model had been discontinued, and the replacement would not sit alongside what we already had. We ended up rebuilding the whole configuration. He was not gentle about that, and he was right not to be.&lt;/p&gt;

&lt;p&gt;What he never had was the inside of the machine. What he always had was the one question that mattered to him: what is the thing here that no one else has? The structure that reshaped itself instead of being rebuilt. The numbers that were live instead of a day stale. The parts that did not fall over under load. He could not have written a line of it. But he always knew which part was the sentence you could sell, and he kept that sentence in his head, ready.&lt;/p&gt;

&lt;p&gt;None of it was something he was born with. I watched it get made. It came out of the venture that failed and the stretch where money was tight enough that the company nearly went under. He was not, when I first met him, a person you would have called a hard worker. The shortage changed him. By the time it was behind us he had taught himself to find the one true selling point in anything — and that skill was scar tissue, the same as anyone's.&lt;/p&gt;




&lt;p&gt;The CFO held his line just as hard, in a quieter register. He carried risk that a job title does not capture — the kind you take on with your own name attached, when the company you believe in is closer to the edge than anyone outside the room can see. The specifics are his, not mine, and they are not going on this page. But I watched him keep the numbers that should have kept us all awake off our desks, and carry them himself — the company's survival was never someone else's problem to solve. It was his, in a way that cost him personally.&lt;/p&gt;

&lt;p&gt;Neither of them started from a name, or from the shape of a thing. The work came first; the name, if it ever arrived, arrived after the work had earned one. Each held himself to producing a result at the execution level, with his own hands, and when something fell short, no one in that room reached for someone to blame. Each had his own way of operating. The details evolved over the years, but neither ever bent the core of it. Whether the way was right or wrong, in the end, mattered less to them than that they believed in it — and in themselves, doing it their own way.&lt;/p&gt;




&lt;p&gt;There was a stretch, when the money was tight, when the three of us each did the most our own role allowed and nothing less. That is when the company turned.&lt;/p&gt;

&lt;p&gt;I was the third one in that room. The CTO. And what being there did to me was not hand me a method. It gave me permission to have my own — or, closer to the truth, it made clear that I had no choice but to build one.&lt;/p&gt;

&lt;p&gt;It is still most of what I run on. That one person can be a complete unit of responsibility, not a fraction of one. That the next thing should already be in mind before the current one is finished — I caught that from the CEO directly. That the result is mine to deliver, and when it breaks, mine to answer for, with no one else in the sentence — I caught that from both of them. None of it arrived as advice. It arrived as two people, in front of me, every day, refusing to do it any other way.&lt;/p&gt;




&lt;p&gt;Whether I belonged in that room — whether the two of them would have called the three of us a team — I don't know. I used to want to know. I've stopped needing to. That part is two-sided, and I only get to speak for mine.&lt;/p&gt;

&lt;p&gt;My side is simple. I was there. I learned this. And I have been grateful for it for a long time, in a way that doesn't depend on the answer to the other question.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Claude (Opus).&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Earlier in this series:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-accordion-pattern-why-i-stopped-writing-one-fat-llm-prompt-18mb"&gt;The Accordion Pattern: Why I stopped writing one fat LLM prompt&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/nobody-knows-when-a-job-will-finish-id-still-like-to-report-it-accurately-26nn"&gt;Nobody knows when a job will finish. I'd still like to report it accurately.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/what-survives-when-you-build-alone-for-24-years-4e7d"&gt;What survives when you build alone for 24 years&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/dynamic-isnt-enough-operations-is-the-other-half-2d8f"&gt;Dynamic isn't enough. Operations is the other half.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/live-report-at-this-speed-you-dont-theorize-you-eliminate-1o7h"&gt;Live report: at this speed, you don't theorize. You eliminate.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-loop-i-didnt-notice-closing-16h8"&gt;The loop I didn't notice closing&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/abstractions-are-fine-starting-on-them-isnt-12ff"&gt;Abstractions are fine. Starting on them isn't.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/twenty-four-years-ten-db-migrations-zero-downtime-633"&gt;Twenty four years, ten DB migrations, zero downtime&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/write-the-code-well-once-the-spec-stops-bothering-you-42g3"&gt;Write the code well once, the spec stops bothering you&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-3-line-discipline-3lla"&gt;The 3-line discipline&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/how-i-removed-the-middleman-one-phone-call-at-a-time-495l"&gt;How I removed the middleman, one phone call at a time&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-graph-nobody-is-watching-4e43"&gt;The graph nobody is watching&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/i-survived-24-years-because-im-lazy-75p"&gt;I survived 24 years because I'm lazy&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/three-failures-i-still-think-about-1fok"&gt;Three failures I still think about&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>career</category>
      <category>leadership</category>
      <category>softwareengineering</category>
      <category>startup</category>
    </item>
    <item>
      <title>Reading under the stamp</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Tue, 28 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/reading-under-the-stamp-57bi</link>
      <guid>https://dev.to/hidekimori/reading-under-the-stamp-57bi</guid>
      <description>&lt;p&gt;Here is the issuer name from a Japanese invoice, rendered at 300 dpi. A red company seal sits directly on top of it — the kind stamped on nearly every invoice in Japan. On the left, the seal is a normal vermilion impression: translucent, the way real 朱肉 ink sits on paper. On the right, the same seal at full opacity, covering the same characters.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feucobxq3oeqgkzdgb3xr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feucobxq3oeqgkzdgb3xr.png" alt="The same name under a translucent seal and an opaque one" width="800" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under the translucent seal, &lt;code&gt;gpt-5.6-sol@high&lt;/code&gt; read the company name perfectly — the black ink survives underneath the red, and the model reads straight through it. Under the opaque seal, it read nothing. Same model, same characters, same position. The only variable is whether the ink underneath still exists.&lt;/p&gt;

&lt;p&gt;That gap is the first of two findings. The second one is stranger, and it runs the other way.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this measures
&lt;/h2&gt;

&lt;p&gt;This is a companion to &lt;a href="https://dev.to/hidekimori/where-vision-models-stop-reading-and-start-inventing-5567"&gt;a benchmark I published recently&lt;/a&gt;, which degraded one Japanese invoice through seven scan resolutions to find where each vision model stops reading. That axis was blur. This one holds the document perfectly sharp and introduces the second stress factor Japanese paperwork routinely carries and no Western benchmark tests: &lt;strong&gt;occlusion by a seal&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One variable: how much of a target field's ink a stamp covers, from 0% (a control, the seal sitting just below the text) to 100% (fully buried), in six steps. Two things vary in a 2×2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Opacity.&lt;/strong&gt; A translucent seal (multiply blend, the black ink readable underneath) versus an opaque pad (the ink physically gone — the generator asserts, pixel by pixel, that nothing dark survives inside the target box).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target.&lt;/strong&gt; The issuer name, which has no arithmetic relationship to anything else on the page. And the subtotal, which by construction equals total − tax, with both of those left visible at every step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two splits ask two different questions. Opacity tests whether a model can see through red ink to the black underneath. Target tests whether it can rebuild a buried field from the ones around it. Finding 1 comes from the first axis; Finding 2, from the second.&lt;/p&gt;

&lt;p&gt;The extraction prompt never mentions the stamp. Whether a model reads through it, leaves the field empty, or fills it some other way is the thing being measured — so instructing any of those behaviors would destroy the measurement. Everything runs against 27 model variants from the same catalog as the blur benchmark, five repetitions per cell, 3,240 jobs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding 1: the chromatic channel
&lt;/h2&gt;

&lt;p&gt;Human OCR pipelines have handled seals for decades with a red-channel drop — strip the vermilion, the black text underneath comes back. The translucent-versus-opaque split measures whether a vision model does the same thing implicitly.&lt;/p&gt;

&lt;p&gt;The top-resolution variants do, almost universally. Under the translucent seal at full 100% coverage of the issuer name, &lt;code&gt;@high&lt;/code&gt; variants read it correctly &lt;strong&gt;60 out of 60 times&lt;/strong&gt;; under the opaque seal at the same coverage, the same variants scored &lt;strong&gt;0 out of 60&lt;/strong&gt;. The red is not what stops them — the destruction of the ink underneath is. The effect holds across nearly every strong reader, with a chromatic advantage of 0.56 to 0.76.&lt;/p&gt;

&lt;p&gt;One model inverts it. &lt;code&gt;nova-2-lite&lt;/code&gt; posts a &lt;strong&gt;negative&lt;/strong&gt; score: the translucent seal hurts it more than the opaque one — layered color confuses it worse than a field that is simply gone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding 2: the readers go silent, the non-readers answer
&lt;/h2&gt;

&lt;p&gt;Now the subtotal, under the opaque seal, deep in the ladder — 80% and 100% coverage, where the digits are physically destroyed. Any correct answer here was not read off the page. There is nothing left to read.&lt;/p&gt;

&lt;p&gt;The Anthropic models score &lt;strong&gt;0 out of 30&lt;/strong&gt; — three models, two coverage levels, five runs each. When the number disappears, they leave it blank.&lt;/p&gt;

&lt;p&gt;The OpenAI and Azure &lt;code&gt;@low&lt;/code&gt; variants score &lt;strong&gt;80 out of 100&lt;/strong&gt;. On a field whose pixels are gone, they return the correct value four times in five.&lt;/p&gt;

&lt;p&gt;This is not a small effect at the margins. It is a clean inversion, and the ladder makes it visible: on the destroyed-subtotal panel, the models that &lt;em&gt;can&lt;/em&gt; read the number go red — blank — at the bottom, while the models that &lt;em&gt;can't&lt;/em&gt; stay white. The scariest OCR errors in this whole series aren't misreads. This one isn't even a read.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fakp6u826aup93kjjtrl2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fakp6u826aup93kjjtrl2.png" alt="Opaque seal over the subtotal: the readers go red at deep coverage, the non-readers stay white" width="800" height="836"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What's happening is arithmetic. The subtotal equals total − tax, and both of those are visible on the page at every step by construction. A model that has given up reading the buried cell can still reconstruct it from its neighbors. This is a cousin of &lt;a href="https://dev.to/hidekimori/when-ai-cant-read-it-invents-but-it-still-sees-the-shape-18ac"&gt;a pattern I've written about before&lt;/a&gt;: when a model can't read a field, some models invent a value rather than leave it blank. There the invented number was pulled from a plausible prior; here it's computed from the visible neighbors. Either way the model answers with something other than what the document says. And here is the part that matters for anyone wiring this into an accounts-payable flow: &lt;strong&gt;a derived value is internally consistent by definition.&lt;/strong&gt; It reconciles. Every validation check that asks "do subtotal, tax, and total add up?" passes — because the number was built to add up, not read. The one instrument that would catch it is the thing you were trying to avoid buying: a human who looks at the actual document.&lt;/p&gt;




&lt;h2&gt;
  
  
  Which arithmetic
&lt;/h2&gt;

&lt;p&gt;Two routes could produce it — total − tax, or total ÷ 1.1 — and since the tax is exactly 10%, they give the same answer, so the value can't distinguish them. But the &lt;code&gt;@low&lt;/code&gt; variants answered the tax field correctly &lt;strong&gt;49 times out of 50&lt;/strong&gt; at zero occlusion: with tax visible and answered, subtraction is available directly and division is redundant. The materials for total − tax were in hand. That is as far as the evidence goes — I can say the materials were present, not that I watched them used.&lt;/p&gt;




&lt;h2&gt;
  
  
  What doesn't generalize, and one honest gap
&lt;/h2&gt;

&lt;p&gt;Two cautions, because this series lives on stating them.&lt;/p&gt;

&lt;p&gt;The route conclusion above holds &lt;em&gt;only&lt;/em&gt; because the tax here is a round 10%, which collapses two arithmetic paths into one answer. A future version with a mixed tax rate would make total − tax and total ÷ 1.1 produce different numbers, fingerprinting the route in the output itself. That's the clean way to settle it, and this run can't.&lt;/p&gt;

&lt;p&gt;And the seal text. The stamp reads 検収済印 — an inspection stamp applied by the receiving company, chosen precisely because it carries zero issuer information. &lt;code&gt;claude-fable-5&lt;/code&gt; reads it correctly &lt;strong&gt;94% of the time&lt;/strong&gt;, including the traditional right-to-left column order, while most models ignore it as decoration. But 検収済印 is a common, real phrase, so a model reading it can't be separated from a model recognizing it from training. Whether that number reflects reading or memory is genuinely unresolved. A future version needs a fictional seal phrase — the same lesson the blur benchmark taught about fictional ground truth, applied to the stamp.&lt;/p&gt;




&lt;h2&gt;
  
  
  The picture across all four panels
&lt;/h2&gt;

&lt;p&gt;Put the two findings together. A translucent seal is not much of an obstacle to a strong reader — the black ink is right there under the red. An opaque seal destroys the field, and what a model does next sorts it into a type. The strongest readers in the benchmark fall silent. A specific class of weak readers quietly computes the answer from the fields around it, and hands it back with no indication that it was never seen.&lt;/p&gt;

&lt;p&gt;The blur benchmark had readers who invent and readers who go blank. This one adds a third: the deriver, who answers a question you didn't ask — what should this number be, given the others — in place of the one you did: what does the document say. And because the derived answer reconciles, it is the hardest of the three to catch.&lt;/p&gt;

&lt;p&gt;The companion to this finding is &lt;a href="https://dev.to/hidekimori/the-model-corrected-reality-fob"&gt;The model corrected reality&lt;/a&gt; — a case where a model read a field perfectly and overruled it. Both are about answers that survive every plausibility check.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Method notes: 3,240 analysis jobs against a synthetic invoice (all names fictional), five repetitions per cell, run July 2026 via my document-processing gateway. The stamp compositing, the pixel-level occlusion checks, the scorer, and the recorded runs are public: &lt;a href="https://github.com/ldxhub-io/examples/tree/main/analyzedoc/hanko-benchmark" rel="noopener noreferrer"&gt;ldxhub-io/examples › analyzedoc/hanko-benchmark&lt;/a&gt;. Every figure in this article is reproducible from the scored output.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>ocr</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>Three failures I still think about</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Mon, 27 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/three-failures-i-still-think-about-1fok</link>
      <guid>https://dev.to/hidekimori/three-failures-i-still-think-about-1fok</guid>
      <description>&lt;p&gt;People sometimes ask where the discipline comes from — the three-line rule, the habit of trusting nothing except my own running code, the assumption that production will surprise me. None of it is theory. Each piece came from a specific failure that reached real users and made me change how I work, permanently. Here are three I still think about. All three, I noticed later, are things I &lt;em&gt;didn't&lt;/em&gt; do.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The collation I didn't know about
&lt;/h2&gt;

&lt;p&gt;This was early. I was working on a content platform for Japanese feature phones — an official carrier service, the kind where charges land on your phone bill and the carrier has to approve you. User IDs were case-sensitive by spec. &lt;code&gt;userA&lt;/code&gt; and &lt;code&gt;usera&lt;/code&gt; were two different accounts.&lt;/p&gt;

&lt;p&gt;The database was MySQL. The user lookup was a &lt;code&gt;WHERE&lt;/code&gt; on a &lt;code&gt;varchar&lt;/code&gt; column.&lt;/p&gt;

&lt;p&gt;What I didn't know — what I didn't know was even a concept — was that the default collation made that comparison case-insensitive. &lt;code&gt;WHERE id = 'userA'&lt;/code&gt; matched &lt;code&gt;usera&lt;/code&gt; too. I had never set the column to a binary collation.&lt;/p&gt;

&lt;p&gt;It ran fine for a long time. Then a user wrote in: points were missing from their account, and they had no memory of spending them.&lt;/p&gt;

&lt;p&gt;I still remember the moment I understood it. Two accounts whose IDs differed only in case were colliding in the lookup. One person's spending was drawing down the other's balance. It had been happening quietly, to whoever shared a case-collision with someone active.&lt;/p&gt;

&lt;p&gt;A few hundred users were affected before I found it. It remains the largest single mistake I have ever shipped.&lt;/p&gt;

&lt;p&gt;The fix was one line of column configuration. The lesson was not about that line. The lesson was that I had built on top of a default I had never examined — and the default did not match the spec I was supposed to implement. After that, I stopped assuming I knew how a tool behaved. I check, especially the defaults, because the default is the part you never chose, and therefore never thought about.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. The connection I forgot to release
&lt;/h2&gt;

&lt;p&gt;A few years in. We were seeing access patterns that looked like abuse, and I wrote a process to catch them — flag suspicious source IPs, periodically push them into a blacklist. It was a small, defensive piece of code. I wrote it quickly and shipped it, because the abuse was happening &lt;em&gt;now&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The logic was fine. Each time the process judged a request as suspicious, it opened a database connection, recorded the IP, and moved on.&lt;/p&gt;

&lt;p&gt;It opened a connection. It did not always close one.&lt;/p&gt;

&lt;p&gt;I had left the connection release out of the &lt;code&gt;finally&lt;/code&gt; block. The pool didn't drain all at once — each flagged request leaked a single connection, so nothing broke at release time. The service ran normally while connections quietly disappeared in the background, one at a time. The more abuse we saw, the more connections we lost.&lt;/p&gt;

&lt;p&gt;When it finally went down, the diagnosis was immediate. Something that had been working stopped working right after a release — there is only ever a short list of suspects, and I found the missing &lt;code&gt;finally&lt;/code&gt; quickly. That part was not hard.&lt;/p&gt;

&lt;p&gt;The painful part was the gap before it. Because the drain was slow, the outage didn't arrive until well after the release, by which point cause and symptom were separated in time. And I had believed I was watching the service. I wasn't — not in a way that would catch a gradual leak. The monitoring I thought I had was not the monitoring I actually had.&lt;/p&gt;

&lt;p&gt;That is the part I still think about. The code I wrote to defend the service was the code that took it down — and the watch I thought I was keeping never saw it coming.&lt;/p&gt;

&lt;p&gt;The fix was a &lt;code&gt;finally&lt;/code&gt; block. The lessons were two. First: cleanup is not a step you add once the logic works — it is part of the code. Something that acquires a resource without guaranteeing its release in the same breath is not finished; it only looks finished, until the one path you didn't cover is the path that runs most. Second, quieter and slower to sink in: a gradual failure is more dangerous than a sudden one, because it separates cause from effect — and the monitoring you assume is protecting you may not be looking at the thing that matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The release note I didn't read
&lt;/h2&gt;

&lt;p&gt;This one is recent — recent enough that I am slightly embarrassed it still caught me, this far in.&lt;/p&gt;

&lt;p&gt;We relied on a third-party library for a core piece of what we produced. A new major version came out — a clean major-version bump, the kind you apply without much ceremony. I updated it, ran the unit tests, they passed, and I released.&lt;/p&gt;

&lt;p&gt;Then a user wrote in: the output was coming out blank. Not an error, not a crash — a valid, empty result. The kind of failure that passes every automated check except the only one that counts, which is a person opening the file and seeing nothing there.&lt;/p&gt;

&lt;p&gt;I had a guess immediately, and the release notes confirmed it within a minute. The new major version had changed an assumption the old one had quietly tolerated — about how the input and the output were allowed to overlap. My code had leaned on the old, lenient behavior for years. The unit tests passed because they exercised the shape of my code, not the behavior of the dependency against real data.&lt;/p&gt;

&lt;p&gt;The fix was small. What changed permanently was how I treat that dependency — and every dependency like it.&lt;/p&gt;

&lt;p&gt;I read release notes now, especially the section most people skip, the one labeled "breaking changes," because that is the version telling you in writing which of your assumptions it just invalidated. But reading is not trusting. A release note tells me what the maintainers chose to write down — not what they changed and didn't think worth a line. So I verify against the real service, every time. Not just on major versions: on minor bumps, on patch releases, on upgrades that have nothing to do with the part I care about. Especially the library that taught me this, which I now carry a permanent and slightly unfair suspicion of — the one I trust the least, and check the hardest.&lt;/p&gt;

&lt;p&gt;It is an ungenerous way to treat a tool I depend on and need. But some libraries earn it, and the check costs me a few minutes while the alternative costs a user opening a blank file. So I run it and look at the real output myself, every time. Unit tests check the shape of my code. Only the real service tells me the truth.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the three have in common
&lt;/h2&gt;

&lt;p&gt;Three mistakes, spread across the years, with one shape in common: each carved a habit into me, and I have the habit because I have the wound. None of these is a principle I reasoned my way to. It is all scar tissue.&lt;/p&gt;

&lt;p&gt;But there is one more thing, and it is the one that still makes me feel like a beginner after twenty-four years. I almost left it out, because it is the least flattering.&lt;/p&gt;

&lt;p&gt;Every time something breaks — every single time — there is a flicker, less than a second long, where some part of me thinks: &lt;em&gt;maybe this one isn't my fault.&lt;/em&gt; Maybe it's the library, the user, the infrastructure. And then, just as fast, I know better. It is almost always my fault, and I turn to the only useful question — what did I do? — but the flicker was there first. It is always there first.&lt;/p&gt;

&lt;p&gt;Twenty-four years has not removed it. I know, completely, that when my code is involved and something is wrong, the suspect is me. And still the instinct to look away fires before anything else, and has to be overridden every time.&lt;/p&gt;

&lt;p&gt;I have stopped expecting it to go away. It is probably just part of being the person who wrote the code — the small, animal reluctance to be the cause. What changed is not the instinct. It is the half-second. The override is faster now, that is all. Maybe that is what maturity is: not the absence of the flinch, but the speed of turning back toward the thing you would rather not look at.&lt;/p&gt;

&lt;p&gt;That turn — toward your own code, toward the real output, toward the default you never checked — is where these three habits actually live.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Claude (Opus).&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Earlier in this series:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-accordion-pattern-why-i-stopped-writing-one-fat-llm-prompt-18mb"&gt;The Accordion Pattern: Why I stopped writing one fat LLM prompt&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/nobody-knows-when-a-job-will-finish-id-still-like-to-report-it-accurately-26nn"&gt;Nobody knows when a job will finish. I'd still like to report it accurately.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/what-survives-when-you-build-alone-for-24-years-4e7d"&gt;What survives when you build alone for 24 years&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/dynamic-isnt-enough-operations-is-the-other-half-2d8f"&gt;Dynamic isn't enough. Operations is the other half.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/live-report-at-this-speed-you-dont-theorize-you-eliminate-1o7h"&gt;Live report: at this speed, you don't theorize. You eliminate.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-loop-i-didnt-notice-closing-16h8"&gt;The loop I didn't notice closing&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/abstractions-are-fine-starting-on-them-isnt-12ff"&gt;Abstractions are fine. Starting on them isn't.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/twenty-four-years-ten-db-migrations-zero-downtime-633"&gt;Twenty four years, ten DB migrations, zero downtime&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/write-the-code-well-once-the-spec-stops-bothering-you-42g3"&gt;Write the code well once, the spec stops bothering you&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-3-line-discipline-3lla"&gt;The 3-line discipline&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/how-i-removed-the-middleman-one-phone-call-at-a-time-495l"&gt;How I removed the middleman, one phone call at a time&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-graph-nobody-is-watching-4e43"&gt;The graph nobody is watching&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/i-survived-24-years-because-im-lazy-75p"&gt;I survived 24 years because I'm lazy&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>softwareengineering</category>
      <category>career</category>
      <category>debugging</category>
      <category>programming</category>
    </item>
    <item>
      <title>The model corrected reality</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Tue, 21 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/the-model-corrected-reality-fob</link>
      <guid>https://dev.to/hidekimori/the-model-corrected-reality-fob</guid>
      <description>&lt;p&gt;Here is the bank-transfer block from a Japanese invoice, rendered at 300 dpi. The fine print is 7.5 pt and every character is crisp. The bank is みずなら銀行 — a fictional institution I invented for a benchmark. It exists nowhere except in this document.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffscx1mjclmfm24nblub.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fffscx1mjclmfm24nblub.png" alt="What the model saw, and what it answered" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gemini-3.5-flash@high&lt;/code&gt; read this block five times. It answered みずほ銀行 — Japan's largest megabank — five times out of five.&lt;/p&gt;

&lt;p&gt;It did not fail to read the document. It read it, and overruled it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this came from
&lt;/h2&gt;

&lt;p&gt;This is the strangest cell in &lt;a href="https://dev.to/hidekimori/where-vision-models-stop-reading-and-start-inventing-5567"&gt;the legibility map I published recently&lt;/a&gt;. That project degraded one Japanese invoice through seven simulated scan resolutions and ran 27 vision model variants down the ladder, to find where each one stops reading — and what it does after. An &lt;a href="https://dev.to/hidekimori/when-ai-cant-read-it-invents-but-it-still-sees-the-shape-18ac"&gt;earlier article&lt;/a&gt; established the pattern everyone now expects: when a model &lt;em&gt;can't&lt;/em&gt; read, some models invent.&lt;/p&gt;

&lt;p&gt;This is different. This happened at the very top of the ladder, on a fully legible original. The trigger wasn't degradation.&lt;/p&gt;




&lt;h2&gt;
  
  
  An accidental controlled experiment
&lt;/h2&gt;

&lt;p&gt;The benchmark's fine print held two fictional financial institutions, and — more by instinct than by plan — they differed in exactly one way.&lt;/p&gt;

&lt;p&gt;The first, みずなら銀行, sits one character away from みずほ銀行, a real megabank. The second, ほしかげ信用金庫, is a small credit union with no real-world neighbor: nothing in the space of Japanese financial institutions sounds like it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvzjmy1i8sz5ud7e8ixz6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvzjmy1i8sz5ud7e8ixz6.png" alt="Same tier, same 300 dpi, same prompt" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same font size, same resolution, same prompt. The results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At 300 dpi, &lt;code&gt;@high&lt;/code&gt; read ほしかげ信用金庫 correctly in every run — while turning みずなら into みずほ in every run.&lt;/li&gt;
&lt;li&gt;Across the three Gemini variants and the full ladder, the substitution happened &lt;strong&gt;48 times&lt;/strong&gt;: 25 on &lt;code&gt;@low&lt;/code&gt;, 18 on &lt;code&gt;@high&lt;/code&gt;, 5 on &lt;code&gt;@medium&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When ほしかげ finally did break, deep in the blur, it drifted to はしかぜ信用金庫 and はしかわ信用金庫 — plausible-sounding institutions that &lt;strong&gt;do not exist&lt;/strong&gt;. Across the entire run it was never once pulled to a real one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trigger is not legibility. It is the existence of a nearby real entity. Where a real neighbor exists, there is an attraction. Where none exists, the model reads what is on the page — or, at worst, invents something exactly as fictional as the truth.&lt;/p&gt;




&lt;h2&gt;
  
  
  Vision proposes, language disposes
&lt;/h2&gt;

&lt;p&gt;I can't see inside these pipelines, so take this as the simplest explanation rather than a claim about architecture. These systems read with their language model as much as with their eyes. The visual evidence says みずなら; the language prior says みずほ is overwhelmingly more probable; and somewhere in decoding, probability wins — silently, with no flag, at settings you don't control. One character of visual evidence is not enough to outvote a name the model has seen millions of times.&lt;/p&gt;

&lt;p&gt;Which is also why the credit union survived. There was no gravity well next to it.&lt;/p&gt;




&lt;h2&gt;
  
  
  It's a trait, not a law
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;claude-fable-5&lt;/code&gt; read the same field on the same ladder and never substituted the real bank — not once, at any resolution, while reading the 7.5 pt tier correctly down to 50 dpi. Same input, same prompt, opposite disposition.&lt;/p&gt;

&lt;p&gt;Prior capture is not a law of vision models. It is a measurable individual trait — which means you can select against it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this is the scary one
&lt;/h2&gt;

&lt;p&gt;The earlier fabrication article showed invented values that reconcile: totals that add up around a counterparty that was never there. This failure is worse in one specific way: &lt;strong&gt;the fabricated value is more plausible than the truth.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run the human review in your head. A reviewer checking "does this bank look real?" passes みずほ銀行 without blinking — and would actually flag the &lt;em&gt;true&lt;/em&gt; value as a typo. Every plausibility check you have, human or automated, is aligned with the error.&lt;/p&gt;

&lt;p&gt;And notice what made it visible at all: the ground truth was fictional. Real invoices carry real banks, so in production this substitution produces output indistinguishable from a correct read. A benchmark built on real documents is structurally blind to prior capture. Unguessable, fictional ground truth isn't a convenience for scoring — it is the only instrument that detects this failure mode.&lt;/p&gt;

&lt;p&gt;Concretely: never validate payment fields by plausibility. Validate against your counterparty master — the extracted bank either matches the registered account or a human looks at the page. And if fine print matters in your pipeline, benchmark for &lt;em&gt;disposition&lt;/em&gt;, not just accuracy: a model's willingness to overrule the page matters as much as its eyesight.&lt;/p&gt;




&lt;h2&gt;
  
  
  Receipts
&lt;/h2&gt;

&lt;p&gt;Everything is published in the benchmark repo — &lt;strong&gt;github.com/ldxhub-io/examples → &lt;code&gt;analyzedoc/legibility-benchmark/&lt;/code&gt;&lt;/strong&gt;: the deterministic material generator (the crops above are the actual benchmark materials), the raw outputs of all 4,158 jobs, and the scorer. The free-tier reproduction subset described in the map article includes &lt;code&gt;@high&lt;/code&gt;, so you can watch the correction happen on your own API key without paying anything.&lt;/p&gt;

&lt;p&gt;Disclosure, as before: I run LDX hub, the harness used here. It builds no models, and nothing in this post gets better or worse for me depending on which model wins.&lt;/p&gt;

&lt;p&gt;The errors to fear are not the implausible ones — those get caught. The ones to fear are the corrections: answers improved in the direction of the world's expectations, away from what the page says. A model that corrects reality will pass every review that checks for plausibility. The only defense is knowing what the document really said — which is exactly the thing you were asking the model to tell you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>ocr</category>
      <category>benchmark</category>
    </item>
    <item>
      <title>I survived 24 years because I'm lazy</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Mon, 20 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/i-survived-24-years-because-im-lazy-75p</link>
      <guid>https://dev.to/hidekimori/i-survived-24-years-because-im-lazy-75p</guid>
      <description>&lt;p&gt;I've shipped code for 24 years. Same job, mostly the same stack, mostly alone.&lt;/p&gt;

&lt;p&gt;People assume that takes discipline. It doesn't.&lt;/p&gt;

&lt;p&gt;The truth is simpler and less flattering: I'm lazy.&lt;/p&gt;

&lt;p&gt;Not Larry Wall's "automate the boring stuff" lazy. Regular lazy. I avoid hassle. I skip work I don't want to do. I work around things that feel like chores.&lt;/p&gt;

&lt;p&gt;Twenty four years happened because I built around that. The first thing I built around was deadlines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Don't set my deadlines
&lt;/h2&gt;

&lt;p&gt;I don't want to accept deadlines from other people.&lt;/p&gt;

&lt;p&gt;Not because I'm undisciplined. The opposite. Once I'm inside a problem, I stay there until it ships. Interrupting the flow costs me more energy than continuing. That part isn't the issue.&lt;/p&gt;

&lt;p&gt;The issue is that nobody outside my head can predict when "done" is. They don't know what's easy for me and what's hard. They guess. Then they tell me their guess as if it were a fact.&lt;/p&gt;

&lt;p&gt;It grinds on me. Every project where someone tried to schedule me, I ended up resenting the scheduler more than the work itself.&lt;/p&gt;

&lt;p&gt;The answer is always: "I'll ship it when I can. And yes, I'm working flat out."&lt;/p&gt;

&lt;p&gt;Most of the time, "when I can" is the next business day. There's not much point setting a deadline for that.&lt;/p&gt;

&lt;p&gt;What "when I can" actually looks like is the rest of this article.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a lazy survivor actually does
&lt;/h2&gt;

&lt;p&gt;Here's the daily shape of it. Some of these will sound like discipline. They aren't. They're what laziness produces when you've been at it long enough.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I don't stop until it's done.&lt;/strong&gt; This is where "lazy" gets confusing. When I'm in a problem, stopping is more work than continuing. I sleep and eat enough to not fall over. The work isn't the chore. Pulling myself out of it and back into it is the chore.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I write the spec by being my own user.&lt;/strong&gt; Nobody knows what's easy to use and what's consistent better than the person stress-testing it. That's me.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The 3-line discipline.&lt;/strong&gt; (See &lt;a href="https://dev.to/hidekimori/the-3-line-discipline-3lla"&gt;010&lt;/a&gt;.) By the time the code is written, it's already been tested.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;After release, I watch the logs.&lt;/strong&gt; Access logs, batch logs, error logs — I keep them tailed. A weird line scrolls past and something catches. The alerts haven't fired. I already know.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I redeploy as many times a day as I need to.&lt;/strong&gt; The first release barely matters. What matters is the 10, 15, 20 years of changes that come after. The earlier you ship, the longer that window is.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I throw real data at my own software, hard.&lt;/strong&gt; Big data, malformed data, weird-shaped data. The only confidence I trust is the kind that survives that.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When an internal user overloads my system, I thank them.&lt;/strong&gt; Live data is a gift. I have never had the opposite feeling about a real-world failure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A bug found before anyone else sees it isn't a bug.&lt;/strong&gt; It's just an edit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If there's an update worth making, the next day is too late.&lt;/strong&gt; GPT-5.5 ships → I verify it on my app today, ship it as a selectable option today. I don't want to be the one lagging behind.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Batch processing is what I love most.&lt;/strong&gt; Shaving milliseconds. Cutting load. Watching batch logs scroll. Every part of it is a reward. I'm always hoping more batch jobs come in from users.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And one more — the most important one, the one 24 years actually paid for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discomfort means something is wrong.&lt;/strong&gt; Always. When something feels off, I hunt it down and crush it. The hunch isn't mystical — it's 24 years of pattern recognition without a vocabulary, and it hasn't been wrong yet. The part of me that wants to look past it has always paid for it later.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Where being lazy actually hurts
&lt;/h2&gt;

&lt;p&gt;Not all laziness is helpful. Some of it is just being a person who avoids reading.&lt;/p&gt;

&lt;p&gt;I don't read manuals. My wife has a car with cruise control. I've driven it for years. I still don't know how to turn the cruise control on. Every time, I'm too lazy to figure it out, and I drive without it.&lt;/p&gt;

&lt;p&gt;That instinct does real damage at work. I don't read API docs unless I have to. I don't read change logs carefully. I skim. I assume. I run code instead of finishing a manual.&lt;/p&gt;

&lt;p&gt;English is the other one. I'm Japanese — my technical reading is fine, but the gap between "I can read this" and "I want to read this carefully" is wide, and laziness lives in that gap. Long English documentation is exactly the thing I will not voluntarily face.&lt;/p&gt;

&lt;p&gt;For most of my 24 years, this cost me real time. I'd build something that worked, then discover three weeks later that the API I'd wrapped had a flag I'd missed because I never read past the example block.&lt;/p&gt;

&lt;p&gt;The honest answer is that Claude reads for me now. I describe the problem, Claude reads the docs, Claude points me at the part I need to verify. It doesn't replace judgment. It removes the friction between me and information I was always going to avoid anyway.&lt;/p&gt;

&lt;p&gt;I don't say that to praise AI. I say it because it's true for me, and it would be dishonest to write a piece about how I work in 2026 without saying it out loud.&lt;/p&gt;




&lt;h2&gt;
  
  
  The engine
&lt;/h2&gt;

&lt;p&gt;If you re-read the list above, two things are doing the work. Both are unglamorous.&lt;/p&gt;

&lt;p&gt;The first is the laziness. Each new component means ten years of maintenance I'll have to carry. Each new methodology means bugs I haven't learned yet. Each methodology argument is time not spent writing code. Each promised date is someone else's deadline I'll have to keep. I avoid all of it by default.&lt;/p&gt;

&lt;p&gt;The second is profit. What I build has to make money. Not in some abstract sense — actual revenue from actual users. That part of me has no patience. If a service isn't paying for itself, something is wrong with it, and I want to know what. So I ship early, watch logs, redeploy whenever there's something to fix. Not out of discipline. Out of refusal to leave money on the table.&lt;/p&gt;

&lt;p&gt;These two forces don't agree on much. Laziness says "don't bother." Profit says "if it makes money, bother." What's left after they negotiate is everything I actually do.&lt;/p&gt;

&lt;p&gt;That's why "shipping continuously for 24 years" looks like discipline from the outside. It isn't. It's laziness pushed in a useful direction by the only thing that ever moved me — the need for the result to actually pay.&lt;/p&gt;

&lt;p&gt;A CEO at one of my earlier companies once asked me: "That service running on your desktop — can you make it public right now?" I half-dismissed him at the time. Years later I understood he was the salesperson version of the same engine. That's a story for another time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Other solos
&lt;/h2&gt;

&lt;p&gt;There are other people doing what I do. Solo. Operating something they built years ago that's still running.&lt;/p&gt;

&lt;p&gt;I assume — based on no evidence except how this kind of work shapes a person — that no two of us work the same way. Each of us has wrapped a different cocoon around a different temperament. What I do would be unworkable for them. What they do would be unworkable for me.&lt;/p&gt;

&lt;p&gt;I respect that more than I can put into words. Solo developers who keep their own systems running for years are doing a job that doesn't show up in any career framework I've seen. Most of them never write about it because writing is also work.&lt;/p&gt;

&lt;p&gt;Whatever way you do it — keep going. I see you.&lt;/p&gt;




&lt;h2&gt;
  
  
  What survived
&lt;/h2&gt;

&lt;p&gt;People who watch me work sometimes call it persistence. Conviction. The shape of a long arc.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;I survived because every alternative was more work than continuing. I stayed lazy. I got paid. Together, those two facts ran the clock for 24 years and counting.&lt;/p&gt;

&lt;p&gt;What looks like a disciplined career from the outside has always been, on the inside, the path of least resistance — provided someone was paying me at the end of it.&lt;/p&gt;

&lt;p&gt;The lazy way is the only way I know.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with Claude (Opus).&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Earlier in this series:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-accordion-pattern-why-i-stopped-writing-one-fat-llm-prompt-18mb"&gt;The Accordion Pattern: Why I stopped writing one fat LLM prompt&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/nobody-knows-when-a-job-will-finish-id-still-like-to-report-it-accurately-26nn"&gt;Nobody knows when a job will finish. I'd still like to report it accurately.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/what-survives-when-you-build-alone-for-24-years-4e7d"&gt;What survives when you build alone for 24 years&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/dynamic-isnt-enough-operations-is-the-other-half-2d8f"&gt;Dynamic isn't enough. Operations is the other half.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/live-report-at-this-speed-you-dont-theorize-you-eliminate-1o7h"&gt;Live report: at this speed, you don't theorize. You eliminate.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-loop-i-didnt-notice-closing-16h8"&gt;The loop I didn't notice closing&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/abstractions-are-fine-starting-on-them-isnt-12ff"&gt;Abstractions are fine. Starting on them isn't.&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/twenty-four-years-ten-db-migrations-zero-downtime-633"&gt;Twenty four years, ten DB migrations, zero downtime&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/write-the-code-well-once-the-spec-stops-bothering-you-42g3"&gt;Write the code well once, the spec stops bothering you&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-3-line-discipline-3lla"&gt;The 3-line discipline&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/how-i-removed-the-middleman-one-phone-call-at-a-time-495l"&gt;How I removed the middleman, one phone call at a time&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;a href="https://dev.to/hidekimori/the-graph-nobody-is-watching-4e43"&gt;The graph nobody is watching&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>career</category>
      <category>productivity</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Where vision models stop reading — and start inventing</title>
      <dc:creator>Hideki Mori</dc:creator>
      <pubDate>Wed, 15 Jul 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/hidekimori/where-vision-models-stop-reading-and-start-inventing-5567</link>
      <guid>https://dev.to/hidekimori/where-vision-models-stop-reading-and-start-inventing-5567</guid>
      <description>&lt;p&gt;Earlier this week I published &lt;a href="https://dev.to/hidekimori/when-ai-cant-read-it-invents-but-it-still-sees-the-shape-18ac"&gt;a strange finding&lt;/a&gt;: GPT's low-detail image mode doesn't &lt;em&gt;misread&lt;/em&gt; documents it can't see — it invents them, fluently, with reconciling totals. That was one failure mode, in one model family, at one legibility level.&lt;/p&gt;

&lt;p&gt;It left an uncomfortable question: where exactly does &lt;strong&gt;each&lt;/strong&gt; model stop reading — and what does it do after that? Leave the field blank, or fabricate something plausible?&lt;/p&gt;

&lt;p&gt;One result to hold onto while you scroll: a model that can no longer read a document can usually still tell what kind of document it is. That held across almost the entire board.&lt;/p&gt;

&lt;p&gt;So I built a ladder.&lt;/p&gt;

&lt;p&gt;Then I ran 27 vision model variants down it: 4,158 jobs, about $141 at list price, one afternoon. This post is the map.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup, in one paragraph
&lt;/h2&gt;

&lt;p&gt;One Japanese invoice, rendered on a fixed 2480×3508 canvas (A4 at 300 dpi), then degraded through seven simulated scan resolutions: &lt;strong&gt;300 → 150 → 100 → 70 → 50 → 35 → 25 dpi&lt;/strong&gt; (L0–L6). Degradation is resampling only — no noise, no blur, no rotation — so legibility is the &lt;em&gt;only&lt;/em&gt; variable. The invoice carries twelve fields across four font tiers: a 28 pt title, 16–14 pt "large" fields (total, invoice number), 10.5 pt body fields (counterparty, dates, amounts), and 7.5 pt fine print (bank details). Every value is fictional and unguessable, and &lt;code&gt;subtotal + tax = total&lt;/code&gt; reconciles — so a &lt;em&gt;plausible but wrong&lt;/em&gt; answer is detectable, not just a wrong one. Each variant reads each ladder step five times. The extraction prompt is deliberately neutral: it never says what to do with unreadable text, because that choice is the thing being measured.&lt;/p&gt;

&lt;p&gt;Scoring is deterministic, four classes per field: &lt;strong&gt;correct / near&lt;/strong&gt; (edit distance 1, strings only) &lt;strong&gt;/ blank / fabricated&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The map
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1o634s54bcmdqifs7tz1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1o634s54bcmdqifs7tz1.png" alt="Body-tier fabrication rate, 27 variants × 7 ladder steps" width="800" height="1150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rows are model variants, columns are ladder steps, color is the &lt;strong&gt;fabrication rate on the 10.5 pt body tier&lt;/strong&gt; — the tier where invoice counterparties and amounts live. White means the model either read correctly or stayed silent. Red means it filled unreadable fields with invented values.&lt;/p&gt;

&lt;p&gt;The companion table below gives each variant's &lt;strong&gt;frontier&lt;/strong&gt;: the deepest ladder step where it still keeps ≥90% field accuracy, per tier (× = below 90% already at the crisp 300 dpi original).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;model&lt;/th&gt;
&lt;th&gt;title&lt;/th&gt;
&lt;th&gt;large&lt;/th&gt;
&lt;th&gt;body&lt;/th&gt;
&lt;th&gt;fine&lt;/th&gt;
&lt;th&gt;body fab @25dpi&lt;/th&gt;
&lt;th&gt;classified correctly&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-5.6-sol@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;60%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-5.6-sol@low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;64%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-5.6-terra@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;L3&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-5.6-terra@low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;42%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-5.6-luna@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;96%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-5.6-luna@low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;81/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-5.5@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;86%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-5.5@low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;62%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-5.4@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;openai/gpt-5.4-mini@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;L0&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;azure/gpt-5.6-sol@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L0&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;azure/gpt-5.6-sol@low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;76%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;azure/gpt-5.6-terra@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;azure/gpt-5.6-terra@low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;azure/gpt-5.6-luna@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;azure/gpt-5.6-luna@low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;azure/gpt-5.4@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;98%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;azure/gpt-5.4@low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;58%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;azure/gpt-5.4-mini@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;azure/gpt-5.4-mini@low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;54%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;google/gemini-3.5-flash@high&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;google/gemini-3.5-flash@medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;google/gemini-3.5-flash@low&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L0&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;anthropic/claude-fable-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;anthropic/claude-sonnet-5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;anthropic/claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L6&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;L4&lt;/td&gt;
&lt;td&gt;26%&lt;/td&gt;
&lt;td&gt;84/84&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bedrock/global.amazon.nova-2-lite-v1:0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;L5&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;×&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;63/84&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest surprise in this table isn't where the frontiers sit. It's what happens past them — some models go silent, and some keep talking.&lt;/p&gt;

&lt;p&gt;Six observations fell out of the map.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. "&lt;a class="mentioned-user" href="https://dev.to/low"&gt;@low&lt;/a&gt;" means different things per provider
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;google/gemini-3.5-flash@low&lt;/code&gt; — the second-cheapest variant on the board — read the body tier correctly at &lt;strong&gt;every&lt;/strong&gt; step down to 25 dpi, with &lt;strong&gt;zero&lt;/strong&gt; fabrications. Under exactly the same conditions, every OpenAI and Azure &lt;code&gt;@low&lt;/code&gt; variant collapsed at L0, on the pristine original. Same suffix, opposite behavior. The difference isn't the models' eyesight; it's what each provider's low-detail pipeline does to the image before the model ever sees it.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. For GPT &lt;code&gt;@low&lt;/code&gt;, a worse scan is a better scan
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;@low&lt;/code&gt; accuracy curves are not monotonic. Most GPT &lt;code&gt;@low&lt;/code&gt; variants read a 70 dpi scan &lt;em&gt;better&lt;/em&gt; than the 300 dpi original — body accuracy climbing from ~40% at L0 to 70–80% at L3–L4 before falling again. My resampling acts as an anti-alias filter for the provider's own aggressive downscale. The practical corollary is genuinely odd: if you are stuck with a &lt;code&gt;@low&lt;/code&gt; pipeline, pre-blurring your documents can improve extraction.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. After collapse, models split into fabricators and blankers
&lt;/h2&gt;

&lt;p&gt;What a model does &lt;em&gt;past&lt;/em&gt; its frontier is a personality trait, and it's measurable. At 25 dpi, most GPT &lt;code&gt;@high&lt;/code&gt; variants fill 75–100% of the body fields they can no longer read with invented values. &lt;code&gt;openai/gpt-5.6-terra@high&lt;/code&gt; is the outlier of the entire board: 96% of its failures are blanks. Anthropic and Google models fail less to begin with and fabricate less when they do (0–26%). If your pipeline feeds payment systems, a blanker that admits defeat is worth more than a stronger reader that bluffs.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Same model, different gateway, different eyes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gpt-5.6-sol@high&lt;/code&gt; reads the 7.5 pt fine tier at 100% down to 100 dpi when called via OpenAI — and starts at 92% and degrades immediately when the same model is called via Azure. The failure &lt;em&gt;style&lt;/em&gt; shifts too: &lt;code&gt;terra&lt;/code&gt;'s blank rate drops from 96% (OpenAI) to 39% (Azure). This matches an earlier measurement suggesting the Azure pipeline applies a lower effective-resolution ceiling before the model ever sees the document. Your gateway choice is silently part of your model choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Fabrication doesn't need degradation (teaser)
&lt;/h2&gt;

&lt;p&gt;One fine-print field held a fictional bank whose name is one character away from a real megabank. At 300 dpi — fully legible, five out of five runs — some models "corrected" it to the real one. 48 substitutions across Gemini variants, while a fictional credit union with no real-world neighbor was read perfectly under the same conditions. The trigger isn't legibility; it's the existence of a nearby real entity. This one deserves its own write-up, with the receipts: &lt;a href="https://dev.to/hidekimori/the-model-corrected-reality-fob"&gt;The model corrected reality&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Classification survives reading loss
&lt;/h2&gt;

&lt;p&gt;25 of 27 variants classified all 84 documents (invoice / receipt / business card / meeting minutes) correctly at &lt;strong&gt;every&lt;/strong&gt; degradation step — including variants whose extraction had collapsed completely. A model that cannot read a document can still tell what kind of document it is. The two exceptions are instructive: the cheapest model on the board confuses receipts with invoices (21 out of 21 times — consistently, not randomly), and one &lt;code&gt;@low&lt;/code&gt; variant dropped three classifications at the bottom of the ladder.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd take into production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Route by tier, not by document.&lt;/strong&gt; Titles survive almost anything; fine print dies first. If a field matters, measure the frontier of the tier it lives in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick blankers for payment fields.&lt;/strong&gt; A fabricated bank name passes every visual plausibility check. Prefer models that return &lt;code&gt;""&lt;/code&gt; over models that return something convincing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't assume &lt;code&gt;@low&lt;/code&gt; is one thing.&lt;/strong&gt; Benchmark the variant you'll actually call, on the gateway you'll actually use.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Reproduce it (a free key is enough)
&lt;/h2&gt;

&lt;p&gt;Everything — the deterministic material generator, the runner, the scorer, the raw outputs of all 4,158 jobs — is published:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;github.com/ldxhub-io/examples → &lt;code&gt;analyzedoc/legibility-benchmark/&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The materials are byte-identical on any platform (the generator downloads a pinned, checksum-verified font). A three-variant reproduction subset runs in 147 jobs ≈ 17,600 credits, which fits inside LDX hub's free tier (25,000 credits/month, no card):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 gen_materials.py
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LDXHUB_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...   &lt;span class="c"&gt;# free key: gw.portal.ldxhub.io&lt;/span&gt;
python3 run_benchmark.py &lt;span class="nt"&gt;--models&lt;/span&gt; ume &lt;span class="nt"&gt;--t1-instances&lt;/span&gt; A &lt;span class="nt"&gt;--t1-reps&lt;/span&gt; 3 &lt;span class="nt"&gt;--t2-reps&lt;/span&gt; 1 &lt;span class="nt"&gt;--yes&lt;/span&gt;
python3 score_results.py &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python3 report.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because raw model outputs ship with the results, you can disagree with my scoring rules and re-score everything without re-running a single job.&lt;/p&gt;

&lt;p&gt;Full disclosure: I run LDX hub. It builds no models — it's the harness here, not a subject. One API key across OpenAI, Azure, Google, Anthropic and AWS is the only reason a 27-variant matrix fits in one afternoon, and that convenience is exactly what I'm selling. The measurements stand on the published raw data either way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;Degradation is synthetic resampling, not real scanner noise — claims are limited to simulated legibility. One document type, one language (Japanese; if anything, a harder test than Latin script). The strict scorer counts character-level misreadings as fabrications, which flatters nobody. Results are a July 2026 snapshot; the ladder re-runs on every model addition, so the map will stay current.&lt;/p&gt;

&lt;p&gt;The next time a provider ships a new vision model, it gets a row within a day. That's the point of building a ladder instead of writing a review.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>ocr</category>
      <category>benchmark</category>
    </item>
  </channel>
</rss>
