<?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: Giuseppe Castelluccio</title>
    <description>The latest articles on DEV Community by Giuseppe Castelluccio (@exochard).</description>
    <link>https://dev.to/exochard</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%2F4146467%2Fb7a54595-10a2-48ce-af39-a6d03b6d4fa5.jpg</url>
      <title>DEV Community: Giuseppe Castelluccio</title>
      <link>https://dev.to/exochard</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/exochard"/>
    <language>en</language>
    <item>
      <title>The number is in the paper. It's just one row down.</title>
      <dc:creator>Giuseppe Castelluccio</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:30:36 +0000</pubDate>
      <link>https://dev.to/exochard/the-number-is-in-the-paper-its-just-one-row-down-15ph</link>
      <guid>https://dev.to/exochard/the-number-is-in-the-paper-its-just-one-row-down-15ph</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge, Week 2&lt;/a&gt;. This week's theme is Side Hustle: rowcheck is a pre-submission number check that a freelance manuscript editor can sell, run on one gaming GPU with an open-weight model.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Table 4 of the OLMo paper compares instruction-tuned models. Two rows sit one above the other: &lt;strong&gt;TÜLU 2&lt;/strong&gt;, with an AlpacaEval win rate of 73.9, and &lt;strong&gt;TÜLU 2+DPO&lt;/strong&gt;, with 85.1.&lt;/p&gt;

&lt;p&gt;Now take a sentence like "TÜLU 2 reaches an 85.1% win rate on AlpacaEval." Every part of it is on that page. Search the PDF for "85.1" and you land in the right table, next to the right column, half an inch from the right model name. The claim is still wrong.&lt;/p&gt;

&lt;p&gt;I gave that sentence to a standard retrieval pipeline: split the paper into chunks, fetch the three best, and ask an open model to judge, with room to write its evidence before its verdict. It answered &lt;strong&gt;supported&lt;/strong&gt;, twice out of two, and its own evidence said: "the row for 'TÜLU 2+DPO' shows a value of 85.1". The model saw the right row and still matched the claim to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;rowcheck&lt;/strong&gt; checks a numeric claim against a paper's tables and points to the exact cell it rests on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python -m rowcheck olmo.pdf "TÜLU 2 reaches an 85.1% win rate on AlpacaEval."
CONTRADICTED  TÜLU 2 reaches an 85.1% win rate on AlpacaEval.
              Table 4 (p. 8) reports 73.9, not 85.1%, for TÜLU 2 / AlpacaEval.
              85.1 is TÜLU 2+DPO's AlpacaEval (the next row), Table 4 p. 8.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;--out report.html&lt;/code&gt; every verdict comes with a crop of the page: the row is boxed, the cell the claim should have used is filled, and the number it actually used is outlined.&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%2Fhevn7xyhvnhea25juww3.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%2Fhevn7xyhvnhea25juww3.png" alt="Table 4 of the OLMo paper with the TÜLU 2 row boxed, its AlpacaEval cell 73.9 filled in red, and 85.1 on the TÜLU 2+DPO row outlined in blue" width="596" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It also catches the quieter mistakes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The column.&lt;/strong&gt; "Mistral 7B reaches 52.2% on MATH." Table 2 gives 13.1% for MATH; 52.2% is the GSM8K cell beside it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The setting.&lt;/strong&gt; "Gemma-2 9B scores 68.6 on GSM8K in a 0-shot setting." The number is right, but Table 13 measured it 5-shot. Verdict: contradicted, and it says why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The unit.&lt;/strong&gt; "Stack-Edu has 21.8T Python tokens." The header says billions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What the table can't tell you.&lt;/strong&gt; "Mistral 7B scores 60.1% on MMLU in a 0-shot setting." The number is right, but Table 2 never states the shot count. Verdict: ambiguous, not supported.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who pays for this
&lt;/h2&gt;

&lt;p&gt;Freelance academic editors already sell language editing per manuscript. What they rarely sell is a number check: does every figure in the abstract and discussion match the tables it came from? Doing that by hand means reading each claim against each table, and it is exactly where row and column slips survive peer review.&lt;/p&gt;

