<?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: Butnar Daniel</title>
    <description>The latest articles on DEV Community by Butnar Daniel (@danielbutnar).</description>
    <link>https://dev.to/danielbutnar</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%2F4149954%2F4f77fa52-1943-42e3-b62d-de9364a76346.jpg</url>
      <title>DEV Community: Butnar Daniel</title>
      <link>https://dev.to/danielbutnar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danielbutnar"/>
    <language>en</language>
    <item>
      <title>Braşov or Brașov? Give 43 LLMs the wrong letter, and a third of their answers keep it</title>
      <dc:creator>Butnar Daniel</dc:creator>
      <pubDate>Thu, 01 Oct 2026 16:05:58 +0000</pubDate>
      <link>https://dev.to/danielbutnar/brasov-or-brasov-15-llms-spell-my-city-right-until-you-spell-it-wrong-first-75a</link>
      <guid>https://dev.to/danielbutnar/brasov-or-brasov-15-llms-spell-my-city-right-until-you-spell-it-wrong-first-75a</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;p&gt;Brașov, the city I live in, is spelled with ș, an s with a comma below. The 1990s stand-in, ş, has a cedilla instead, and to a spell-checker or an exact-match lookup, Brașov and Braşov are different words. I asked Claude Opus 5.5 the same question about one Romanian news paragraph, sent four different ways. Three answers are spelled right. Which one isn't?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A.&lt;/strong&gt; Festivalul a avut loc la Sighișoara, iar biletele pentru ediția de anul viitor se vor pune în vânzare în decembrie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B.&lt;/strong&gt; Festivalul a avut loc la Sighişoara, iar biletele pentru ediţia de anul viitor se vor pune în vânzare în decembrie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C.&lt;/strong&gt; Festivalul a avut loc la Sighișoara, iar biletele pentru ediția de anul viitor se vor pune în vânzare în decembrie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;D.&lt;/strong&gt; Festivalul a avut loc la Sighișoara, iar biletele pentru ediția de anul viitor se vor pune în vânzare în decembrie.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;/p&gt;
  The answer
  &lt;p&gt;&lt;strong&gt;B.&lt;/strong&gt; &lt;em&gt;Sighişoara&lt;/em&gt; and &lt;em&gt;ediţia&lt;/em&gt; carry the cedilla, because the paragraph I gave it did. A, C and D are the same characters. The question was "Where did the festival take place, and when do next year's tickets go on sale?" (In Sighișoara; in December.)&lt;/p&gt;

&lt;p&gt;Couldn't spot it? In my first human test, 10 Romanians did no better than a coin flip, partly because my page drew the two letters alike on some screens. &lt;a href="https://danielbutnar.github.io/comma-below/human/test.html?from=dev" rel="noopener noreferrer"&gt;Try the fixed two-minute test&lt;/a&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;th&gt;My paragraph was typed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;correctly (ș ț)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;with the old cedilla letters (ş ţ)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;without any diacritics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;with the cedilla letters, plus one sentence in the system prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Models know the right letter. Hand them text typed with the old one and a third of their answers keep it. Most of those keep every one, and when they add a word of their own, it gets the cedilla too, as if they had switched to my keyboard. I call it the &lt;strong&gt;keyboard echo&lt;/strong&gt;. It's one Romanian letter, but I suspect the lesson isn't Romanian: the text you hand a model, retrieval chunks included, is also a style sample, and familiar errors in it can come back in the answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A third of the answers keep my wrong letter.&lt;/strong&gt; Typed correctly, 99% of answers came back clean; without diacritics, 96%; with the cedilla letters, 68%. Every one of the 43 models did it at least once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model's own words get it too.&lt;/strong&gt; And only the cedilla survives: a dot below and old mojibake (text garbled by the wrong encoding) get repaired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Newer can be worse.&lt;/strong&gt; Claude Opus 4.8 → 5 went from 95% to 56% clean and Sonnet 4.6 → 5 from 95% to 58%, and the drops repeated in a fresh run. Others moved up, but within noise, so test each one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One sentence in the system prompt fixes most of it, and breaks Turkish&lt;/strong&gt;, where the cedilla is right. A few lines of code fixed every Romanian answer and changed 5 of 112 Turkish replies.&lt;/li&gt;
&lt;/ul&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%2F26vz456sw76ijmy6a04y.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%2F26vz456sw76ijmy6a04y.png" alt="Typed with the cedilla, a third of the answers keep it. 43 models, the same 22 Romanian prompts. Share of clean answers (with ș or ț, and no cedilla or look-alike letter): typed correctly 99%, without diacritics 96%, with the cedilla letters 68%, with the cedilla letters plus one system-prompt sentence 95%. Typed correctly, gpt-oss-120b and gpt-oss-20b kept 82% clean; typed with the cedilla, the best were Claude Opus 4.8, Claude Sonnet 4.6 and GPT-5.6 Sol at 95%, the lowest gpt-oss-120b at 32%." width="800" height="765"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Romanian has two letters with a comma underneath, &lt;strong&gt;ș&lt;/strong&gt; and &lt;strong&gt;ț&lt;/strong&gt; (U+0219, U+021B). The 1990s encodings didn't have them, so people typed the Turkish &lt;strong&gt;ş&lt;/strong&gt; and a &lt;strong&gt;ţ&lt;/strong&gt; with a cedilla (U+015F, U+0163). Unicode added the right letters in 1999, yet 29 of the 35 Romanian front pages I fetched on 30 September still had cedilla letters, the government's site among them. On a page marked as Romanian, fonts like Roboto and Segoe UI even draw ş with a comma, so the people who publish the wrong letter may never see it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;"Brașov" == "Braşov"&lt;/code&gt; is &lt;code&gt;false&lt;/code&gt;, and the cedilla spelling is all over the text models learn from and that retrieval hands them. So I asked: do models write the right letter, and what do they do when the text they read has the wrong one?&lt;/p&gt;

&lt;p&gt;The main test has 22 prompts: 6 guest messages to a Brașov guesthouse's booking assistant, 6 news paragraphs to summarize, and 10 questions about those texts (the quiz is one). Each went out four ways, like the quiz: correct, without diacritics, with cedillas, and with cedillas plus a one-sentence rule in the system prompt. An answer is &lt;strong&gt;clean&lt;/strong&gt; when it has at least one ș or ț and no cedilla or look-alike letter. A script, not an LLM, checks that by code point, and before any real run, fake models had to score as designed: one that copies its input 0%, one that fixes everything 100%. Twelve more tasks on &lt;a href="https://www.kaggle.com/benchmarks/danielbutnar/romanian-comma-below" rel="noopener noreferrer"&gt;Kaggle Benchmarks&lt;/a&gt; probe the edges; for tasks 8 to 13 I wrote my predictions into each notebook before its first run (see &lt;a href="https://www.kaggle.com/benchmarks/tasks/danielbutnar/romanian-comma-below-13-other-wrong-letters" rel="noopener noreferrer"&gt;task 13&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  The 13 tasks
  &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;Task&lt;/th&gt;
&lt;th&gt;What it asks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Write&lt;/td&gt;
&lt;td&gt;English prompt, plain ASCII names: does the Romanian answer use ș/ț?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Echo&lt;/td&gt;
&lt;td&gt;Reply, summarize or answer from text typed correctly or with cedillas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Fix&lt;/td&gt;
&lt;td&gt;"Proofread this" vs "correct the diacritics" vs an explicit instruction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;See&lt;/td&gt;
&lt;td&gt;Same characters? Any cedilla here? Which code point?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Restore&lt;/td&gt;
&lt;td&gt;Diacritics where only context decides (&lt;em&gt;peste&lt;/em&gt; / &lt;em&gt;pește&lt;/em&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;Write &lt;code&gt;fix_romanian(text)&lt;/code&gt;; 14 hidden tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Echo + rule&lt;/td&gt;
&lt;td&gt;Echo's cedilla inputs plus one system-prompt sentence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Echo, four inputs&lt;/td&gt;
&lt;td&gt;The main test: correct, cedilla, no diacritics, cedilla + rule; every model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Contagion&lt;/td&gt;
&lt;td&gt;Four documents and a question about one; the target clean or typed with cedillas, and 0, 1 or 3 of the others typed with cedillas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Turkish guests&lt;/td&gt;
&lt;td&gt;Turkish, where ş is correct: no rule, the full rule, the rule scoped to Romanian&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Other look-alikes&lt;/td&gt;
&lt;td&gt;English with Cyrillic look-alikes or mojibake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Turns&lt;/td&gt;
&lt;td&gt;Does a cedilla first text change the second summary?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Other wrong letters&lt;/td&gt;
&lt;td&gt;Task 8's summaries and document questions with a dot below (ṣ) or old mojibake (º) instead of the cedilla&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;p&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;43 models in the main test, 44 in all.&lt;/strong&gt; Round one (30 September – 1 October) ran 15 models on tasks 1–7. Round two (8–10 October) ran the main test on the 43 models Kaggle served that answered (GPT-6 Astra's runs failed), and tasks 9–13 on 10 to 28 models each. The lineup lets me compare versions of one model on identical prompts, and reasoning on and off. The fresh run I check findings against repeats 16 of the main test's prompts on 28 models (task 13). Everything ran on Kaggle's model proxy with default settings and an 8,192-token cap, for $30.19 of the free quota.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Full model list
  &lt;p&gt;&lt;strong&gt;Anthropic:&lt;/strong&gt; Claude Haiku 4.5, Claude Opus 4.5, Claude Opus 4.6, Claude Opus 4.7, Claude Opus 4.8, Claude Opus 5, Claude Opus 5.5, Claude Sonnet 4.5, Claude Sonnet 4.6, Claude Sonnet 5, Claude Sonnet 5.5. &lt;strong&gt;Google:&lt;/strong&gt; Gemini 2.5 Flash, Gemini 3 Flash, Gemini 3.5 Flash, Gemini 3.6 Flash, Gemini 3.7 Flash, Gemini 3.8 Flash, Gemini 3.1 Flash-Lite, Gemini 3.5 Flash-Lite, Gemini 2.5 Pro, Gemini 3.1 Pro, Gemma 4 26B A4B, Gemma 4 31B. &lt;strong&gt;OpenAI:&lt;/strong&gt; GPT-5.4, GPT-5.5, GPT-6 Astra, GPT-5.6 Luna, GPT-6 Luna, GPT-5.6 Sol, GPT-6 Sol, GPT-6.1 Sol, GPT-5.6 Terra, GPT-5.4 nano, GPT-5.4 mini, gpt-oss-20b, gpt-oss-120b. &lt;strong&gt;xAI:&lt;/strong&gt; Grok 4.20 Non-Reasoning, Grok 4.20 Reasoning. &lt;strong&gt;DeepSeek:&lt;/strong&gt; DeepSeek-R1. &lt;strong&gt;Alibaba:&lt;/strong&gt; Qwen 3 235B, Qwen 3 Coder 480B, Qwen 3 Next 80B Instruct, Qwen 3 Next 80B Thinking. &lt;strong&gt;Zhipu:&lt;/strong&gt; GLM-5.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

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

&lt;h3&gt;
  
  
  1. Models restore missing diacritics but keep wrong ones
&lt;/h3&gt;

&lt;p&gt;Many Romanians type without diacritics ("Brasov"), and the models put them back (96% clean, against 68% for cedilla input).&lt;/p&gt;

&lt;p&gt;They can tell. The 15 first-round models wrote ș and ț correctly from English prompts (98%), spotted the cedilla in every sentence typed entirely with it, and fixed 99% of texts when told to correct the diacritics. They just don't fix it unprompted, or mention it: none of the 3,160 answers to cedilla text did (my prompts asked for short answers).&lt;/p&gt;

&lt;p&gt;A few models add cedillas on their own, mainly gpt-oss-120b, gpt-oss-20b and Claude Haiku 4.5 ("Șoferii sunt &lt;strong&gt;rugaţi&lt;/strong&gt;…", from gpt-oss); for the rest, the cedilla comes from my text.&lt;/p&gt;

&lt;p&gt;DeepSeek-R1 shows its reasoning, so I could check whether it notices. Without the rule, its reasoning mentioned the letters in 3 of 88 answers; with it, in 43 of 44, mostly repeating the rule, and 6 of those still came out with cedillas. In one, it planned for "proper Romanian orthography with ș/ț commas below, not cedillas" and answered "&lt;strong&gt;Autorităţile&lt;/strong&gt; recomandă &lt;strong&gt;turiştilor&lt;/strong&gt; să verifice starea drumului înainte de plecare." Both words carry the cedilla. Reasoning didn't help Grok 4.20 either: 41% clean with it, 45% without.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The keyboard echo: the model's own words get my cedilla too
&lt;/h3&gt;

&lt;p&gt;Answers to questions about a document came back clean 52% of the time, summaries 68%, guest replies 96%: the more of my words an answer reuses, the more cedillas it keeps. And it's mostly all or nothing: 75% of the answers that kept any of my cedillas kept all of them.&lt;/p&gt;