&lt;p&gt;rowcheck turns that into an add-on the editor can price per paper. The editor pastes the claims from the text, runs the tool on the PDF, and sends the author an HTML report with every mismatch boxed on the page. Each verdict carries its own evidence, so the editor checks the tool's work in one look before anything goes to the client.&lt;/p&gt;

&lt;p&gt;Two things make the business work, and both come from the model being open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The manuscript never leaves the editor's machine.&lt;/strong&gt; A client's unpublished paper is confidential. A cloud API means a third party sees it; a local model doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost per paper is close to zero.&lt;/strong&gt; Gemma 4 runs on a 16 GB consumer card at about 6 seconds per claim, so a paper with 40 numeric claims takes about four minutes. There is no per-token bill eating the margin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I haven't landed a paying user yet. The next step is to offer it free to two editors for their next manuscripts and ask what they would pay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;iframe src="https://player.vimeo.com/video/1235744320" width="710" height="399"&gt;
&lt;/iframe&gt;
&lt;br&gt;
The report for the six claims above, with every crop: &lt;a href="https://exochard.github.io/rowcheck/" rel="noopener noreferrer"&gt;exochard.github.io/rowcheck&lt;/a&gt;. On a phone, each crop opens scrolled to the cell it checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/exochard/rowcheck" rel="noopener noreferrer"&gt;github.com/exochard/rowcheck&lt;/a&gt; (MIT). &lt;code&gt;./fetch-papers.sh&lt;/code&gt; downloads the ten CC BY papers and checks their hashes; &lt;code&gt;python -m eval.run dev&lt;/code&gt; reruns the development table.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;Three stages. Each one does the job it's good at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Tables as positioned words, not text.&lt;/strong&gt; PDF tables in papers are mostly borderless, with stacked headers and two-column pages. Text extraction flattens them into a run of numbers. rowcheck keeps every word with its box on the page. It groups the words into visual lines and finds each table from its caption. Then it works out the columns from where the numbers line up across rows, and hangs the header words over each column. A row comes out as &lt;code&gt;Mistral 7B Pretrained | MMLU=60.1% | HellaSwag=81.3% | …&lt;/code&gt;. When it can't label a row one-to-one, it passes the raw line on. A wrong label is worse than none.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. An open-weight model reads the table.&lt;/strong&gt; Gemma 4 (26B-A4B, Q4) runs locally through llama.cpp on a 16 GB card. It gets the claim and the three best-matching tables. Under a JSON schema, it names the table, the row, the column and the cell, plus the status of every setting the claim states. I run it with thinking off, about 6 s per claim; with thinking on it spent its whole token budget recounting columns and returned nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Code decides the numbers.&lt;/strong&gt; The model's reading is a proposal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the cited cell must be printed on that exact line;&lt;/li&gt;
&lt;li&gt;the claimed value is compared to it with a tolerance;&lt;/li&gt;
&lt;li&gt;the unit is checked against the column header;&lt;/li&gt;
&lt;li&gt;an explicit shot count in the claim is checked against the row, the header and the caption;&lt;/li&gt;
&lt;li&gt;if the model picks "TÜLU 2+DPO" for a claim about "TÜLU 2", the row whose name matches exactly wins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model never gets the last word on a number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it work?
&lt;/h3&gt;

&lt;p&gt;Before running anything, I wrote 30 claims from six CC BY papers (Mistral 7B, OLMo, OLMo 2, Phi-3, Gemma 2, SmolLM2) and labelled each one from the page. I also froze a split: four papers for development, two held back. Then I added six trap claims that swap a row, a unit, a denominator or a setting.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;rowcheck&lt;/th&gt;
&lt;th&gt;chunk retrieval + same model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Development claims (20)&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Held-out claims (10)&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trap claims marked "supported" (6)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The comparison uses the same Gemma model and lets it write its evidence before its verdict. An earlier version of my baseline asked for the verdict first, and that made it look worse than it is. Overall accuracy is close, and I won't pretend otherwise: the plain pipeline is good on most of these claims. The difference is what you can do with the answer: rowcheck gives you the page, the row and the cell, so checking its work takes one look instead of a re-read. And on the held-out papers it made one mistake I care about. It called a 10-shot TruthfulQA number "0-shot" supported. Shot counts are now checked in code. That fix came after the held-out run, so it isn't in these numbers.&lt;/p&gt;

&lt;p&gt;Then I tried it on four papers it had never seen: Gemma, Mixtral, Nemotron-4 and Qwen, with 22 new claims labelled before any run, nine of them traps. It broke. It got 9 of 13 claims right where plain retrieval got 12, and it called four of the nine traps "supported". The failures were all about reading the table, not the model's judgement: a header like "Gemma" printed once above "2B 7B", a family name like "LLaMA" printed once beside four size rows, an asterisk that was a different Unicode character. I fixed the reader, not the prompts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Four unseen papers: 22 claims, 9 of them traps&lt;/th&gt;
&lt;th&gt;rowcheck&lt;/th&gt;
&lt;th&gt;chunk retrieval + same model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Honest claims right (of 13), first run&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traps marked "supported" (of 9), first run&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Honest claims right (of 13), after fixing the table reader&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traps marked "supported" (of 9), after the fixes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The "after" rows are on the papers I fixed against, so read them as development numbers. On the two original held-out papers the same fixes take rowcheck from 7 to 8 of 10.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limits
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tables drawn as images, and rotated tables, are out of reach.&lt;/li&gt;
&lt;li&gt;When two tables carry the same row, it can pick the appendix copy that lacks the unit.&lt;/li&gt;
&lt;li&gt;Ten papers and 58 claims make a small test, and the first contact with new papers went badly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Open Innovation Matters
&lt;/h2&gt;

&lt;p&gt;The papers here are open (CC BY), and the model reading them is an open-weight model running on my own GPU. Checking someone's numbers shouldn't require sending their paper, or your draft, to a service.&lt;/p&gt;

&lt;p&gt;For a side business the open model is the whole margin. A closed API would bill every claim and see every client manuscript. Here the weights sit on my disk, the tables never leave the machine, and when a new paper layout broke the reader I fixed the code that reads tables instead of waiting on a provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;Best Use of Gemma: Gemma 4 26B-A4B does the table reading, served locally by llama.cpp on a 16 GB card. It is a mixture-of-experts model with about 4B parameters active per token, which is what brings a claim down to about six seconds on consumer hardware. It answers under a JSON schema with thinking off, and code checks every number it cites before a verdict goes out.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>An Italian copy reviewer that cites its style guide, built on Sanity Context</title>
      <dc:creator>Giuseppe Castelluccio</dc:creator>
      <pubDate>Wed, 30 Sep 2026 08:55:41 +0000</pubDate>
      <link>https://dev.to/exochard/an-italian-copy-reviewer-that-cites-its-style-guide-built-on-sanity-context-3h7p</link>
      <guid>https://dev.to/exochard/an-italian-copy-reviewer-that-cites-its-style-guide-built-on-sanity-context-3h7p</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path One: Ship an Agent That Queries Real Content&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I'm Italian, and part of my work is checking the Italian copy of software websites. The same mistakes keep shipping: "Fidato da" for "Trusted by", "Utensileria" (a hardware store) as the menu label for "Tools", "5 Mld+ Altoparlanti nativi" (native loudspeakers) for "5B+ native speakers". A general model catches some of these and misses others, and when it does catch one it cannot tell you which house rule it applied.&lt;/p&gt;

&lt;p&gt;So I built a reviewer agent. You give it a string, where it appears and, if you have it, the English source. It reads the team's Italian style guide from a Sanity Knowledge Base, answers &lt;code&gt;ok&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt;, proposes the corrected text, and lists the Knowledge Base entries it relied on. The style guide is structured content in Sanity: 21 rules and 24 glossary terms. Fourteen rules carry real strings that shipped on live product pages next to their native correction; six are house decisions the team made.&lt;/p&gt;

&lt;p&gt;I also measured it, and I report every run. On the 12 held-out generic pairs the Knowledge Base never saw, the Knowledge Base and the model alone are within one pair of each other: with the same code filter behind both arms, the latest run gives 11 against 10 pairs fully right, and the Sep 30 run rescored the same way gives 9 against 10. A general model already knows most of what makes Italian copy wrong, so a style guide cannot add much there. The Knowledge Base earns its place on rules a model cannot know: house decisions that differ from generic Italian. On a separate slice of 16 pairs built only from such rules, the agent with the Knowledge Base gets 10 of 16 pairs fully right and the same model without it gets none. The details are below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fexochard%2Fitalian-copy-reviewer%2Fmain%2Fdemo%2Fsanity-demo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fexochard%2Fitalian-copy-reviewer%2Fmain%2Fdemo%2Fsanity-demo.gif" alt="Three reviews in the local web UI, each citing the Knowledge Base entries it used" width="720" height="1013"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three real reviews in the local web UI, grounded in the Knowledge Base. Each took 12 to 17 seconds on the free Gemini tier; the GIF cuts the wait, and every card shows the real time.&lt;/p&gt;