&lt;p&gt;What surprised me were the model's own words. They can't be copied, so their spelling shows which keyboard the model is typing on. Claude Opus 5 summarized a city-hall notice that asked drivers (&lt;em&gt;rugați&lt;/em&gt;, "asked") to park elsewhere:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Şoferii sunt &lt;strong&gt;îndrumaţi&lt;/strong&gt; să parcheze pe strada Mureşenilor sau lângă Gara Braşov.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Îndrumaţi&lt;/em&gt; ("directed") isn't in my text. The model chose it, and typed it on my keyboard. Seven other models chose the same word, cedilla and all. Across all the echo answers, when an answer kept all my cedillas, 95% of the ș/ț words it added had the cedilla; when it kept some, 9.5%. Those partial answers tended to keep the cedilla in place names like Braşov and Sighişoara, the way you'd quote a name, and wrote their own words with the comma. So there are two behaviors: quoting names, and switching keyboards.&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%2Fex6w2eovaluvruht3qda.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%2Fex6w2eovaluvruht3qda.png" alt="When an answer kept all my cedillas, 95% of the new ș/ț words it added had the cedilla too (86 of 91); when it kept some of them, 9.5% (9 of 95); when it kept none, 0.5%. A new word is one not in my text and not an inflection of a word in it. Tasks 2 and 8." width="800" height="548"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The gap held within single models and again on the fresh run, as I'd predicted: 13 of 15 new words against 1 of 18.&lt;/p&gt;

&lt;p&gt;Could keeping my spelling just be faithful quoting? Not for a word the model chose itself, and the same models silently turn Brasov into Brașov. Even if I excuse every place name copied from my text as a quotation, only 73% of answers are clean.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The documents around it turn the echo up or down
&lt;/h3&gt;

&lt;p&gt;I hid each question's document among three others and typed 0, 1 or 3 of them with cedillas. Around a clean document, the neighbors hardly mattered: at least 95% of answers stayed clean. Around a cedilla document, three clean neighbors pulled answers up to &lt;strong&gt;78% clean&lt;/strong&gt; and three cedilla ones down to &lt;strong&gt;36%&lt;/strong&gt;; 14 of the 15 models moved that way, none the other.&lt;/p&gt;

&lt;p&gt;In a chat, a first message typed with cedillas hardly mattered: only 2 of 60 clean second summaries picked up one, both from Claude Haiku 4.5.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Only the cedilla sticks: a dot below and mojibake get repaired
&lt;/h3&gt;

&lt;p&gt;Why the cedilla? Either any near-twin of the right letter slips through, or only the familiar wrong one does. A dot below (Braṣov) tells them apart: it looks just as close, but no Romanian text uses it. Before a fresh run on 28 models I wrote down that it would be repaired, along with mojibake (Braºov), which looks broken.&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%2Fs54muqbymhr8skxc38st.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%2Fs54muqbymhr8skxc38st.png" alt="Of three wrong spellings, only the cedilla survives. 28 models, the same 16 summaries and document questions. Of the ș and ț letters in words the answers reused from my text, 38% came back with the cedilla (Braşov), 0.9% with a dot below (Braṣov) and 1.4% as old mojibake (Braºov)." width="800" height="591"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;27 of the 28 models kept the dot less often than the cedilla, and none kept it more. So it isn't just a near-twin slipping through.&lt;/p&gt;

&lt;p&gt;Familiarity fits: the cedilla fills real Romanian web text, and ş is a real Turkish letter. English shows the same pattern, though I'd predicted the opposite: in answers to questions about an English document, models kept the mojibake common on the web (ZÃ¼rich) in 41%, rare Cyrillic look-alikes in 9%. Both sat in names there, so part of that may be quoting. I didn't measure how common each spelling is, so this is a hypothesis.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Newer can be worse: Opus 4.8 kept 95% clean, Opus 5 56%
&lt;/h3&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%2Fggmztybl45818r3lny8d.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%2Fggmztybl45818r3lny8d.png" alt="Of 19 upgrades on the same prompts, 2 copied my cedillas far more often, and none far less: Claude Opus 4.8 95% to Opus 5 56%, Claude Sonnet 4.6 95% to Sonnet 5 58%. The other changes, among them GPT Sol 5.6 to 6 (95% to 64%) and GPT Luna 5.6 to 6 (64% to 89%), don't survive a correction for all 19 comparisons." width="800" height="983"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I compared 19 upgrades on identical prompts. The three cleanest models all got worse in their next version: Opus 4.8 → 5 (95% → 56%), Sonnet 4.6 → 5 (95% → 58%), GPT-5.6 Sol → 6 (95% → 64%). Only the two Claude drops are too big to be chance, even allowing for 19 comparisons; they come from the same generation, so probably one cause (Opus 5.5 recovered part of it, Sonnet 5.5 didn't).&lt;/p&gt;

&lt;p&gt;When you pick the best three of 43, some will slip on a rerun by chance alone, so I checked the fresh run: all three drops came back, GPT Sol's included, each by 30 points or more, while a model's rate moved a median of 9 points between identical runs. So test every upgrade, and run it more than once.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. One sentence stops the echo, and breaks Turkish
&lt;/h3&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%2Ffuf5spjmgbuj7k3e7azv.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%2Ffuf5spjmgbuj7k3e7azv.png" alt="A Romanian-only swap in code cleans every Romanian answer and changes 5 of 112 Turkish replies. Romanian answers clean and Turkish replies still correct: no fix 53% and 100%; one sentence in the system prompt 93% and 71%; the same sentence for Romanian only 94% and 95%; swapping every ş for ș in code 100% and 0%; swapping only outside Turkish-looking sentences 100% and 96%." width="800" height="857"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One sentence in the system prompt lifted clean answers on cedilla input from 68% to 95% in the main test, and from 53% to 93% on the fresh run's summaries and questions in the chart: &lt;em&gt;"Use correct Romanian orthography: write ș and ț with a comma below, never ş or ţ with a cedilla, even if the input uses them."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But in Turkish, ş is correct. Brașov gets Turkish guests too, so I sent the booking assistant eight messages in Turkish. Without the rule, every reply was correct; with it, 71%. Some spelled Turkish with the Romanian ș, some switched to Romanian, some avoided ş altogether. Gemini 3.7 Flash offered a honeymoon couple a "&lt;strong&gt;muhteșem&lt;/strong&gt; dağ manzaralı" room (a magnificent mountain view) and a "&lt;strong&gt;Șarap&lt;/strong&gt; tadımı" (wine tasting). Adding "this applies to Romanian only" brought Turkish back to 95%. (A script sorted the replies by language and I checked every flagged word; no Turkish speaker did.)&lt;/p&gt;

&lt;p&gt;Code does better. Swapping every ş for ș broke all 112 correct Turkish replies. Swapping only in sentences without ı, ğ, ç, ö or ü fixed all 352 broken Romanian answers and changed just 5 Turkish replies, 4 of them short greetings like "Hoş geldiniz". One caveat: I wrote that rule looking at these same replies, so expect it to do a little worse on new text.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Got Wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My predictions.&lt;/strong&gt; 2 of 20 failed and 6 were too close to call. Most had no threshold until I scored them, and I count a result within 5 points of its threshold, or a correlation that could be chance, as too close to call; the one test with thresholds written first (dot below and mojibake) went 5 for 5. The table is in the block below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My sample.&lt;/strong&gt; Everything rests on short paragraphs I wrote about one city, and 35 of the 43 models ran the main test once, so one model's rate carries an interval about 36 points wide. Trust the patterns more than any one rank. "A third" also depends on my mix of prompts: answers from a document kept the cedilla nearly half the time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;/p&gt;
  Two smaller mistakes, and every prediction scored
  &lt;p&gt;&lt;strong&gt;My upload.&lt;/strong&gt; The Kaggle CLI reads task files with the system encoding, cp1250 on my laptop, so my first task file would have uploaded "Brașov" as "BraČ™ov". The task files are now pure ASCII and check a hash of their data before running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My first fix.&lt;/strong&gt; The &lt;code&gt;fix_romanian()&lt;/code&gt; in the first version of this post broke Turkish, the same way the one-sentence rule does.&lt;/p&gt;