&lt;p&gt;The same three reviews as a video: &lt;a href="https://vimeo.com/1231260014" rel="noopener noreferrer"&gt;https://vimeo.com/1231260014&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A review looks like this (real output from the Sep 30 test run, &lt;code&gt;gemini-3.1-flash-lite&lt;/code&gt;, input &lt;em&gt;Fidato da oltre 100.000 imprese&lt;/em&gt;):&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;"verdict"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The phrase 'Fidato da' is a literal calque of 'Trusted by' and is considered unnatural in Italian marketing copy."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"evidence"&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="s2"&gt;"Fidato da"&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;"corrected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Scelto da oltre 100.000 imprese"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rules"&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="s2"&gt;"Calques, False Friends &amp;amp; Idiom Errors/&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Trusted by&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; calque (major)"&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;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/exochard/italian-copy-reviewer" rel="noopener noreferrer"&gt;https://github.com/exochard/italian-copy-reviewer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;TypeScript on Node 22 with the Vercel AI SDK; a Sanity Studio schema; a small Python script that turns the rules into Sanity documents. &lt;code&gt;npm test&lt;/code&gt; runs without network.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The content model.&lt;/strong&gt; Two document types in the Studio schema:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;l10nRule&lt;/code&gt;: title, slug, category (calque, agreement, number format…), severity, the rule in plain words, preferred forms, forms to avoid, and an array of &lt;em&gt;real shipped examples&lt;/em&gt; (product type, where the text appears, English source, shipped Italian, native correction).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;glossaryTerm&lt;/code&gt;: the English term, the preferred Italian, the forms to avoid, a note.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The examples are the point. "Don't translate literally" is useless to a model; "a software company shipped &lt;em&gt;Fidato da oltre 9,500 marchi e organizzazioni globali&lt;/em&gt; and a native reviewer changed it to &lt;em&gt;Scelto da oltre 9.500 marchi e organizzazioni in tutto il mondo&lt;/em&gt;" is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Knowledge Base.&lt;/strong&gt; Two sources: the production dataset above, and Microsoft's public Italian Localization Style Guide (a PDF). They sometimes disagree. When it built the Knowledge Base, Sanity Context flagged three such conflicts as Issues instead of merging them silently; I read each one and resolved it in the dashboard before the final runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The agent.&lt;/strong&gt; The AI SDK connects to the Context MCP endpoint in knowledge-base mode. The system prompt tells the model to call &lt;code&gt;initial_context&lt;/code&gt; first and then &lt;code&gt;knowledge_base_read&lt;/code&gt; on the entries that could apply. The output is a typed object (verdict, reason, the wrong fragments quoted from the text, corrected text, cited entry paths), so every answer says which rules it used.&lt;/p&gt;

&lt;p&gt;The first measured run taught me something. With the Knowledge Base the agent flagged correct text more often than the model alone, because it applied entries whose trigger was not there: it flagged "AI" in a sentence with no "AI" in it, and asked for "oltre" on a number that had no "+". So every flag now has to quote the wrong words, and code drops a flag whose quote is not in the text. The prompt also says an entry applies only when the text contains what the entry is about. I tuned this on the training pairs only.&lt;/p&gt;

&lt;p&gt;That was not enough. The second test run showed flags whose quote was the whole string: the agent read the capitalisation entry and called &lt;em&gt;Automazione batch con un clic&lt;/em&gt; and &lt;em&gt;RECENSIONI DEGLI UTENTI&lt;/em&gt; "Title Case". So a flag that relies only on the Title Case, "+" or AI entry now survives only when the text holds that entry's trigger: a capitalised word in mid-sentence (ALL CAPS never counts), a plus sign, or "AI" and "IA" mixed in one string, which is exactly what the AI entry says. Re-applied offline to the saved training outputs, this took the grounded agent from 12/17 to 16/17 without losing a caught bug. I first applied this filter to the grounded arm only, which is not a fair comparison: re-applied offline to the saved Sep 30 test run, the same filter moves the model-alone arm from 9/12 to 10/12. So the like-for-like numbers below put both arms behind the same filter (&lt;code&gt;--shared-filter&lt;/code&gt; in &lt;code&gt;src/eval.ts&lt;/code&gt;), and I also show the arms with no filter at all.&lt;/p&gt;

&lt;p&gt;One fix came from reading a test failure, and I'd rather say so. On &lt;em&gt;USER REVIEW&lt;/em&gt; both arms read the text between the prompt's &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt; and &lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt; markers as an empty placeholder. The prompt now says the text between the markers is the literal string.&lt;/p&gt;

&lt;p&gt;In every grounded run on Oct 1, 24 of 24 on the test pairs and 32 of 32 on the house pairs, the agent read at least one entry before answering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keeping the test honest.&lt;/strong&gt; The benchmark has 29 pairs. The train half feeds the Knowledge Base; the test half never enters it, and a unit test asserts the two halves are disjoint. Each pair is scored only when the agent flags the shipped string &lt;strong&gt;and&lt;/strong&gt; leaves the native fix alone, so a reviewer that flags everything scores zero.&lt;/p&gt;

&lt;p&gt;Every test run, same 12 pairs, &lt;code&gt;gemini-3.1-flash-lite&lt;/code&gt;, pairs fully right. These are the original runs, where only the Knowledge Base arm had the filter; the like-for-like table follows:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Test run&lt;/th&gt;
&lt;th&gt;Changed before it (tuned on training pairs)&lt;/th&gt;
&lt;th&gt;Model alone&lt;/th&gt;
&lt;th&gt;+ Sanity Knowledge Base&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sep 28&lt;/td&gt;
&lt;td&gt;first version&lt;/td&gt;
&lt;td&gt;7/12&lt;/td&gt;
&lt;td&gt;4/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 29&lt;/td&gt;
&lt;td&gt;quote check; "entry applies only when triggered" prompt; two entries reworded&lt;/td&gt;
&lt;td&gt;9/12&lt;/td&gt;
&lt;td&gt;5/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sep 30&lt;/td&gt;
&lt;td&gt;per-rule trigger check; literal-text prompt&lt;/td&gt;
&lt;td&gt;9/12&lt;/td&gt;
&lt;td&gt;9/12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Sep 30 run in full:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Bugs caught&lt;/th&gt;
&lt;th&gt;Fixes left alone&lt;/th&gt;
&lt;th&gt;Pairs fully right&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gemini-3.1-flash-lite alone&lt;/td&gt;
&lt;td&gt;11/12&lt;/td&gt;
&lt;td&gt;10/12&lt;/td&gt;
&lt;td&gt;9/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemini-3.1-flash-lite + Sanity Knowledge Base&lt;/td&gt;
&lt;td&gt;10/12&lt;/td&gt;
&lt;td&gt;11/12&lt;/td&gt;
&lt;td&gt;9/12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Like for like, the Sep 30 run rescored offline with &lt;code&gt;src/rescore.ts&lt;/code&gt;, pairs fully right:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;No filter&lt;/th&gt;
&lt;th&gt;Same filter on both arms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Model alone&lt;/td&gt;
&lt;td&gt;9/12&lt;/td&gt;
&lt;td&gt;10/12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ Sanity Knowledge Base&lt;/td&gt;
&lt;td&gt;9/12&lt;/td&gt;
&lt;td&gt;9/12&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A fresh run on Oct 1 with the shared filter on, after the Knowledge Base was rebuilt with the house rules, gave 11/12 for the Knowledge Base and 10/12 for the model alone.&lt;/p&gt;

&lt;p&gt;The model alone moves between runs too: both arms share the prompt, and the model is not fully repeatable even at temperature 0. Twelve pairs is a small test, so one pair is eight points.&lt;/p&gt;