&lt;p&gt;Four of the verdicts too close to call rest on evidence that could be chance: the rank correlations in tasks 9 and 11 (permutation p = 0.20 and 0.11), task 8's upgrades, which went both ways before the correction, and task 12's 2 of 60 chat conversations, both from Claude Haiku 4.5.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Prediction&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Correct input stays near 100% for every model.&lt;/td&gt;
&lt;td&gt;lowest: gpt-oss-120b and gpt-oss-20b, 82%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;failed&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Cedilla input fails far more often than input without diacritics.&lt;/td&gt;
&lt;td&gt;96% vs 68% clean answers&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Newer versions in a family copy the cedilla at least as often as older ones.&lt;/td&gt;
&lt;td&gt;of 19 upgrades, 2 copied more and none copied less after a correction for multiple tests (3 and 1 at p &amp;lt; 0.05 before it); no overall trend (rank correlation -0.29)&lt;/td&gt;
&lt;td&gt;inconclusive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;The one-sentence rule lifts cedilla input to at least 90% for most models.&lt;/td&gt;
&lt;td&gt;36 of 43 models at 90% or more&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;With a clean target, answers stay clean for most models even when the other three documents are cedilla.&lt;/td&gt;
&lt;td&gt;13 of 15 models at 90% or more&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;The models that rewrote whole answers in cedilla in task 2 are the ones whose clean-target answers get infected.&lt;/td&gt;
&lt;td&gt;rank correlation +0.38 over 15 models; only Claude Opus 5 and Claude Sonnet 5 had infected answers&lt;/td&gt;
&lt;td&gt;inconclusive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;A cedilla target is copied less when the other documents are clean.&lt;/td&gt;
&lt;td&gt;78% vs 36% clean answers&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Without a rule, Turkish replies keep ş.&lt;/td&gt;
&lt;td&gt;100% correct&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Task 7's rule makes some models write Turkish with the Romanian comma letter.&lt;/td&gt;
&lt;td&gt;6 of 14 models, 15 replies&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;The scoped rule keeps Turkish intact and still fixes Romanian.&lt;/td&gt;
&lt;td&gt;Turkish 94.6% correct (bar set when scoring: 95%; no rule: 100%, full rule: 71%), Romanian 99%&lt;/td&gt;
&lt;td&gt;inconclusive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Correct English input stays near 100%.&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Cyrillic look-alikes are copied far more often than mojibake.&lt;/td&gt;
&lt;td&gt;clean answers: 95% with look-alikes in the names, 80% with mojibake&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;failed&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;The models that copy the Romanian cedilla most also copy the look-alikes most.&lt;/td&gt;
&lt;td&gt;rank correlation +0.55 over 10 models, resting on the 6 of 120 answers that kept a look-alike (4 models)&lt;/td&gt;
&lt;td&gt;inconclusive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;A cedilla first text infects some clean second summaries, mostly after a cedilla first summary.&lt;/td&gt;
&lt;td&gt;3% infected (2 of 60 conversations, both Claude Haiku 4.5); 11% when the first summary was itself in cedilla; the chats with no cedilla at all had 1 of 60 too (Claude Haiku 4.5)&lt;/td&gt;
&lt;td&gt;inconclusive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;A clean first text does not protect a cedilla second text much.&lt;/td&gt;
&lt;td&gt;83% clean after a clean first text, 68% on its own (the same models' task 2 summaries)&lt;/td&gt;
&lt;td&gt;inconclusive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;The dot below is kept in a smaller share of s/t letters than the cedilla (paired, 95% interval below zero).&lt;/td&gt;
&lt;td&gt;0.9% vs 38.1% of letters kept (-37 points, -45 to -29)&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;The old mojibake is also kept less often than the cedilla.&lt;/td&gt;
&lt;td&gt;1.4% vs 38.1% (-37 points, -44 to -29)&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Repaired dot-below and mojibake letters become comma-below letters in at least 90% of cases.&lt;/td&gt;
&lt;td&gt;98% comma, 1.6% cedilla&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;Both rules give at least 90% clean answers, and the scoped rule is within 5 points of the full rule.&lt;/td&gt;
&lt;td&gt;scoped 94%, full 93%&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;In cedilla answers that keep every cedilla, at least 80% of the model's own new s/t words have the cedilla; when they keep only some, at most 30%.&lt;/td&gt;
&lt;td&gt;13 of 15 vs 1 of 18 new words&lt;/td&gt;
&lt;td&gt;held&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed in How I Think About These Models
&lt;/h2&gt;

&lt;p&gt;I went in expecting a knowledge test, and the models passed it. What I hadn't thought about: the text I hand a model is also a style sample, encoding included. The model answers in that style, most closely when it summarizes or answers from my text, which is what retrieval does all day. Treat retrieved text as both content and a style sample: normalize it before the model sees it, and check the answer after.&lt;/p&gt;

&lt;p&gt;So I fix it in code, before the text reaches the model and again after, and only where the text is Romanian:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="n"&gt;CEDILLA_TO_COMMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maketrans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;şŞţŢ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;șȘțȚ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;T_ONLY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maketrans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ţŢ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;țȚ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Turkish has no ţ, so it is always wrong
&lt;/span&gt;&lt;span class="n"&gt;NOT_ROMANIAN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ıİğĞçÇöÖüÜ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# letters Turkish uses and Romanian words don't
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fix_romanian&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NFC&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "s" + combining cedilla -&amp;gt; "ş"
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[^.!?\n]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;T_ONLY&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;NOT_ROMANIAN&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;CEDILLA_TO_COMMA&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has two blind spots: a short Turkish sentence without ı, ğ, ç, ö or ü, like "Hoş geldiniz", and a Romanian sentence quoting a name like Zürich.&lt;/p&gt;

&lt;p&gt;Does your model do it? Send it the prompt in step 1 below and paste its answer: every cedilla letter turns red. (No box? &lt;a href="https://danielbutnar.github.io/comma-below/post/checker.html" rel="noopener noreferrer"&gt;Open the checker on its own page&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;iframe width="100%" height="760px" src="https://livecodes.io/?mode=result&amp;amp;title=Cedilla%20checker&amp;amp;html=%3C%21doctype+html%3E%0A%3Chtml+lang%3D%22en%22%3E%0A%3Chead%3E%0A%3Cmeta+charset%3D%22utf-8%22%3E%0A%3Cmeta+name%3D%22viewport%22+content%3D%22width%3Ddevice-width%2C+initial-scale%3D1%22%3E%0A%3Ctitle%3ECedilla+checker%3C%2Ftitle%3E%0A%3Cstyle%3E%0A%3Aroot%7B--bg%3A%23fcfcfb%3B--ink%3A%231c1a17%3B--muted%3A%235f5a52%3B--line%3A%238f897e%3B--bad%3A%23b2452f%3B--badbg%3A%23fbe3dc%3B--ok%3A%231f6b58%3B--btn%3A%231c1a17%3B--btnink%3A%23fff%3B--box%3A%23fff%7D%0A%40media+%28prefers-color-scheme%3Adark%29%7B%3Aroot%7B--bg%3A%23171615%3B--ink%3A%23eeebe5%3B--muted%3A%23aaa49a%3B--line%3A%2377726a%3B--bad%3A%23ff8d74%3B--badbg%3A%234a2219%3B--ok%3A%236fd1b3%3B--btn%3A%23eeebe5%3B--btnink%3A%23171615%3B--box%3A%23201f1d%7D%7D%0A%2A%7Bbox-sizing%3Aborder-box%3Bmargin%3A0%7D%0A%5Bhidden%5D%7Bdisplay%3Anone%21important%7D%0Abody%7Bbackground%3Avar%28--bg%29%3Bcolor%3Avar%28--ink%29%3Bfont%3A15px%2F1.5+system-ui%2C-apple-system%2C%22Segoe+UI%22%2CRoboto%2Csans-serif%3Bpadding%3A14px+16px%7D%0Ah1%7Bfont-size%3A17px%3Bfont-weight%3A650%7D%0Ap.sub%7Bcolor%3Avar%28--muted%29%3Bfont-size%3A13.5px%3Bmargin%3A2px+0+12px%7D%0A.step%7Bfont-size%3A13px%3Bfont-weight%3A650%3Bletter-spacing%3A.04em%3Btext-transform%3Auppercase%3Bcolor%3Avar%28--muted%29%3Bmargin%3A10px+0+4px%7D%0Apre%2Ctextarea%2C.out%7Bwidth%3A100%25%3Bborder%3A1px+solid+var%28--line%29%3Bborder-radius%3A8px%3Bbackground%3Avar%28--box%29%3Bcolor%3Avar%28--ink%29%3Bfont%3A14px%2F1.5+system-ui%2C-apple-system%2C%22Segoe+UI%22%2CRoboto%2Csans-serif%3Bpadding%3A8px+10px%7D%0Apre%7Bwhite-space%3Apre-wrap%3Bmax-height%3A96px%3Boverflow%3Aauto%7D%0Atextarea%7Bmin-height%3A76px%3Bresize%3Avertical%7D%0A.row%7Bdisplay%3Aflex%3Bflex-wrap%3Awrap%3Bgap%3A8px%3Bmargin-top%3A6px%3Balign-items%3Acenter%7D%0Abutton%7Bfont%3A600+13.5px%2F1+system-ui%2C-apple-system%2C%22Segoe+UI%22%2Csans-serif%3Bpadding%3A8px+12px%3Bborder-radius%3A7px%3Bborder%3A1px+solid+var%28--btn%29%3Bbackground%3Avar%28--btn%29%3Bcolor%3Avar%28--btnink%29%3Bcursor%3Apointer%7D%0Abutton.ghost%7Bbackground%3Atransparent%3Bcolor%3Avar%28--ink%29%7D%0Abutton%3Afocus-visible%2Ctextarea%3Afocus-visible%2Cpre%3Afocus-visible%2C.out%3Afocus-visible%7Boutline%3A3px+solid+%232a78d6%3Boutline-offset%3A2px%7D%0A.verdict%7Bfont-weight%3A650%3Bmargin-top%3A8px%7D%0A.verdict.bad%7Bcolor%3Avar%28--bad%29%7D+.verdict.ok%7Bcolor%3Avar%28--ok%29%7D%0A.out%7Bmargin-top%3A6px%3Bmin-height%3A40px%3Bmax-height%3A150px%3Boverflow%3Aauto%7D%0Amark%7Bbackground%3Avar%28--badbg%29%3Bcolor%3Avar%28--bad%29%3Bfont-weight%3A700%3Bborder-bottom%3A2px+solid+var%28--bad%29%3Bborder-radius%3A2px%3Bpadding%3A0+1px%7D%0A.okl%7Bborder-bottom%3A2px+solid+var%28--ok%29%7D%0A.tr%7Bborder-bottom%3A2px+dotted+var%28--muted%29%7D%0A.note%7Bcolor%3Avar%28--muted%29%3Bfont-size%3A12.5px%3Bmargin-top%3A4px%7D%0A.hint%7Bcolor%3Avar%28--muted%29%3Bfont-size%3A12.5px%7D%0A.sr%7Bposition%3Aabsolute%3Bwidth%3A1px%3Bheight%3A1px%3Boverflow%3Ahidden%3Bclip%3Arect%280+0+0+0%29%3Bwhite-space%3Anowrap%7D%0A%40media+%28max-width%3A480px%29%7Bbody%7Bpadding%3A12px%7Dpre%7Bmax-height%3A72px%7D.out%7Bmax-height%3A120px%7Dbutton%7Bpadding%3A8px+9px%3Bfont-size%3A13px%7D%7D%0A%3C%2Fstyle%3E%0A%3C%2Fhead%3E%0A%3Cbody%3E%0A%3Ch1%3EDoes+your+model+copy+the+cedilla%3F%3C%2Fh1%3E%0A%3Cp+class%3D%22sub%22%3ETwo+steps.+The+check+runs+in+your+browser.+Like+my+scorer%2C+it+reads+each+letter+by+its+code+point+after+NFC.+Unlike+it%2C+it+looks+only+at+%26%23x15F%3B+%26%23x163%3B+%26%23x219%3B+%26%23x21B%3B%2C+and+it+skips+%26%23x15F%3B+in+sentences+that+look+Turkish.%3C%2Fp%3E%0A%3Ch2+class%3D%22step%22+id%3D%22s1%22%3E1+%26%23xB7%3B+Send+this+to+your+chatbot%3C%2Fh2%3E%0A%3Cpre+id%3D%22prompt%22+tabindex%3D%220%22+aria-labelledby%3D%22s1%22%3E%3C%2Fpre%3E%0A%3Cdiv+class%3D%22row%22%3E%3Cbutton+id%3D%22copyPrompt%22%3ECopy+the+prompt%3C%2Fbutton%3E%3Cspan+class%3D%22hint%22+id%3D%22copyMsg%22+aria-live%3D%22polite%22%3E%3C%2Fspan%3E%3C%2Fdiv%3E%0A%3Ch2+class%3D%22step%22+id%3D%22s2%22%3E2+%26%23xB7%3B+Paste+its+answer+here%3C%2Fh2%3E%0A%3Cp+class%3D%22note%22+id%3D%22tnote%22+style%3D%22margin%3A0+0+4px%22%3EOr+any+Romanian+text%3A+a+web+page%2C+a+database+export%2C+a+translation.%3C%2Fp%3E%0A%3Ctextarea+id%3D%22t%22+spellcheck%3D%22false%22+placeholder%3D%22Paste+here%22+aria-labelledby%3D%22s2%22+aria-describedby%3D%22tnote%22%3E%3C%2Ftextarea%3E%0A%3Cdiv+class%3D%22row%22%3E%3Cbutton+class%3D%22ghost%22+id%3D%22sample%22%3EShow+Claude+Opus+5.5%27s+real+answer%3C%2Fbutton%3E%3Cbutton+class%3D%22ghost%22+id%3D%22clear%22%3EClear%3C%2Fbutton%3E%3C%2Fdiv%3E%0A%3Cdiv+class%3D%22verdict%22+id%3D%22verdict%22%3E%3C%2Fdiv%3E%0A%3Cdiv+class%3D%22out%22+id%3D%22out%22+role%3D%22region%22+tabindex%3D%220%22+aria-label%3D%22Your+text%2C+with+the+s+and+t+letters+marked%22+hidden%3E%3C%2Fdiv%3E%0A%3Cp+class%3D%22note%22+id%3D%22legend%22+hidden%3E%3C%2Fp%3E%0A%3Cdiv+class%3D%22row%22+id%3D%22fixRow%22+hidden%3E%3Cbutton+id%3D%22fix%22%3EFix+and+copy+%28Romanian+only%29%3C%2Fbutton%3E%3Cbutton+class%3D%22ghost%22+id%3D%22undo%22+hidden%3EUndo%3C%2Fbutton%3E%3Cspan+class%3D%22hint%22+id%3D%22fixMsg%22%3E%3C%2Fspan%3E%3C%2Fdiv%3E%0A%3Cp+class%3D%22note%22+id%3D%22fixNote%22+hidden%3EThe+fix+swaps+%26%23x15F%3B+%26%23x163%3B+for+%26%23x219%3B+%26%23x21B%3B+sentence+by+sentence.+In+a+sentence+with+%26%23x131%3B%2C+%26%23x11F%3B%2C+%26%23xE7%3B%2C+%26%23xF6%3B+or+%26%23xFC%3B+it+leaves+%26%23x15F%3B+alone%2C+so+most+Turkish+keeps+its+%26%23x15F%3B%2C+but+a+short+Turkish+sentence+without+those+letters%2C+like+%22Ho%26%23x15F%3B+geldiniz%21%22%2C+still+gets+changed.+%26%23x163%3B+is+always+swapped%3A+Turkish+has+no+%26%23x163%3B.+It%27s+the+%3Ccode%3Efix_romanian%28%29%3C%2Fcode%3E+from+the+post.%3C%2Fp%3E%0A%3Cdiv+class%3D%22sr%22+id%3D%22live%22+aria-live%3D%22polite%22%3E%3C%2Fdiv%3E%0A%3Cscript%3E%0Avar+FIX+%3D+%7B%22%5Cu015F%22%3A%22%5Cu0219%22%2C%22%5Cu015E%22%3A%22%5Cu0218%22%2C%22%5Cu0163%22%3A%22%5Cu021B%22%2C%22%5Cu0162%22%3A%22%5Cu021A%22%7D%3B%0Avar+COMMA+%3D+%2F%5B%5Cu0219%5Cu0218%5Cu021B%5Cu021A%5D%2F%2C+S+%3D+%2F%5B%5Cu015F%5Cu015E%5Cu0219%5Cu0218%5D%2F%3B%0Avar+NOT_ROMANIAN+%3D+%2F%5B%5Cu0131%5Cu0130%5Cu011F%5Cu011E%5Cu00E7%5Cu00C7%5Cu00F6%5Cu00D6%5Cu00FC%5Cu00DC%5D%2F%3B%0Afunction+fixRomanian%28text%29%7B%0Areturn+text.normalize%28%22NFC%22%29.replace%28%2F%5B%5E.%21%3F%5Cn%5D%2B%2Fg%2C+function%28sentence%29%7B%0Areturn+sentence.replace%28NOT_ROMANIAN.test%28sentence%29+%3F+%2F%5B%5Cu0163%5Cu0162%5D%2Fg+%3A+%2F%5B%5Cu015F%5Cu015E%5Cu0163%5Cu0162%5D%2Fg%2C+function%28c%29%7Breturn+FIX%5Bc%5D%3B%7D%29%3B%0A%7D%29%3B%0A%7D%0Afunction+toCedilla%28s%29%7Breturn+s.replace%28%2F%5Cu0219%2Fg%2C%22%5Cu015F%22%29.replace%28%2F%5Cu0218%2Fg%2C%22%5Cu015E%22%29.replace%28%2F%5Cu021B%2Fg%2C%22%5Cu0163%22%29.replace%28%2F%5Cu021A%2Fg%2C%22%5Cu0162%22%29%3B%7D%0Afunction+cp%28c%29%7Breturn+%22U%2B0%22+%2B+c.charCodeAt%280%29.toString%2816%29.toUpperCase%28%29%3B%7D%0Avar+TEXT+%3D+%22Drumul+Transf%5Cu0103g%5Cu0103r%5Cu0103%5Cu0219an+se+%5Cu00EEnchide+%5Cu00EEn+fiecare+an+la+sf%5Cu00E2r%5Cu0219itul+lunii+octombrie%2C+iar+redeschiderea+are+loc%2C+de+obicei%2C+la+%5Cu00EEnceputul+lui+iulie.+Autorit%5Cu0103%5Cu021Bile+recomand%5Cu0103+turi%5Cu0219tilor+s%5Cu0103+verifice+starea+drumului+%5Cu00EEnainte+de+plecare%2C+deoarece+ninsorile+pot+ap%5Cu0103rea+%5Cu0219i+%5Cu00EEn+septembrie.%22%3B%0Avar+PROMPT+%3D+%22Answer+the+question+using+only+the+text+below.+Answer+in+Romanian%2C+in+one+sentence.%5Cn%5CnText%3A%5Cn%22+%2B+toCedilla%28TEXT%29+%2B+%22%5Cn%5CnQuestion%3A+When+does+the+road+usually+close%2C+and+when+does+it+reopen%3F%22%3B%0Avar+SAMPLE+%3D+%22Drumul+Transf%5Cu0103g%5Cu0103r%5Cu0103%5Cu015Fan+se+%5Cu00EEnchide+%5Cu00EEn+fiecare+an+la+sf%5Cu00E2r%5Cu015Fitul+lunii+octombrie+%5Cu015Fi+se+redeschide%2C+de+obicei%2C+la+%5Cu00EEnceputul+lunii+iulie.%22%3B%0Avar+%24+%3D+function%28id%29%7Breturn+document.getElementById%28id%29%3B%7D%3B%0A%24%28%22prompt%22%29.textContent+%3D+PROMPT%3B%0Avar+keys+%3D+matchMedia%28%22%28pointer%3Acoarse%29%22%29.matches+%3F+%22press+and+hold%2C+then+tap+Copy.%22+%3A+%22press+Ctrl%2BC+%28%5Cu2318C+on+a+Mac%29.%22%3B%0Avar+timer%3B%0Afunction+announce%28text%2C+now%29%7B+clearTimeout%28timer%29%3B+timer+%3D+setTimeout%28function%28%29%7B+%24%28%22live%22%29.textContent+%3D+text%3B+%7D%2C+now+%3F+0+%3A+700%29%3B+%7D%0Afunction+clip%28text%29%7B+return+new+Promise%28function%28done%29%7B+done%28navigator.clipboard.writeText%28text%29%29%3B+%7D%29%3B+%7D%0Afunction+copyText%28text%2C+msg%2C+box%29%7B%0Aclip%28text%29.then%28function%28%29%7B%0Amsg.textContent+%3D+%22Copied.%22%3B+clearTimeout%28msg.timer%29%3B+msg.timer+%3D+setTimeout%28function%28%29%7B+msg.textContent+%3D+%22%22%3B+%7D%2C+3000%29%3B%0A%7D%29.catch%28function%28%29%7B%0AclearTimeout%28msg.timer%29%3B%0Abox.focus%28%29%3B%0Aif+%28box.select%29+box.select%28%29%3B+else+getSelection%28%29.selectAllChildren%28box%29%3B%0Amsg.textContent+%3D+%22Selected%3A+%22+%2B+keys%3B%0A%7D%29%3B%0A%7D%0A%24%28%22copyPrompt%22%29.onclick+%3D+function%28%29%7B+copyText%28PROMPT%2C+%24%28%22copyMsg%22%29%2C+%24%28%22prompt%22%29%29%3B+%7D%3B%0Avar+before+%3D+null%3B%0Afunction+count%28k%2C+one%2C+many%29%7B+return+k+%2B+%28k+%3D%3D%3D+1+%3F+one+%3A+many%29%3B+%7D%0Afunction+check%28now%29%7B%0Avar+raw+%3D+%24%28%22t%22%29.value%2C+s+%3D+raw.normalize%28%22NFC%22%29%2C+out+%3D+%24%28%22out%22%29%2C+v+%3D+%24%28%22verdict%22%29%3B%0Avar+n+%3D+%7Bmark%3A+0%2C+okl%3A+0%2C+skip%3A+0%2C+tr%3A+0%7D%2C+sent+%3D+%7Bskip%3A+0%2C+tr%3A+0%7D%2C+seen+%3D+%7Bmark%3A+%7B%7D%2C+okl%3A+%7B%7D%2C+tr%3A+%7B%7D%7D%2C+buf+%3D+%22%22%3B%0Aout.textContent+%3D+%22%22%3B%0Av.className+%3D+%22verdict%22%3B%0Aif+%28%21s.trim%28%29%29%7B+v.textContent+%3D+%24%28%22legend%22%29.textContent+%3D+%22%22%3B+out.hidden+%3D+%24%28%22legend%22%29.hidden+%3D+true%3B+showFix%28false%29%3B+announce%28%22%22%2C+now%29%3B+return%3B+%7D%0Afor+%28var+part+of+s.match%28%2F%5B%5E.%21%3F%5Cn%5D%2B%7C%5B.%21%3F%5Cn%5D%2B%2Fg%29+%7C%7C+%5B%5D%29%7B%0Avar+turkish+%3D+NOT_ROMANIAN.test%28part%29%2C+had+%3D+%7Bskip%3A+n.skip%2C+tr%3A+n.tr%7D%3B%0Afor+%28var+ch+of+part%29%7B%0Avar+kind+%3D+%21FIX%5Bch%5D+%26%26+%21COMMA.test%28ch%29+%3F+%22%22+%3A+turkish+%26%26+S.test%28ch%29+%3F+%28FIX%5Bch%5D+%3F+%22skip%22+%3A+%22tr%22%29+%3A+FIX%5Bch%5D+%3F+%22mark%22+%3A+%22okl%22%3B%0Aif+%28%21kind%29%7B+buf+%2B%3D+ch%3B+continue%3B+%7D%0Aout.append%28buf%29%3B+buf+%3D+%22%22%3B%0Avar+e+%3D+document.createElement%28kind+%3D%3D%3D+%22mark%22+%3F+%22mark%22+%3A+%22span%22%29%3B%0Ae.className+%3D+kind+%3D%3D%3D+%22skip%22+%3F+%22tr%22+%3A+kind%3B%0Ae.textContent+%3D+ch%3B%0Ae.title+%3D+cp%28ch%29+%2B+%7Bmark%3A+%22%2C+a+cedilla%3A+wrong+in+Romanian%22%2C+okl%3A+%22%2C+the+comma+below%22%2C+skip%3A+%22%2C+a+cedilla%3A+skipped%2C+the+sentence+has+%5Cu0131+%5Cu011F+%5Cu00E7+%5Cu00F6+or+%5Cu00FC%22%2C+tr%3A+%22%2C+the+comma+below%2C+in+a+sentence+that+looks+Turkish%22%7D%5Bkind%5D%3B%0Aout.append%28e%29%3B%0An%5Bkind%5D%2B%2B%3B%0Aseen%5Be.className%5D%5Bch%5D+%3D+1%3B%0A%7D%0Aif+%28n.skip+%3E+had.skip%29+sent.skip%2B%2B%3B%0Aif+%28n.tr+%3E+had.tr%29+sent.tr%2B%2B%3B%0A%7D%0Aout.append%28buf%29%3B%0Aout.hidden+%3D+false%3B%0Avar+notes+%3D+%28n.skip+%3F+%22+Skipped+%22+%2B+n.skip+%2B+%22+%5Cu015F+in+%22+%2B+count%28sent.skip%2C+%22+sentence%22%2C+%22+sentences%22%29+%2B+%22+with+%5Cu0131+%5Cu011F+%5Cu00E7+%5Cu00F6+or+%5Cu00FC+%28Turkish%2C+or+a+name+like+Z%5Cu00FCrich+or+Erdo%5Cu011Fan%29%3A+check+by+eye.%22+%3A+%22%22%29+%2B%0A%28n.tr+%3F+%22+%22+%2B+count%28n.tr%2C+%22+%5Cu0219+is%22%2C+%22+%5Cu0219+are%22%29+%2B+%22+in+%22+%2B+%28sent.tr+%3D%3D%3D+1+%3F+%22a+sentence+that+looks%22+%3A+%22sentences+that+look%22%29+%2B+%22+Turkish%2C+and+Turkish+writes+%5Cu015F%3A+check+by+eye.%22+%3A+%22%22%29+%2B%0A%28raw+%21%3D%3D+s+%3F+%22+Some+letters+were+typed+as+two+code+points%3B+shown+here+combined.%22+%3A+%22%22%29%3B%0Avar+comma+%3D+n.okl+%3F+count%28n.okl%2C+%22+letter+has%22%2C+%22+letters+have%22%29+%2B+%22+the+comma+below+%28%5Cu0219+%5Cu021B%29.%22+%3A+%22None+has+the+comma+below+%28%5Cu0219+%5Cu021B%29.%22%3B%0Aif+%28n.mark%29%7B%0Av.className+%3D+%22verdict+bad%22%3B%0Av.textContent+%3D+count%28n.mark%2C+%22+letter+has%22%2C+%22+letters+have%22%29+%2B+%22+a+cedilla+%28%5Cu015F+%5Cu0163%29%2C+marked+red+below.+%22+%2B+comma+%2B+notes%3B%0A%7D+else+if+%28n.skip+%7C%7C+n.tr%29+v.textContent+%3D+%22Nothing+marked+red.%22+%2B+%28n.okl+%3F+%22+%22+%2B+comma+%3A+%22%22%29+%2B+notes%3B%0Aelse+if+%28n.okl%29%7B%0Av.className+%3D+%22verdict+ok%22%3B%0Av.textContent+%3D+%28n.okl+%3D%3D%3D+1+%3F+%22Clean%3A+the+one+s%2Ft+letter+has%22+%3A+%22Clean%3A+all+%22+%2B+n.okl+%2B+%22+s%2Ft+letters+have%22%29+%2B+%22+the+comma+below+%28%5Cu0219+%5Cu021B%29.%22+%2B+notes%3B%0A%7D+else+v.textContent+%3D+%22No+%5Cu0219%2C+%5Cu021B%2C+%5Cu015F+or+%5Cu0163+here.+My+scorer+fails+an+answer+with+no+%5Cu0219+or+%5Cu021B%3A+did+the+model+drop+the+diacritics%3F%22+%2B+notes%3B%0Afunction+list%28o%29%7B+return+Object.keys%28o%29.map%28function%28c%29%7B+return+c+%2B+%22+%22+%2B+cp%28c%29%3B+%7D%29.join%28%22%2C+%22%29%3B+%7D%0A%24%28%22legend%22%29.textContent+%3D+%28n.mark+%3F+%22Red%3A+a+cedilla%2C+%22+%2B+list%28seen.mark%29+%2B+%22.+%22+%3A+%22%22%29+%2B+%28n.okl+%3F+%22Green+line%3A+the+comma+below%2C+%22+%2B+list%28seen.okl%29+%2B+%22.+%22+%3A+%22%22%29+%2B%0A%28n.skip+%7C%7C+n.tr+%3F+%22Grey+dots%3A+in+a+sentence+that+looks+Turkish%2C+%22+%2B+list%28seen.tr%29+%2B+%22.%22+%3A+%22%22%29%3B%0A%24%28%22legend%22%29.hidden+%3D+%21%24%28%22legend%22%29.textContent%3B%0AshowFix%28n.mark+%3E+0%29%3B%0Aannounce%28v.textContent%2C+now%29%3B%0A%7D%0Afunction+showFix%28fixable%29%7B%0A%24%28%22fix%22%29.hidden+%3D+%21fixable%3B%0A%24%28%22undo%22%29.hidden+%3D+before+%3D%3D%3D+null%3B%0A%24%28%22fixRow%22%29.hidden+%3D+%24%28%22fixNote%22%29.hidden+%3D+%21fixable+%26%26+before+%3D%3D%3D+null%3B%0A%7D%0Afunction+reset%28text%29%7B+before+%3D+null%3B+%24%28%22fixMsg%22%29.textContent+%3D+%22%22%3B+if+%28text+%21%3D%3D+undefined%29+%24%28%22t%22%29.value+%3D+text%3B+%7D%0A%24%28%22t%22%29.oninput+%3D+function%28%29%7B+reset%28%29%3B+check%28false%29%3B+%7D%3B%0A%24%28%22sample%22%29.onclick+%3D+function%28%29%7B+reset%28SAMPLE%29%3B+check%28true%29%3B+%7D%3B%0A%24%28%22clear%22%29.onclick+%3D+function%28%29%7B+reset%28%22%22%29%3B+check%28true%29%3B+%24%28%22t%22%29.focus%28%29%3B+%7D%3B%0A%24%28%22fix%22%29.onclick+%3D+function%28%29%7B%0Avar+t+%3D+%24%28%22t%22%29%2C+fixed+%3D+fixRomanian%28t.value%29%3B%0Abefore+%3D+t.value%3B%0At.value+%3D+fixed%3B%0Acheck%28true%29%3B%0Afunction+say%28text%29%7B+%24%28%22fixMsg%22%29.textContent+%3D+text%3B+announce%28%24%28%22verdict%22%29.textContent+%2B+%22+%22+%2B+text%2C+true%29%3B+%7D%0Aclip%28fixed%29.then%28function%28%29%7B+say%28%22Fixed+and+copied.%22%29%3B+%24%28%22undo%22%29.focus%28%29%3B+%7D%29.catch%28function%28%29%7B%0At.focus%28%29%3B+t.select%28%29%3B+say%28%22Fixed.+The+text+in+the+box+is+selected%3A+%22+%2B+keys%29%3B%0A%7D%29%3B%0A%7D%3B%0A%24%28%22undo%22%29.onclick+%3D+function%28%29%7B+reset%28before%29%3B+check%28true%29%3B+%24%28%22t%22%29.focus%28%29%3B+%7D%3B%0A%3C%2Fscript%3E%0A%3C%2Fbody%3E%0A%3C%2Fhtml%3E%0A" title="LiveCodes Playground"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Measure Next
&lt;/h2&gt;

&lt;p&gt;Whether the echo survives a real retrieval pipeline, which cleans and chunks documents first; whether other languages with a look-alike letter from an old encoding echo it too; and whether it tracks how tokenizers split each letter (ş is common in Turkish text, ṣ is rare).&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://www.kaggle.com/benchmarks/danielbutnar/romanian-comma-below" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Romanian Comma Below on Kaggle&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Thirteen tasks, with every model answer in the run outputs. Read &lt;a href="https://www.kaggle.com/benchmarks/tasks/danielbutnar/romanian-comma-below-8-echo-four-inputs" rel="noopener noreferrer"&gt;task 8's leaderboard&lt;/a&gt;; the overall score averages different tasks for different models.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Reading the leaderboard
  &lt;p&gt;This post's numbers come from the runs up to 10 October; later runs on the leaderboard aren't in them. Task 8's leaderboard uses each model's latest run and counts an empty answer as a failure. That's why Qwen 3 Next Thinking shows 45%, and why Grok 4.5 and 4.6, listed by Kaggle but with every call errored (they're not among my 44), show 0%.&lt;/p&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  All the numbers (the 15 first-round models)
  &lt;p&gt;Task scores in %. Echo with cedilla is the mean of every run on the same prompts (tasks 2 and 8); Echo + rule pools tasks 7 and 8. See counts the first true/false or code point in each answer, as Kaggle does: DeepSeek-R1 listed every letter's code point before giving the right one, so on its final answers it scores 100 (Gemini 3.5 Flash-Lite 89).&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;Write&lt;/th&gt;
&lt;th&gt;Echo: cedilla&lt;/th&gt;
&lt;th&gt;Echo: no diacritics&lt;/th&gt;
&lt;th&gt;Echo + rule&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;th&gt;See&lt;/th&gt;
&lt;th&gt;Restore&lt;/th&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3 235B&lt;/td&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;61&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.1 Pro&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.7 Flash&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;76&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.8 Flash&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-R1&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;td&gt;69&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.5 Flash-Lite&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;67&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.5&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;66&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 5&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;td&gt;95&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.4 nano&lt;/td&gt;
&lt;td&gt;96&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;90&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;56&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Astra&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;55&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;td&gt;94&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 4.5&lt;/td&gt;
&lt;td&gt;92&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;72&lt;/td&gt;
&lt;td&gt;93&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok 4.20 Reasoning&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;td&gt;82&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;83&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The task builder, the fake models that test the scorer, the analysis and a script that re-checks this post's numbers against the raw answers are on &lt;a href="https://github.com/danielbutnar/comma-below" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Every input is generated from correctly spelled text in &lt;code&gt;src/items.py&lt;/code&gt;, so testing another letter pair or language is a small change. Built with &lt;a href="https://pypi.org/project/kaggle-benchmarks/" rel="noopener noreferrer"&gt;kaggle-benchmarks&lt;/a&gt;; the checker runs on &lt;a href="https://livecodes.io" rel="noopener noreferrer"&gt;LiveCodes&lt;/a&gt;. I used Claude as a coding and editing assistant.&lt;/p&gt;

&lt;p&gt;If your pipeline touches Romanian, paste one of your model's answers into the checker above, and run &lt;code&gt;fix_romanian()&lt;/code&gt; on the way in and on the way out. And if your language has its own ş, tell me in the comments.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Drum Bun: an agent that knows Romania rewrote its vignette this week</title>
      <dc:creator>Butnar Daniel</dc:creator>
      <pubDate>Wed, 30 Sep 2026 08:52:51 +0000</pubDate>
      <link>https://dev.to/danielbutnar/drum-bun-an-agent-that-knows-romania-rewrote-its-vignette-this-week-2d0g</link>
      <guid>https://dev.to/danielbutnar/drum-bun-an-agent-that-knows-romania-rewrote-its-vignette-this-week-2d0g</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;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Drum Bun tells a driver what their car needs between Romania and Germany or Austria, computed from dated, structured content in Sanity; the model never computes a price. On 18 questions in four languages: agent 18 / 18, semantic search over the same documents 16, keyword search 13, the same model without the content 4. &lt;a href="https://drum-bun-agent.vercel.app" rel="noopener noreferrer"&gt;Try it live&lt;/a&gt;, no login.&lt;/p&gt;

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

&lt;p&gt;Every Christmas, Romanians who live in Germany and Austria drive home through Hungary: over the 2019 holidays about 941,000 people crossed Romania's western border, 361,000 at Nădlac II alone (&lt;a href="https://www.politiadefrontiera.ro/ro/main/i-peste-25-milioane-de-persoane-au-tranzitat-frontiera-in-perioada-sarbatorilor-de-iarna-19003.html" rel="noopener noreferrer"&gt;Romanian Border Police&lt;/a&gt;). The questions before the drive are always the same: which vignettes, for which days, bought where, and what else gets you fined on the way? The answers are spread over four toll operators, four languages and a lot of outdated blog posts.&lt;/p&gt;

&lt;p&gt;This year they changed under everyone's feet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Romania&lt;/strong&gt; replaced its rovinietă on &lt;strong&gt;1 October 2026&lt;/strong&gt;: new seller (TollRo), prices in lei &lt;strong&gt;by Euro class&lt;/strong&gt;, and a car whose class cannot be shown pays the Euro 0 rate. It went live at midnight, as planned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hungary&lt;/strong&gt; added an &lt;strong&gt;M1 regional&lt;/strong&gt; vignette in 2026 that can make a yearly crossing cost less than half the national one, if you know that the Pest and Komárom-Esztergom county vignettes leave a gap on the M1 that only Fejér covers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Austria&lt;/strong&gt; sells only digital vignettes for validity from &lt;strong&gt;1 December 2026&lt;/strong&gt;, raised the on-the-spot penalty to €200, and its 2027 prices are not published yet, so a Christmas trip cannot be priced exactly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Drum Bun&lt;/strong&gt; ("have a good road", what Romanians say before a journey) tells you what your car needs for &lt;em&gt;your&lt;/em&gt; trip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the cheapest set of products per country for your dates and vehicle, with the price valid on each travel day (and a flag when it is not published yet);&lt;/li&gt;
&lt;li&gt;the rules that apply on those dates (winter tyres, emission zones at the destination, border checks);&lt;/li&gt;
&lt;li&gt;the outdated claims you probably read, each linked to the fact that corrects it;&lt;/li&gt;
&lt;li&gt;an agent you can ask in Romanian, German, Hungarian or English, which shows every Knowledge Base entry and query it used.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://drum-bun-agent.vercel.app" rel="noopener noreferrer"&gt;https://drum-bun-agent.vercel.app&lt;/a&gt; (no login; the planner works without the model, the chat is rate-limited)&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/X2qOuYowQro" width="710" height="399"&gt;
  &lt;/iframe&gt;
&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%2Fe0ee7hongyxl9sas5zb6.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%2Fe0ee7hongyxl9sas5zb6.png" alt="The Brașov to Munich plan: a strip map of the route through Romania, Hungary, Austria and Germany, the warnings before you go, and what to buy in each country with prices for each travel day" width="800" height="772"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The route as a strip map drawn from the data: tolled sections red with a yellow core, free sections hollow, borders dashed.&lt;/em&gt;&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%2Fo50o3z86hv7hkw1nlmsy.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%2Fo50o3z86hv7hkw1nlmsy.png" alt="The agent checks two claims from a blog; the expanded trace shows a Knowledge Base search and the two entries it opened" width="800" height="549"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Every answer shows its steps: planner run, Knowledge Base entries opened, GROQ queries.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/danielbutnar" rel="noopener noreferrer"&gt;
        danielbutnar
      &lt;/a&gt; / &lt;a href="https://github.com/danielbutnar/drum-bun" rel="noopener noreferrer"&gt;
        drum-bun
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Road-trip agent on Sanity Context: vignettes, tolls and rules between Romania and Germany/Austria, priced for your dates
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Drum Bun&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;What your car needs on the road between Romania and Germany or Austria&lt;/strong&gt;: vignettes and tolls for your dates (the cheapest combination), winter-tyre and emission-zone rules, and which claims you read online are out of date. Every answer is traced to an official source.&lt;/p&gt;
&lt;p&gt;Live: &lt;a href="https://drum-bun-agent.vercel.app" rel="nofollow noopener noreferrer"&gt;https://drum-bun-agent.vercel.app&lt;/a&gt; · Demo video (2:52): &lt;a href="https://youtu.be/X2qOuYowQro" rel="nofollow noopener noreferrer"&gt;https://youtu.be/X2qOuYowQro&lt;/a&gt; · Sanity project &lt;code&gt;pd5e7gez&lt;/code&gt;, dataset &lt;code&gt;production&lt;/code&gt; (public) · Built for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16" rel="nofollow"&gt;DEV Sanity Challenge&lt;/a&gt;, Path One: &lt;a href="https://dev.to/danielbutnar/drum-bun-an-agent-that-knows-romania-rewrote-its-vignette-this-week-2d0g" rel="nofollow"&gt;the post&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"Drum bun!" is what Romanians say before a journey, and what the sign says when you leave a town.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why this needs structure&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;"What does Brașov → Munich on 20 December, back 3 January, cost in a Euro 5 diesel?" has no page that answers it. The answer is computed from:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;route&lt;/strong&gt;: which road sections, in which Hungarian counties, which are tolled (the Salzburg Nord → Walserberg stretch is free, the M1 between…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/danielbutnar/drum-bun" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;h3&gt;
  
  
  1. Structured content that computes
&lt;/h3&gt;

&lt;p&gt;"Brașov to Munich, 20 December, back 3 January, Euro 5 diesel" has no page that answers it. So the content is modelled as the pieces the answer is computed from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;route&lt;/code&gt; → ordered &lt;code&gt;legs&lt;/code&gt; → &lt;code&gt;roadSection&lt;/code&gt; references. A section has &lt;code&gt;tolled&lt;/code&gt;, &lt;code&gt;coveredBy[]&lt;/code&gt; (the products that make it legal, any one is enough), &lt;code&gt;counties[]&lt;/code&gt; and &lt;code&gt;exemptVehicles[]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tollProduct&lt;/code&gt; with &lt;code&gt;validity&lt;/code&gt; (&lt;code&gt;days&lt;/code&gt;, &lt;code&gt;months&lt;/code&gt;, &lt;code&gt;calendarYear&lt;/code&gt; with overlap windows) and dated &lt;code&gt;prices[]&lt;/code&gt;: each price has &lt;code&gt;validFrom&lt;/code&gt;/&lt;code&gt;validTo&lt;/code&gt; and an optional emission &lt;code&gt;band&lt;/code&gt; (&lt;code&gt;euroMin&lt;/code&gt;, &lt;code&gt;euroMax&lt;/code&gt;, &lt;code&gt;electric&lt;/code&gt;, &lt;code&gt;appliesWhenUnknown&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rule&lt;/code&gt; with a yearly &lt;code&gt;season&lt;/code&gt; window, &lt;code&gt;conditional&lt;/code&gt; + &lt;code&gt;condition&lt;/code&gt;, &lt;code&gt;effectiveFrom/To&lt;/code&gt; and &lt;code&gt;severity&lt;/code&gt;; &lt;code&gt;zone&lt;/code&gt; with &lt;code&gt;dieselMinEuro&lt;/code&gt; and &lt;code&gt;status&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;claim&lt;/code&gt;: a statement found online, its &lt;code&gt;verdict&lt;/code&gt; (&lt;code&gt;outdated&lt;/code&gt;, &lt;code&gt;wrong&lt;/code&gt;, &lt;code&gt;misleading&lt;/code&gt;), the &lt;code&gt;source&lt;/code&gt; it was seen on, and &lt;code&gt;correctedBy[]&lt;/code&gt; references to the facts that are true now.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;source&lt;/code&gt; with &lt;code&gt;trust&lt;/code&gt; (official, club, press, blog, forum), &lt;code&gt;language&lt;/code&gt; and &lt;code&gt;checkedAt&lt;/code&gt;. Every fact references at least one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two of the stored prices of the 12-month rovinietă, as the planner reads them:&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="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;292&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"validFrom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"validTo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-12-31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"band"&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="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Euro IV–V"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"euroMin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"euroMax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&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="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;330&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"validFrom"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"validTo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-12-31"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"band"&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="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Euro 0–III or unknown"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"euroMin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"euroMax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"appliesWhenUnknown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;A pure planner with 21 unit tests walks that structure: it picks the price valid on each date, runs a small dynamic program for the cheapest cover (two 1-day vignettes beat one 2-month; a 2026 Austrian annual vignette still covers 3 January 2027), solves an exact set cover over Hungarian county vignettes and the M1 regional one, and applies Austria's 18-day rule for online purchases. The agent calls it as a tool. &lt;strong&gt;The model never computes a price.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  2. A Knowledge Base from official pages in four languages
&lt;/h3&gt;

&lt;p&gt;The Knowledge Base "Drum Bun road rules" is built from the dataset (a GROQ query that flattens each document into readable fields) plus 20 web pages: the toll operators, ministries and cities in Romanian, Hungarian, German and English, &lt;strong&gt;and the blogs drivers actually read&lt;/strong&gt;. The first build filed 14 issues; after all fixes and a full rebuild the Knowledge Base has 22 entries from 103 dataset documents and 20 web pages, 21 decided issues and 9 standing instructions. I resolved them with &lt;code&gt;@sanity/client&lt;/code&gt;'s &lt;code&gt;context&lt;/code&gt; API instead of clicking through the Dashboard, so every decision is in the repo with its reason (&lt;a href="https://github.com/danielbutnar/drum-bun/blob/main/context/knowledge-bases/decisions.md" rel="noopener noreferrer"&gt;decisions.md&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two of the four conflicts came from &lt;strong&gt;my own vocabulary&lt;/strong&gt;: the build read my enum value &lt;code&gt;carTrailer&lt;/code&gt; as "a trailer" and concluded trailers must carry warning triangles. One standing instruction fixed the vocabulary for every future build. Of the other two, one was true for different dates (Austria's substitute toll was €120 before 2026, €200 since), so an instruction now makes entries state the date; the other was a Hungarian motorcycle price that is right once you know motorcycles buy the car's annual vignette.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;invented&lt;/strong&gt; Hungarian purchase points (post offices, the automobile club) that no source names. An instruction limits entries to the channels the sources name.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;missed&lt;/strong&gt; one: an entry quoted the Romanian Interior Ministry page's winter-tyre fine, computed with an old penalty-point value. I wrote an instruction scoped to that page; the background contradiction check then filed an issue against the stale entry by itself, and applying it fixed the number. That loop, human instruction → automatic check → issue → rebuild, is the part I would not want to build myself.&lt;/li&gt;
&lt;li&gt;It &lt;strong&gt;keeps up&lt;/strong&gt;. On 1 October, the day Romania switched, I updated the dataset and ran a refresh: it re-checked every source and filed four issues, all about pages it had imported but never cited (CNAIR's own TollRo announcement, and four more pages with outdated prices or the German car-toll myth). I applied them and rebuilt the two entries that still mentioned the postponement bill.&lt;/li&gt;
&lt;/ul&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%2Ffmxmbpu8dretu9f0dpqf.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%2Ffmxmbpu8dretu9f0dpqf.png" alt="A Knowledge Base conflict on the How it knows page: the side that was not kept, the side that was kept, and why" width="800" height="237"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One of the conflicts the build filed, with the decision and the reason.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The public &lt;a href="https://drum-bun-agent.vercel.app/knowledge" rel="noopener noreferrer"&gt;How it knows&lt;/a&gt; page shows every issue with both sides, what was kept, the standing instructions and the outline, from a snapshot the script writes into the dataset.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Sanity Context MCP, two endpoints
&lt;/h3&gt;

&lt;p&gt;One endpoint serves one mode, so there are two: &lt;code&gt;drum-bun-kb&lt;/code&gt; (Knowledge Base mode: outline + &lt;code&gt;knowledge_base_read&lt;/code&gt;/&lt;code&gt;knowledge_base_search&lt;/code&gt;) and &lt;code&gt;drum-bun-rules&lt;/code&gt; (GROQ mode over the dataset, embeddings enabled). The agent (AI SDK 7, AI Gateway) fetches both initial contexts once, inlines them, and gets the planner as a third tool. Conversations are saved with Sanity Context Insights; a daily cron classifies them.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Does structure beat search?
&lt;/h3&gt;

&lt;p&gt;Eighteen questions in Romanian, German, Hungarian and English, each with facts an answer must contain and outdated facts it must not (&lt;a href="https://github.com/danielbutnar/drum-bun/blob/main/evals/questions.json" rel="noopener noreferrer"&gt;questions&lt;/a&gt;). Three baselines get the &lt;strong&gt;same model, the same prompt and output budget, and the same fields of the same Sanity documents&lt;/strong&gt; the Knowledge Base ingests. Only retrieval differs. (Re-run on 1 October with semantic search added, because an any-word keyword search alone is an easy opponent.)&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;correct&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Drum Bun agent (planner + Knowledge Base + GROQ via Sanity Context)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18 / 18&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic search over the same documents (embeddings, top 6)&lt;/td&gt;
&lt;td&gt;16 / 18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyword search over the same documents (any word, top 6)&lt;/td&gt;
&lt;td&gt;13 / 18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The same model with no documents&lt;/td&gt;
&lt;td&gt;4 / 18&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model on its own gets only the facts that have not changed in years (4 / 18). Give it the structured documents and even a search box answers most single facts, because the dataset states them plainly, with dates and Euro bands: that is the first point for structured content. Search breaks where the answer has to be put together from many documents. For the Christmas trip, semantic search's six hits covered Romania and Munich and nothing on Hungary or Austria. Asked which county vignettes cover the drive from Nădlac, it named the four M1 counties and forgot the two on the M43 and M5. The planner walks the route, so it cannot forget a country or a county.&lt;/p&gt;

&lt;p&gt;Scoring notes: the model writes non-breaking hyphens ("1‑day", "Bács‑Kiskun") that my patterns did not match, so the scorer now reads them as plain hyphens for every system (before that fix: 16, 15, 12, 4). One of the new Hungarian questions turned out lenient: keyword search passes it by naming the counties without the cheapest combination. Every run, with every answer, is in &lt;a href="https://github.com/danielbutnar/drum-bun/tree/main/evals/runs" rel="noopener noreferrer"&gt;&lt;code&gt;evals/runs&lt;/code&gt;&lt;/a&gt;; the method is in &lt;a href="https://github.com/danielbutnar/drum-bun/blob/main/evals/RESULTS.md" rel="noopener noreferrer"&gt;RESULTS.md&lt;/a&gt;. The first version of this post reported the 29 September run: 15 / 16 against keyword search's 11 / 16.&lt;/p&gt;

&lt;p&gt;Honest notes: the first run, on 29 September, scored the agent &lt;strong&gt;6 / 16&lt;/strong&gt; against keyword search's 9. It asked "which city?" for general questions, answered an English question in Romanian, returned empty answers when reasoning tokens ate the output budget, and hit the free model tier's 5-requests-a-minute limit. The fixes were a tool for single-product prices, language detection in code, a bigger output budget and a paced runner. That moment, from the failing answers to the paced re-run:&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Claude Code"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building Drum Bun with Claude Code&lt;/span&gt;
      &lt;span class="agent-session-model-badge"&gt;claude-opus-5-5&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The repo is public at &lt;a href="https://github.com/danielbutnar/drum-bun" rel="noopener noreferrer"&gt;https://github.com/danielbutnar/drum-bun&lt;/a&gt; with everything committed so far. Checking the evaluation:&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;node -e "
const r=JSON.parse(require('fs').readFileSync('evals/runs/2026-09-2...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ node -e "
const r=JSON.parse(require('fs').readFileSync('evals/runs/2026-09-29T18-29-39-078Z.json','utf8'));
for(const x of r.results){if(x.agent.pass)continue;console.log('=== '+x.id+' missing:'+JSON.stringify(x.agent.missing)+' wrong:'+JSON.stringify(x.agent.wrong));console.log(x.agent.text.slice(0,500).replace(/\n+/g,' | '))}"&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;=== christmas-munich missing:["1-day|one-day|1 day"] wrong:[]
Pe scurt — ca să nu fiți amendat: | - România: rovinietă 1 zi pentru 20.12.2026 și pentru 03.01.2027 — 26 RON/zi (2×26 = 52 RON). [CNAIR (RO)](https://portal.etoll.ro/) / [Ordin MTI 888/2026 (RO)](https://lege5.ro/Gratuit/ge4tqnzthazti/ordinul-nr-888-2026-pentru-aprobarea-categoriilor-de-vehicule-si-a-nivelului-rovinietei)   | - Ungaria: e‑vignette D1 (daily) pentru 20.12.2026 și 03.01.2027 — 5.550 HUF/zi (2×5.550 = 11.100 HUF). Cumpărați înainte de a intra pe autostradă, introduceți corect numărul
=== at-18-days missing:["18","(point of sale|petrol station|sales point|toll station|vending machine|10-day)"] wrong:[]
Which Romanian start city are you leaving from — Brașov, Bucharest, Cluj‑Napoca or Timișoara?
=== ro-unknown-euro missing:["39"] wrong:[]
Which exact travel date do you mean? (give one YYYY‑MM‑DD date after 2026-10-01)
=== ro-7-day missing:["(nu|no)","10 zile|10 days"] wrong:[]
Te referi la rovinieta pentru România? Pot presupune că ești cu o mașină privată (car) sau vrei să-mi spui alt tip de vehicul?
=== de-ms-tyres missing:["(Alpine|Schneeflocke|3PMSF|Bergpiktogramm)","2024"] wrong:[]
Kurzfrage: Von welcher rumänischen Stadt starten Sie, welches Ziel in Deutschland und an welchen Datum(en) fahren Sie?
=== euro5-cities missing:["Munich.{0,200}(yes|allowed|may enter|can)","Stuttgart.{0,300}(not|banned|no)"] wrong:[]
What date will you travel (YYYY-MM-DD)?
=== hu-surcharge missing:["27,?790|27 790","60"] wrong:[]
I can check that precisely, but I need your trip details first. What is your Romanian start city (Brașov, Bucharest, Cluj-Napoca or Timișoara), destination (Vienna, Munich, Stuttgart, Frankfurt or Berlin) and travel date(s)?
=== at-ersatzmaut missing:["200"] wrong:[]
Für welches Datum 2026 soll ich die Ersatzmaut prüfen?
=== hu-buy-after missing:["60 minutes"] wrong:[]

=== ro-winter-dates missing:["(snow|ice|conditions)"] wrong:[]&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;cd ~/code/danielbutnar/drum-bun/web &amp;amp;&amp;amp; node - &amp;lt;&amp;lt;'JS'
const fs=require('fs')
l...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ cd ~/code/danielbutnar/drum-bun/web &amp;amp;&amp;amp; node - &amp;lt;&amp;lt;'JS'
const fs=require('fs')
let s=fs.readFileSync('lib/agent.ts','utf8')
const rep=(a,b)=&amp;gt;{if(!s.includes(a))throw new Error('missing '+a.slice(0,60));s=s.replace(a,b)}
rep(`- If the trip is missing details the planner needs (start city, destination, dates, vehicle), ask one short question, or assume a car and say so. Unknown fuel or Euro class is fine: the planner prices the worst case and says so.`,
`- Most questions are general (a rule, one product's price, a fine, a claim someone read). Answer those directly from the Knowledge Base or GROQ. Never ask for a start city, destination or dates unless the user wants a full trip plan.
- For a full trip plan, call plan_trip with what you have: assume a car if the vehicle is missing and say so. Unknown fuel or Euro class is fine: the planner prices the worst case and says so. Ask one short question only when the start city, the destination or the departure date is missing.
- A question about a price or rule "after" or "from" a date means that date: do not ask which date.`)
rep(`- Answer in the user's language (Romanian, German, Hungarian or English).`, `- Answer in the language of the user's latest message (Romanian, German, Hungarian or English), even if the trip is in Romania.`)
rep(`      maxOutputTokens: 1400,`, `      // Reasoning tokens come out of the same budget: too low and the answer is empty.
      maxOutputTokens: 5000,`)
fs.writeFileSync('lib/agent.ts',s)
JS
sleep 3; npx tsx ../evals/run.mts http://localhost:3000 at-18-days,ro-unknown-euro,ro-7-day,de-ms-tyres,euro5-cities,hu-surcharge,at-ersatzmaut,hu-buy-after,ro-winter-dates,christmas-munich 2&amp;gt;&amp;amp;1 | grep -v "Assertion\|^\s*at "&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;christmas-munich   agent fail  baseline fail  tools: plan_trip
at-18-days         agent PASS  baseline PASS  tools: plan_trip,knowledge_base_read
ro-unknown-euro    agent fail  baseline PASS  tools: 
ro-7-day           agent PASS  baseline fail  tools: knowledge_base_read
de-ms-tyres        agent PASS  baseline PASS  tools: knowledge_base_search,knowledge_base_read
euro5-cities       agent fail  baseline fail  tools: knowledge_base_search
hu-surcharge       agent fail  baseline fail  tools: knowledge_base_read
at-ersatzmaut      agent PASS  baseline PASS  tools: knowledge_base_read
hu-buy-after       agent PASS  baseline PASS  tools: knowledge_base_search,knowledge_base_read
ro-winter-dates    agent fail  baseline PASS  tools: knowledge_base_search,knowledge_base_read

agent 5/10, keyword baseline 6/10
written /~/code/danielbutnar/drum-bun/evals/runs/2026-09-29T18-34-09-725Z.json&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;cd ~/code/danielbutnar/drum-bun &amp;amp;&amp;amp; node -e "
const r=JSON.parse(require('fs')...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ cd ~/code/danielbutnar/drum-bun &amp;amp;&amp;amp; node -e "
const r=JSON.parse(require('fs').readFileSync('evals/runs/2026-09-29T18-34-09-725Z.json','utf8'));
for(const x of r.results){if(x.agent.pass)continue;console.log('=== '+x.id+' missing:'+JSON.stringify(x.agent.missing)+' wrong:'+JSON.stringify(x.agent.wrong));console.log(x.agent.text.slice(0,900).replace(/\n+/g,' | '))}"&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;=== christmas-munich missing:["1-day|one-day|1 day"] wrong:[]
Pe scurt — ca să nu fiți amendat: 1) rovinietă valabilă pentru fiecare zi când circulați pe drumurile naţionale din RO; 2) e‑vigneta maghiară înainte de a intra pe autostradă (plata / număr de înmatriculare corect); 3) vigneta austriacă pentru zilele pe A‑utostrăzi; 4) WWF‑sticker (Umweltplakette verde) lipită înainte de intrarea în zona low‑emission a Münchenului. Mașina Euro 5 diesel este permisă în München. | Ce cumpărați (cheapest combo sugerat) și costuri estimate: | - România: 2 × Rovinietă 1 zi (26 RON / zi) — 52 RON total. Cumpărați prin TollRo (CNAIR). [CNAIR / TollRo](https://portal.etoll.ro/)   | - Ungaria: 2 × e‑vigneta D1 (napi) — 5.550 HUF / zi → 11.100 HUF total. Cumpărați înainte pe ematrica.nemzetiutdij.hu; introduceți corect plăcuţa și codul RO. [Nemzeti Útdíj](https://ematrica.nemzetiutdij.hu/)   | - Austria: 2 × vigneta 1‑day (auto) — 9.60 EUR / zi → 19.20 EUR total. Cumpăra
=== ro-unknown-euro missing:["39"] wrong:[]
I can fetch the official price — I just need one short detail: which of these start cities is your trip from: Brașov, Bucharest, Cluj‑Napoca or Timișoara?
=== euro5-cities missing:["Munich.{0,200}(yes|allowed|may enter|can)","Stuttgart.{0,300}(not|banned|no)"] wrong:[]

=== hu-surcharge missing:["27,?790|27 790","60"] wrong:[]

=== ro-winter-dates missing:["(snow|ice|conditions)"] wrong:[]&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;grep -n "\[agent\]" -A 6 "~/.claude/projects/C--Users-danie-code-danielbutnar...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ grep -n "\[agent\]" -A 6 "~/.claude/projects/C--Users-danie-code-danielbutnar/1d3dd969-55b7-437f-822b-82660f6f0d7c/tool-results/mcp-Claude_Browser-preview_logs-1790706873153.txt" | grep -v "^\s*$" | cut -c1-300 | head -40&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;1:[agent] stream error Error [GatewayInternalServerError]: AI Gateway requires a valid credit card on file to service requests. Please visit https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dadd-credit-card to add a card and unlock your free credits.
2-    at [3mignore-listed frames[23m {
3-  statusCode: [33m403[39m,
4-  generationId: [90mundefined[39m,
5-  isRetryable: [33mfalse[39m,
6-  type: [32m'internal_server_error'[39m,
7-  [cause]: Error [AI_APICallError]: {"error":{"message":"AI Gateway requires a valid credit card on file to service requests. Please visit https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dadd-credit-card to add a card and unlock your free credits.","type":"customer_verification_required"
--
47:[agent] ui stream error Error [GatewayInternalServerError]: AI Gateway requires a valid credit card on file to service requests. Please visit https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dadd-credit-card to add a card and unlock your free credits.
48-    at [3mignore-listed frames[23m {
49-  statusCode: [33m403[39m,
50-  generationId: [90mundefined[39m,
51-  isRetryable: [33mfalse[39m,
52-  type: [32m'internal_server_error'[39m,
53-  [cause]: Error [AI_APICallError]: {"error":{"message":"AI Gateway requires a valid credit card on file to service requests. Please visit https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dadd-credit-card to add a card and unlock your free credits.","type":"customer_verification_required
--
93:[agent] stream error Error [GatewayInternalServerError]: Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted access.
94-    at [3mignore-listed frames[23m {
95-  statusCode: [33m403[39m,
96-  generationId: [90mundefined[39m,
97-  isRetryable: [33mfalse[39m,
98-  type: [32m'internal_server_error'[39m,
99-  [cause]: Error [AI_APICallError]: {"error":{"message":"Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted access.","type":"no_providers_available","param":{"error":"Free tier users do no
--
140:[agent] ui stream error Error [GatewayInternalServerError]: Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted access.
141-    at [3mignore-listed frames[23m {
142-  statusCode: [33m403[39m,
143-  generationId: [90mundefined[39m,
144-  isRetryable: [33mfalse[39m,
145-  type: [32m'internal_server_error'[39m,
146-  [cause]: Error [AI_APICallError]: {"error":{"message":"Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted access.","type":"no_providers_available","param":{"error":"Free tier users do n
--
187:[agent] stream error Error [AI_RetryError]: Failed after 3 attempts. Last error: GatewayRateLimitError: Rate limit exceeded for openai/gpt-5-mini for provider azure: this team's limit of 5 requests per minute (across all regions) was reached. Retry after 60s.
188-    at [3mignore-listed frames[23m {
189-  cause: [90mundefined[39m,
190-  reason: [32m'maxRetriesExceeded'[39m,
191-  errors: [
192-    Error [GatewayRateLimitError]: Rate limit exceeded for openai/gpt-5-mini for provider azure: this team's limit of 5 requests per minute (across all regions) was reached. Retry after 60s.
193-        at [3mignore-listed frames[23m {
--&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;cd ~/code/danielbutnar/drum-bun/web &amp;amp;&amp;amp; cat &amp;gt;&amp;gt; lib/plan-tool.ts &amp;lt;&amp;lt;'EOF'

expor...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ cd ~/code/danielbutnar/drum-bun/web &amp;amp;&amp;amp; cat &amp;gt;&amp;gt; lib/plan-tool.ts &amp;lt;&amp;lt;'EOF'

export const COUNTRIES = ['RO', 'HU', 'AT', 'DE'] as const

const PRODUCTS_QUERY = `*[_type == "tollProduct" &amp;amp;&amp;amp; country-&amp;gt;code == $country &amp;amp;&amp;amp; $vehicle in vehicles] | order(kind asc, name.en asc){
  _id, name, kind, vehicles, localCategory, county, validity, prices[]{amount, currency, validFrom, validTo, status, band},
  activation, purchase[]{channel, label, url}, summary, penalty,
  pendingChanges[]{summary, wouldTakeEffect, checkAgainBy, "source": source-&amp;gt;{title, url}},
  "sources": sources[]-&amp;gt;{title, url, publisher, language, trust}
}`

// One country's products with the price valid on a date, for questions about
// a single product ("what does X cost after 1 October?"). Deterministic, one
// round trip, no trip details needed.
export const countryProductsTool = tool({
  description:
    'List every toll product of one country (RO, HU, AT) for a vehicle, with the official price valid on a given date, validity rule, online-purchase delay, penalty and sources. Use it for questions about one product, a price on a date, a Euro-class price, or a penalty, when no full trip is involved.',
  inputSchema: z.object({
    country: z.enum(COUNTRIES),
    date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe('The date the price should be valid on. Use today if the user gave none.'),
    vehicle: z.enum(['car', 'carTrailer', 'camper', 'motorcycle']),
    fuel: z.enum(['petrol', 'diesel', 'hybrid', 'electric', 'lpg']).nullable(),
    euroNorm: z.number().int().min(0).max(6).nullable(),
  }),
  execute: async ({country, date, vehicle, fuel, euroNorm}) =&amp;gt; {
    const products = await sanity.fetch&amp;lt;TollProduct[]&amp;gt;(PRODUCTS_QUERY, {country, vehicle})
    return products.map((p) =&amp;gt; {
      const hit = priceOn(p.prices ?? [], p.validity.unit === 'calendarYear' ? `${date.slice(0, 4)}-07-01` : date, {fuel, euroNorm})
      return {
        name: p.name.en,
        localName: p.name.ro ?? p.name.hu ?? p.name.de ?? null,
        kind: p.kind,
        county: p.county ?? null,
        validity: p.validity.wording,
        priceOnDate: hit
          ? {amount: hit.price.amount, currency: hit.price.currency, band: hit.price.band?.label ?? null, estimate: hit.estimate, bandNote: hit.bandNote}
          : null,
        onlineDelayDays: p.activation?.onlineDelayDays ?? 0,
        immediateOptions: p.activation?.immediateOptions ?? null,
        penalty: p.penalty?.note ?? null,
        buy: (p.purchase ?? []).filter((c) =&amp;gt; c.url).map((c) =&amp;gt; ({label: c.label, url: c.url})),
        pendingChanges: p.pendingChanges ?? [],
        sources: p.sources,
      }
    })
  },
})
EOF
node - &amp;lt;&amp;lt;'JS'
const fs=require('fs')
let s=fs.readFileSync('lib/plan-tool.ts','utf8')
s=s.replace("import {planTrip} from './planner'\nimport {fetchPlannerData} from './queries'", "import {planTrip, priceOn} from './planner'\nimport {fetchPlannerData, sanity} from './queries'\nimport type {TollProduct} from './types'")
fs.writeFileSync('lib/plan-tool.ts',s)

let a=fs.readFileSync('lib/agent.ts','utf8')
const rep=(x,y)=&amp;gt;{if(!a.includes(x))throw new Error('missing '+x.slice(0,60));a=a.replace(x,y)}
rep("import {planTripTool, today} from './plan-tool'", "import {countryProductsTool, planTripTool, today} from './plan-tool'")
rep(`function instructions(date: string) {`, `const LANGUAGE_NAMES = {ro: 'Romanian', de: 'German', hu: 'Hungarian', en: 'English'} as const

/** Cheap language detection for the reply: the model drifts into Romanian when a trip starts in Romania. */
export function detectLanguage(text: string): keyof typeof LANGUAGE_NAMES {
  const t = \` \${text.toLowerCase()} \`
  const score = (re: RegExp) =&amp;gt; (t.match(re) ?? []).length
  const ro = score(/[ăâîșşțţ]|\\s(și|să|pentru|cu|ce|care|este|mașin\\w*|rovinieta|pot|trebuie|nu)\\s/g)
  const de = score(/[äöüß]|\\s(ich|und|der|die|das|nicht|mit|für|reichen|wie|ist|mein\\w*|brauche)\\s/g)
  const hu = score(/[őű]|\\s(és|hogy|nem|kell|autópálya|mennyi|megyei|vagy)\\s/g)
  const best = Math.max(ro, de, hu)
  if (best === 0) return 'en'
  return best === ro ? 'ro' : best === de ? 'de' : 'hu'
}

function instructions(date: string) {`)
rep(`1. plan_trip: the deterministic planner`, `1. plan_trip and country_products: the deterministic planner and price lookup`)
rep(`For any question about what a trip needs or costs, call it first. Never compute prices, validity or coverage yourself.`, `For any question about what a trip needs or costs, call plan_trip first. For one product's price on a date (e.g. by Euro class) or a penalty, call country_products. Never compute prices, validity or coverage yourself.`)
rep(`    const system = [
      instructions(today()),`, `    const lastUser = [...messages].reverse().find((m) =&amp;gt; m.role === 'user')
    const lang = detectLanguage(lastUser?.parts.map((p) =&amp;gt; (p.type === 'text' ? p.text : '')).join(' ') ?? '')
    const system = [
      instructions(today()),
      \`The user's latest message is in \${LANGUAGE_NA
[… 1299 characters trimmed]&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;ok
TSC-DONE&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The code typechecks. Pulling the new Insights token into local env, adding a language-detection test, and pacing the evaluation to stay under 5 requests a minute:&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;cat &amp;gt; web/lib/agent.test.ts &amp;lt;&amp;lt;'EOF'
import {describe, expect, it} from 'vites...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ cat &amp;gt; web/lib/agent.test.ts &amp;lt;&amp;lt;'EOF'
import {describe, expect, it} from 'vitest'

import {detectLanguage} from './agent'

describe('detectLanguage', () =&amp;gt; {
  it('recognises the four languages of the route', () =&amp;gt; {
    expect(detectLanguage('Cât costă rovinieta de 12 luni pentru un diesel Euro 5?')).toBe('ro')
    expect(detectLanguage('Reichen M+S-Reifen im Winter in Deutschland?')).toBe('de')
    expect(detectLanguage('Mennyi a megyei matrica és hogy kell megvenni?')).toBe('hu')
    expect(detectLanguage('I drive from Brașov to Munich on 20 December. What do I need?')).toBe('en')
  })
})
EOF
node - &amp;lt;&amp;lt;'JS'
const fs=require('fs')
let s=fs.readFileSync('evals/run.mts','utf8')
s=s.replace("  const [agent, baseline] = await Promise.allSettled([askAgent(q.q), askBaseline(q.q)])",
"  // The free AI Gateway tier allows 5 requests a minute: run one system at a\n  // time and pause, so a rate limit never counts as a wrong answer.\n  const [agent] = await Promise.allSettled([askAgent(q.q)])\n  await sleep(PAUSE_MS)\n  const [baseline] = await Promise.allSettled([askBaseline(q.q)])\n  await sleep(PAUSE_MS / 3)")
s=s.replace("const only = process.argv[3]?.split(',')", "const only = process.argv[3]?.split(',')\nconst PAUSE_MS = Number(process.env.EVAL_PAUSE_MS ?? 45_000)\nconst sleep = (ms: number) =&amp;gt; new Promise((r) =&amp;gt; setTimeout(r, ms))")
s=s.replace("      if (ev.type === 'tool-input-available') tools.push(ev.toolName)", "      if (ev.type === 'tool-input-available') tools.push(ev.toolName)\n      if (ev.type === 'error') text += `\\n[ERROR] ${ev.errorText}`")
fs.writeFileSync('evals/run.mts',s)
JS
cd web &amp;amp;&amp;amp; npx vitest run lib 2&amp;gt;&amp;amp;1 | tail -3&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;[2m   Start at [22m 21:36:03
[2m   Duration [22m 2.72s[2m (transform 396ms, setup 0ms, collect 1.68s, tests 32ms, environment 1ms, prepare 911ms)[22m&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;npx vitest run lib 2&amp;gt;&amp;amp;1 | grep -E "Tests|FAIL|passed|failed" | head -5&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ npx vitest run lib 2&amp;gt;&amp;amp;1 | grep -E "Tests|FAIL|passed|failed" | head -5&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt; [31m❯[39m lib/agent.test.ts [2m([22m[2m1 test[22m[2m | [22m[31m1 failed[39m[2m)[22m[32m 12[2mms[22m[39m
[31m⎯⎯⎯⎯⎯⎯⎯[39m[1m[41m Failed Tests 1 [49m[22m[31m⎯⎯⎯⎯⎯⎯⎯[39m
[41m[1m FAIL [22m[49m lib/agent.test.ts[2m &amp;gt; [22mdetectLanguage[2m &amp;gt; [22mrecognises the four languages of the route
[2m Test Files [22m [1m[31m1 failed[39m[22m[2m | [22m[1m[32m1 passed[39m[22m[90m (2)[39m
[2m      Tests [22m [1m[31m1 failed[39m[22m[2m | [22m[1m[32m21 passed[39m[22m[90m (22)[39m&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The English question with "Brașov" in it was detected as Romanian, the exact failure the check is meant to catch. The fix: ignore place names, and count English words too.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;node - &amp;lt;&amp;lt;'JS'
const fs=require('fs')
let a=fs.readFileSync('lib/agent.ts','ut...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ node - &amp;lt;&amp;lt;'JS'
const fs=require('fs')
let a=fs.readFileSync('lib/agent.ts','utf8')
const start=a.indexOf('export function detectLanguage')
const end=a.indexOf('\n}\n', start)+3
a=a.slice(0,start)+`export function detectLanguage(text: string): keyof typeof LANGUAGE_NAMES {
  // Place names carry diacritics in every language here (Brașov, München), so
  // they must not decide the language.
  const t = \` \${text.toLowerCase()} \`.replace(/brașov|brasov|timișoara|bucurești|cluj-napoca|nădlac|borș|münchen|wien|győr|hegyeshalom/g, ' ')
  const score = (re: RegExp) =&amp;gt; (t.match(re) ?? []).length
  const ro = score(/[ăâîșşțţ]|\\s(și|să|pentru|cu|ce|care|este|mașin\\w*|rovinieta|pot|trebuie|nu|cât|costă)\\s/g)
  const de = score(/[äöüß]|\\s(ich|und|der|die|das|nicht|mit|für|reichen|wie|ist|mein\\w*|brauche|im|in der)\\s/g)
  const hu = score(/[őű]|\\s(és|hogy|nem|kell|autópálya|mennyi|megyei|vagy|matrica)\\s/g)
  const en = score(/\\s(the|and|what|do|does|i|to|from|on|is|my|need|cost|can|which|are)\\s/g)
  const best = Math.max(ro, de, hu, en)
  if (best === 0 || best === en) return 'en'
  return best === ro ? 'ro' : best === de ? 'de' : 'hu'
}
`+a.slice(end)
fs.writeFileSync('lib/agent.ts',a)
JS
npx vitest run lib 2&amp;gt;&amp;amp;1 | grep -E "Tests |FAIL" | head -3&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;[31m⎯⎯⎯⎯⎯⎯⎯[39m[1m[41m Failed Tests 1 [49m[22m[31m⎯⎯⎯⎯⎯⎯⎯[39m
[41m[1m FAIL [22m[49m lib/agent.test.ts[2m &amp;gt; [22mdetectLanguage[2m &amp;gt; [22mrecognises the four languages of the route
[2m      Tests [22m [1m[31m1 failed[39m[22m[2m | [22m[1m[32m21 passed[39m[22m[90m (22)[39m&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;npx vitest run lib/agent.test.ts 2&amp;gt;&amp;amp;1 | grep -E "Expected|Received|toBe|❯ lib...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ npx vitest run lib/agent.test.ts 2&amp;gt;&amp;amp;1 | grep -E "Expected|Received|toBe|❯ lib" | head -6&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Expected: [32m"[7md[27me"[39m
Received: [31m"e[7mn[27m"[39m&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;cat &amp;gt; /tmp/lang.test.ts &amp;lt;&amp;lt;'EOF'
EOF
node -e "
const t=' reichen m+s-reifen im...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ cat &amp;gt; /tmp/lang.test.ts &amp;lt;&amp;lt;'EOF'
EOF
node -e "
const t=' reichen m+s-reifen im winter in deutschland? ';
const sc=(re)=&amp;gt;(t.match(re)??[]).length;
console.log('de',sc(/[äöüß]|\s(ich|und|der|die|das|nicht|mit|für|reichen|wie|ist|mein\w*|brauche|im|in der)\s/g),'en',sc(/\s(the|and|what|do|does|i|to|from|on|is|my|need|cost|can|which|are)\s/g))"; grep -n "reichen" lib/agent.ts&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;de 2 en 0
29:  const de = score(/[äöüß]|s(ich|und|der|die|das|nicht|mit|für|reichen|wie|ist|meinw*|brauche|im|in der)s/g)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;My scripted edit lost the regex backslashes (&lt;code&gt;\s&lt;/code&gt; became &lt;code&gt;s&lt;/code&gt;). Rewriting the function directly:&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-read"&gt;Read&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;~\code\danielbutnar\drum-bun\web\lib\agent.ts&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;danielbutnar&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;drum&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;bun&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="mi"&gt;20&lt;/span&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LANGUAGE_NAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Romanian&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;de&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;German&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hu&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hungarian&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;English&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;
&lt;span class="mi"&gt;21&lt;/span&gt;  
&lt;span class="mi"&gt;22&lt;/span&gt;  &lt;span class="cm"&gt;/** Cheap language detection for the reply: the model drifts into Romanian when a trip starts in Romania. */&lt;/span&gt;
&lt;span class="mi"&gt;23&lt;/span&gt;  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;detectLanguage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;keyof&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;LANGUAGE_NAMES&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="mi"&gt;24&lt;/span&gt;    &lt;span class="c1"&gt;// Place names carry diacritics in every language here (Brașov, München), so&lt;/span&gt;
&lt;span class="mi"&gt;25&lt;/span&gt;    &lt;span class="c1"&gt;// they must not decide the language.&lt;/span&gt;
&lt;span class="mi"&gt;26&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;` &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt; `&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/brașov|brasov|timișoara|bucurești|cluj-napoca|nădlac|borș|münchen|wien|győr|hegyeshalom/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;27&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
&lt;span class="mi"&gt;28&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;ăâîșşțţ&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;|s&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;și|să|pentru|cu|ce|care|este|mașinw*|rovinieta|pot|trebuie|nu|cât|costă&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;s/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;29&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;de&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;äöüß&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;|s&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;ich|und|der|die|das|nicht|mit|für|reichen|wie|ist|meinw*|brauche|im|in der&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;s/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;30&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;őű&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;|s&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;és|hogy|nem|kell|autópálya|mennyi|megyei|vagy|matrica&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;s/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;31&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/s&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;the|and|what|do|does|i|to|from|on|is|my|need|cost|can|which|are&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;s/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;32&lt;/span&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;de&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mi"&gt;33&lt;/span&gt;    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;en&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="mi"&gt;34&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;ro&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;de&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hu&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="mi"&gt;35&lt;/span&gt;  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="mi"&gt;36&lt;/span&gt;  
&lt;span class="mi"&gt;37&lt;/span&gt;  &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;instructions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;Edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;~\code\danielbutnar\drum-bun\web\lib\agent.ts&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;danielbutnar&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;drum&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;bun&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;The&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;danielbutnar&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;drum&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;bun&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;lib&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="nx"&gt;has&lt;/span&gt; &lt;span class="nx"&gt;been&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="nx"&gt;successfully&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;npx vitest run lib 2&amp;gt;&amp;amp;1 | grep -E "Tests |FAIL" | head -3; grep -n '\\\\s\|(?...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ npx vitest run lib 2&amp;gt;&amp;amp;1 | grep -E "Tests |FAIL" | head -3; grep -n '\\\\s\|(?=' lib/agent.ts | head -3&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;[2m      Tests [22m [1m[32m22 passed[39m[22m[90m (22)[39m
28:  const ro = score(/[ăâîșşțţ]|\s(și|să|pentru|cu|ce|care|este|mașin\w*|rovinieta|pot|trebuie|nu|cât|costă)(?=\s)/g)
29:  const de = score(/[äöüß]|\s(ich|und|der|die|das|nicht|mit|für|reichen|wie|ist|mein\w*|brauche|im)(?=\s)/g)
30:  const hu = score(/[őű]|\s(és|hogy|nem|kell|autópálya|mennyi|megyei|vagy|matrica)(?=\s)/g)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;All 22 tests pass. Checking the editor's two type complaints against the real compiler:&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-bash"&gt;Bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;npx tsc --noEmit -p . 2&amp;gt;&amp;amp;1 | grep -v "LayoutProps\|PageProps" | head -5; echo...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ npx tsc --noEmit -p . 2&amp;gt;&amp;amp;1 | grep -v "LayoutProps\|PageProps" | head -5; echo TSC-DONE; cd .. &amp;amp;&amp;amp; git add -A &amp;amp;&amp;amp; git commit -qm "Agent: country_products tool, language detection, rate-limit handling; eval pacing; cover image and post draft

Co-Authored-By: Claude Opus 5.5 &amp;lt;noreply@anthropic.com&amp;gt;" &amp;amp;&amp;amp; git push -q origin main 2&amp;gt;&amp;amp;1 | tail -2; git log --oneline | head -1&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;TSC-DONE
dac1675 Agent: country_products tool, language detection, rate-limit handling; eval pacing; cover image and post draft&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Both were stale; the compiler is clean. Committed and pushed. Starting the full paced evaluation in the background (about 20 minutes, to respect the rate limit):&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        20 messages (range 332-351) of 507 total
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;h3&gt;
  
  
  What did not go smoothly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A dataset source query with &lt;code&gt;select()&lt;/code&gt; made the ingest fail with a generic server error; plain projections work.&lt;/li&gt;
&lt;li&gt;A website source crawls everything under the URL's path: the Munich city page pulled in 306 pages of a 150-source budget. Single deep URLs import one page.&lt;/li&gt;
&lt;li&gt;GROQ mode needs a deployed Studio, not only a deployed schema.&lt;/li&gt;
&lt;li&gt;The AI Gateway's free tier does not include Claude models and allows 5 requests a minute, so the live agent runs on &lt;code&gt;gpt-5-mini&lt;/code&gt;, the example questions replay recorded answers (labelled as such, with an "ask it live" button), and a busy model says so instead of failing silently. The planner makes the model choice matter less.&lt;/li&gt;
&lt;li&gt;The first eval run lost to keyword search (above). Measuring early is what made the agent good.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Project ID: &lt;strong&gt;&lt;code&gt;pd5e7gez&lt;/code&gt;&lt;/strong&gt;, dataset &lt;strong&gt;&lt;code&gt;production&lt;/code&gt;&lt;/strong&gt; (public). Try &lt;a href="https://pd5e7gez.api.sanity.io/v2026-09-01/data/query/production?query=*%5B_id%3D%3D%22product-ro-12month%22%5D%5B0%5D%7Bname%2C%20prices%7D" rel="noopener noreferrer"&gt;one product's stored prices&lt;/a&gt;: &lt;code&gt;*[_id=="product-ro-12month"][0]{name, prices}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Organization &lt;code&gt;ob2cyckj9&lt;/code&gt;, Knowledge Base &lt;code&gt;kbv1SRpT2A3t&lt;/code&gt;, MCP endpoints &lt;code&gt;drum-bun-kb&lt;/code&gt; and &lt;code&gt;drum-bun-rules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Studio: &lt;a href="https://drum-bun.sanity.studio" rel="noopener noreferrer"&gt;https://drum-bun.sanity.studio&lt;/a&gt; (members only; the schema is in &lt;a href="https://github.com/danielbutnar/drum-bun/blob/main/studio/schemaTypes" rel="noopener noreferrer"&gt;&lt;code&gt;studio/schemaTypes&lt;/code&gt;&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Read the content without a login: &lt;a href="https://pd5e7gez.api.sanity.io/v2026-09-01/data/query/production?query=*%5B_id%3D%3D%22route-brasov-munich%22%5D%5B0%5D%7Btitle%2C%20legs%5B%5D%7B%22country%22%3A%20country-%3Ecode%2C%20km%2C%20%22sections%22%3A%20sections%5B%5D-%3E%7Broad%2C%20from%2C%20to%2C%20tolled%7D%7D%7D" rel="noopener noreferrer"&gt;a route with its road sections&lt;/a&gt;, &lt;a href="https://pd5e7gez.api.sanity.io/v2026-09-01/data/query/production?query=*%5B_id%3D%3D%22section-hu-m1-39-48%22%5D%5B0%5D%7Broad%2C%20from%2C%20to%2C%20tolled%2C%20counties%2C%20%22coveredBy%22%3A%20coveredBy%5B%5D-%3Ename.en%7D" rel="noopener noreferrer"&gt;the M1 stretch only the Fejér vignette covers&lt;/a&gt;, &lt;a href="https://pd5e7gez.api.sanity.io/v2026-09-01/data/query/production?query=*%5B_id%3D%3D%22claim-ro-7-and-90-days%22%5D%5B0%5D%7Bstatement%2C%20quote%2C%20verdict%2C%20%22seenOn%22%3A%20seenOn-%3E%7Bpublisher%2C%20url%2C%20trust%7D%2C%20explanation%2C%20%22correctedBy%22%3A%20correctedBy%5B%5D-%3Ename.en%7D" rel="noopener noreferrer"&gt;a blog claim and the products that correct it&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Agent Session
&lt;/h2&gt;

&lt;p&gt;I built Drum Bun with Claude Code. The excerpt in section 4 comes from that session: the first eval losing to keyword search, and the fixes that followed.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