&lt;p&gt;What still fails in the Sep 30 run: both arms passed &lt;em&gt;Transcrizione&lt;/em&gt;, which should be &lt;em&gt;Trascrizione&lt;/em&gt;, so neither checks spelling letter by letter. With the Knowledge Base the agent called the correct &lt;em&gt;Ottieni informazioni utili e pratiche&lt;/em&gt; a calque of "Get", and let an English string, &lt;em&gt;Larger file per task&lt;/em&gt;, through as correct. The model alone failed on two correct strings instead: it carried the "+" of the English source into &lt;em&gt;oltre 100 siti&lt;/em&gt; and called it redundant, and it objected to the preposition in &lt;em&gt;per attività&lt;/em&gt;. What the Knowledge Base does well is the explanation: every flag names the entry it came from, so a reviewer can see why it fired and fix the entry instead of the prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the Knowledge Base cannot be replaced.&lt;/strong&gt; A keyword search over a style guide would not beat a model on generic Italian, and on that test it does not. So I added a held-out slice of 16 pairs (&lt;code&gt;house&lt;/code&gt; in &lt;code&gt;content/split.json&lt;/code&gt;, pairs in &lt;code&gt;content/house.jsonl&lt;/code&gt;) whose verdict depends on six rules and five glossary terms the team chose and that exist only in the Knowledge Base: always "tu" and never "Lei", error messages that open with "Non siamo riusciti a", no exclamation marks in product copy, "persona" instead of "utente", no "per favore", the euro sign after the amount, "spazio di lavoro" for "workspace", "pagamento" for "checkout", plan names kept in English as "Free" and "Pro". Both halves of every pair are correct Italian in general; only the shipped one breaks a house rule. For example: &lt;em&gt;Acceda al suo account per continuare&lt;/em&gt; against &lt;em&gt;Accedi al tuo account per continuare&lt;/em&gt;, &lt;em&gt;Invita un utente nel progetto&lt;/em&gt; against &lt;em&gt;Invita una persona nel progetto&lt;/em&gt;, &lt;em&gt;Da €29 al mese&lt;/em&gt; against &lt;em&gt;Da 29 € al mese&lt;/em&gt;. No prompt, filter or rule was tuned on this slice, and none of its strings is in the seed (a unit test checks both the split and the seed). Pairs fully right, both arms behind the same filter: Knowledge Base 10/16, model alone 0/16. Neither arm flagged a single correct string. The six the agent missed: &lt;em&gt;checkout&lt;/em&gt;, &lt;em&gt;e-mail&lt;/em&gt; and two uses of &lt;em&gt;utente&lt;/em&gt; went through although the glossary covers them, and so did an error opening with &lt;em&gt;Errore:&lt;/em&gt; and an exclamation mark. I wrote these pairs myself to test the claim, so they say what the Knowledge Base can do when the rules are in it, not how often real copy breaks house rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;p&gt;Project ID: &lt;code&gt;3ezm3tm1&lt;/code&gt; (dataset &lt;code&gt;production&lt;/code&gt;). The schema is in &lt;code&gt;studio/schemaTypes/&lt;/code&gt; and the seed documents in &lt;code&gt;content/seed.ndjson&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The dataset is public, so anyone can query it without a token, for example the six house rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-G&lt;/span&gt; &lt;span class="s2"&gt;"https://3ezm3tm1.api.sanity.io/v2025-02-19/data/query/production"&lt;/span&gt; &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'query=*[_type=="l10nRule" &amp;amp;&amp;amp; category=="house_style"]{title, rule, prefer, avoid}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How this was made
&lt;/h2&gt;

&lt;p&gt;I wrote the benchmark strings and their corrections from my own QA audits; they predate the challenge, and the brand names are masked. The code and the Knowledge Base content were written during the challenge with an AI coding assistant (Claude Code): I set the spec and the acceptance checks, it drafted the code, and I reviewed, ran and tested every part. The numbers above come from &lt;code&gt;npm run eval&lt;/code&gt; runs (&lt;code&gt;results/eval-*.json&lt;/code&gt;) and are reported as measured, including the ones that went against the Knowledge Base. &lt;code&gt;src/rescore.ts&lt;/code&gt; re-applies the flag checks to a saved run without calling any model and prints the arms with and without the filter.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI reviewers catch Italian UI bugs, then flag the good fixes too</title>
      <dc:creator>Giuseppe Castelluccio</dc:creator>
      <pubDate>Mon, 28 Sep 2026 13:29:15 +0000</pubDate>
      <link>https://dev.to/exochard/would-you-ship-this-italian-a-benchmark-built-from-real-website-bugs-15nm</link>
      <guid>https://dev.to/exochard/would-you-ship-this-italian-a-benchmark-built-from-real-website-bugs-15nm</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Benchmarked
&lt;/h2&gt;

&lt;p&gt;Companies ship Italian that no Italian would write. In my own QA audits of software websites this year I found a transcription tool whose "Tools" menu said &lt;em&gt;Utensileria&lt;/em&gt; (a hardware store), a homepage bragging about &lt;em&gt;5 Mld+ Altoparlanti nativi&lt;/em&gt; (five billion native loudspeakers), and more than one &lt;em&gt;Fidato da&lt;/em&gt;, a word-for-word "Trusted by" that reads as machine output.&lt;/p&gt;

&lt;p&gt;Models are now the reviewers many teams rely on, so I wanted to know: shown a string on its own, the way a reviewer sees it, can a model tell a shipped defect from its native fix?&lt;/p&gt;

&lt;p&gt;The benchmark has 29 real pairs: the string as it shipped, and the version a native speaker would ship. Brand names are masked. The model sees each half separately, with the product type, where the text appears and the English source when there is one, and answers &lt;code&gt;ok&lt;/code&gt; or &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The scoring is the part I care about. A pair counts only when the model flags the shipped string &lt;strong&gt;and&lt;/strong&gt; leaves the fix alone. A model that flags everything scores zero, and so does one that approves everything. Reviewers that cry wolf are useless in practice, and a plain accuracy number would hide them.&lt;/p&gt;

&lt;p&gt;The defects cover ten types: 8 untranslated leftovers, 5 calques, 4 agreement errors, 3 number formats, 2 each of wrong word sense, misspelling and broken grammar, and one each of a broken encoding, an unidiomatic phrase and English-style capitalisation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;p&gt;I ran the task on the 18 models Kaggle Benchmarks offered me, from Google, Anthropic, OpenAI, xAI, Alibaba, DeepSeek, Zhipu and the open Gemma and gpt-oss families, big and small, because a localisation team picks a reviewer by cost as much as by quality.&lt;/p&gt;

&lt;p&gt;Not all of them produced answers. In the first runs, with all models in parallel, seven returned API errors on most calls. I re-ran those seven one at a time on Oct 1. Five then answered cleanly, while three open models still hit rate limits and one was not found, so I report those four as not measured rather than as zero. A run counts only when at most 2 of its 58 answers were unreadable. That leaves 14 models and 21 clean runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;p&gt;Pairs fully right, bugs caught and fixes left alone, out of 29. Where a model has two clean runs, both are shown.&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;Pairs fully right&lt;/th&gt;
&lt;th&gt;Bugs caught&lt;/th&gt;
&lt;th&gt;Fixes left alone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.8 Flash&lt;/td&gt;
&lt;td&gt;26 · 25&lt;/td&gt;
&lt;td&gt;27 · 26&lt;/td&gt;
&lt;td&gt;28 · 27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.7 Flash&lt;/td&gt;
&lt;td&gt;26 · 23&lt;/td&gt;
&lt;td&gt;27 · 27&lt;/td&gt;
&lt;td&gt;28 · 25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.5 Flash-Lite&lt;/td&gt;
&lt;td&gt;26 · 25&lt;/td&gt;
&lt;td&gt;26 · 27&lt;/td&gt;
&lt;td&gt;29 · 27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;23 · 24&lt;/td&gt;
&lt;td&gt;25 · 27&lt;/td&gt;
&lt;td&gt;26 · 26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.1 Pro (preview)&lt;/td&gt;
&lt;td&gt;23 · 24&lt;/td&gt;
&lt;td&gt;27 · 27&lt;/td&gt;
&lt;td&gt;25 · 26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.5&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;29&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 2.5 Flash&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.4 mini&lt;/td&gt;
&lt;td&gt;19 · 19&lt;/td&gt;
&lt;td&gt;24 · 24&lt;/td&gt;
&lt;td&gt;23 · 23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.4 nano&lt;/td&gt;
&lt;td&gt;18 · 16&lt;/td&gt;
&lt;td&gt;26 · 26&lt;/td&gt;
&lt;td&gt;20 · 19&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fec4zdt72obgj5pwracgk.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%2Fec4zdt72obgj5pwracgk.png" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Catching the bug is the easy half.&lt;/strong&gt; Every clean model caught 24 or more of the 29 shipped defects. What separated them was the other half: leaving a correct native string alone. GPT-5.4 nano catches as many bugs as the leaders and then flags a third of the native fixes too. GPT-5.5 is the extreme case: the only model to catch all 29 bugs, and one that flagged 7 of the 29 native fixes on the way. A reviewer like that sends a translator to "fix" good Italian, which is worse than no reviewer.&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%2Fp0m3omye6ju98lx2id1r.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%2Fp0m3omye6ju98lx2id1r.png" alt=" " width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Numbers are the blind spot.&lt;/strong&gt; Across the 21 clean runs, models caught every untranslated leftover and broken encoding, 103 of 105 calques and 83 of 84 agreement errors. They caught the three number-format errors only 25 times in 63, and only GPT-5.5, the model that flags everything, caught all three. All three are the English "+" glued to a number: &lt;em&gt;6+ piattaforme&lt;/em&gt;, &lt;em&gt;15.000+ computer&lt;/em&gt;, and &lt;em&gt;oltre 100+ siti&lt;/em&gt;, which says "more than" twice. Italian writes &lt;em&gt;oltre 6&lt;/em&gt;. The strings look like numbers, so models wave them through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The small model tied the big ones.&lt;/strong&gt; Gemini 3.5 Flash-Lite, the cheapest Gemini on the list, left all 29 native fixes alone in one run and tied the top score. Gemini 3.1 Pro did not beat the Flash models, and Claude Opus 5 scored two pairs above Haiku 4.5, inside the noise below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One run is not a result.&lt;/strong&gt; The same model moved by up to three pairs between runs (Gemini 3.7 Flash scored 26 and then 23; my first notebook run gave 25). On a 29-pair benchmark, a one- or two-pair gap between models is noise; I would not rank models on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it changed about how I think
&lt;/h2&gt;

&lt;p&gt;I used to judge an AI reviewer by what it catches. After this I judge it by what it leaves alone. Recall is cheap: the weakest clean model still caught 24 of 29 bugs. The cost sits in the false alarms, because each one sends a translator off to defend a correct string.&lt;/p&gt;

&lt;p&gt;I also stopped trusting "give the model the style guide" as the fix. I tested that on a smaller follow-up for another DEV challenge: the same 29 pairs split so the 12 test pairs never reached the guide, a different model (Gemini 3.1 Flash-Lite), and a real Italian style guide served to it as a knowledge base. On its first run the style guide made it worse, 4 of 12 held-out pairs right against 7 without it, because the model applied rules whose trigger was not in the string. It took two rounds of fixes to bring the grounded version level with the plain one, 9 of 12 each. In that test, the rules raised false alarms long before they lowered anything.&lt;/p&gt;

&lt;p&gt;What I'd measure next: more pairs per defect type, especially numbers and idiom, so each type gets its own reliable score, and whether a rule written for exactly the "+" pattern closes the number blind spot without new false alarms.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;The benchmark and its leaderboard are public on Kaggle: &lt;a href="https://www.kaggle.com/benchmarks/giuseppecastelluccio/italian-ui-strings-shipped-vs-fixed" rel="noopener noreferrer"&gt;https://www.kaggle.com/benchmarks/giuseppecastelluccio/italian-ui-strings-shipped-vs-fixed&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The task behind it, with its notebook and every model's run: &lt;a href="https://www.kaggle.com/benchmarks/tasks/giuseppecastelluccio/italian-ui-strings-shipped-vs-fixed" rel="noopener noreferrer"&gt;https://www.kaggle.com/benchmarks/tasks/giuseppecastelluccio/italian-ui-strings-shipped-vs-fixed&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How this was made
&lt;/h2&gt;

&lt;p&gt;The strings and their corrections come from my QA audits, made before the challenge; I checked every correction again as a native speaker for this benchmark. The task code was written during the challenge with an AI coding assistant (Claude Code): I set the scoring rule and the checks, it drafted the code, and I reviewed and ran it. The two charts were drawn from the same run results with a short matplotlib script. A stub model that always answers right scores 1.0 and one that flags everything scores 0.0.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
