<?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: Joe Buckle</title>
    <description>The latest articles on DEV Community by Joe Buckle (@joebuckle-dev).</description>
    <link>https://dev.to/joebuckle-dev</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%2F266265%2F4888d4ab-b03b-4d99-b4e1-fadc3cd6bc05.jpeg</url>
      <title>DEV Community: Joe Buckle</title>
      <link>https://dev.to/joebuckle-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joebuckle-dev"/>
    <language>en</language>
    <item>
      <title>Translating a Japanese Sega Saturn Game From 1998: The User Interface</title>
      <dc:creator>Joe Buckle</dc:creator>
      <pubDate>Tue, 04 Aug 2026 17:40:48 +0000</pubDate>
      <link>https://dev.to/joebuckle-dev/translating-a-japanese-sega-saturn-game-from-1998-the-user-interface-4o8</link>
      <guid>https://dev.to/joebuckle-dev/translating-a-japanese-sega-saturn-game-from-1998-the-user-interface-4o8</guid>
      <description>&lt;p&gt;This is part 2 of a 3 part series, probably...&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Title&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;&lt;a href="https://dev.to/joebuckle-dev/no-one-asked-for-this-translating-a-1998-japanese-dating-sim-i-bought-by-accident-2m1"&gt;Translating a 1998 Japanese Visual Novel on Sega Saturn I Bought by Accident&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Translating a Japanese Sega Saturn Game From 1998: The User Interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;...incoming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It is worth noting up front that without AI I don't think this project would be possible for me. It has been instrumental in discovering the source code and figuring out where all this stuff goes. &lt;/p&gt;

&lt;p&gt;Part 1 ended with a Sega Saturn in the corner of the room playing a Japanese visual novel in English.&lt;/p&gt;

&lt;p&gt;The dialogue worked. That was the important milestone. The characters were speaking English, the script was extracted, translated, rebuilt, and running on the original hardware.&lt;/p&gt;

&lt;p&gt;But of course when I tried to play it the dialogue was English but everything around it was still Japanese.&lt;/p&gt;

&lt;p&gt;Menus. Locations. Profile cards. The save screen - the parts of the game that tell you what to do.&lt;/p&gt;

&lt;p&gt;The UI was about to be a very different problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  The text that isn't in the script
&lt;/h2&gt;

&lt;p&gt;The dialogue was relatively straightforward because it lived in a place designed for dialogue.&lt;/p&gt;

&lt;p&gt;This wasn't the case for the user interface.&lt;/p&gt;

&lt;p&gt;The menus and labels are stored inside the game's executable as raw strings. They are not in the script files, and they are not stored in a nice, editable format.&lt;/p&gt;

&lt;p&gt;Each string sits in a fixed space, with the next string immediately after it.&lt;/p&gt;

&lt;p&gt;The UI was built around fixed Japanese labels, with no extra space reserved for longer text in another language, so English strings simply overflow into whatever is next to them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;アルバイト&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;fits perfectly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Part-time Job&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;does not.&lt;/p&gt;

&lt;p&gt;The extra bytes have to come from somewhere.&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%2Fpd87g19c311ecwf5qg0r.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%2Fpd87g19c311ecwf5qg0r.png" alt="Garbled, overlapping English text on the Yokohama map - what overflow actually looks like on screen" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is what it looks like when it goes wrong. That label should say one thing. It is drawing several things at once, on top of each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first UI mystery
&lt;/h2&gt;

&lt;p&gt;Luckily, some English text already looked better than expected.&lt;/p&gt;

&lt;p&gt;If you read the last article, you'll know the dialogue renderer needed a patch because every character was treated as the same width.&lt;/p&gt;

&lt;p&gt;The UI was different.&lt;/p&gt;

&lt;p&gt;Some labels were compressed. Others were spaced out.&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%2F5jrqzojlpvrtt75w3wwc.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%2F5jrqzojlpvrtt75w3wwc.png" alt="The schedule label before and after: PLANS is spaced out wide while the date panel beside it draws narrow" width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before I had it looking like the above, P L A N S was letter-spaced across a wide gap and overflowing its container.&lt;/p&gt;

&lt;p&gt;The game actually contains two versions of its font. A wide one at 24 pixels for Japanese, and a narrow one at 16 pixels that covers everything that isn't a kanji, including the full Latin alphabet.&lt;/p&gt;

&lt;p&gt;The renderer automatically chooses between them depending on the character being drawn.&lt;/p&gt;

&lt;p&gt;This was something the original developers had already solved in 1998!&lt;/p&gt;

&lt;p&gt;They built an interface that could mix Japanese and English characters, even though English was never going to be the primary language. Lucky us!&lt;/p&gt;




&lt;h2&gt;
  
  
  When English exposes old assumptions
&lt;/h2&gt;

&lt;p&gt;The problems started appearing when English got longer.&lt;/p&gt;

&lt;p&gt;One menu centres its text using a simple calculation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;advance&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;96 pixels of usable width, divide the leftover space, nudge it right by 4. Perfectly sensible.&lt;/p&gt;

&lt;p&gt;Then you give it a seven character English word:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// "PROFILE" -&amp;gt; 7 characters at 24 pixels each&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;168&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// x = -32&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no safety check, I'm guessing because there never needed to be. No Japanese label was ever long enough to go negative. Mine were, immediately, and the label starts drawing outside its own area.&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%2Fx9gbbz6lkg9gh32lpcda.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%2Fx9gbbz6lkg9gh32lpcda.png" alt="The travel map title overflowing its box, spilling over stray characters" width="800" height="621"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That title plate is meant to say "Travel Map". The text has escaped the box and collided with whatever was already on screen.&lt;/p&gt;

&lt;p&gt;So rather than making this a complicated rom hack, I decided to get Claude to change the words so they still made sense but fit the rules the original game expected.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;STATUS&lt;/code&gt; survives. &lt;code&gt;PROFILE&lt;/code&gt; does not.&lt;/p&gt;




&lt;h2&gt;
  
  
  The plan was simple
&lt;/h2&gt;

&lt;p&gt;Most UI strings have pointers - so the obvious solution would be to move the longer English string elsewhere and update the pointer.&lt;/p&gt;

&lt;p&gt;This seems to be a common technique in reverse engineering.&lt;/p&gt;

&lt;p&gt;And mostly, it worked. Locations, menu entries, options, 64 profile fields. All moved without complaint.&lt;/p&gt;

&lt;p&gt;Then the schedule screen locked the game solid.&lt;/p&gt;

&lt;p&gt;I got this wrong three times. I blamed the line length, then the memory region I was writing into, then the renderer. What I should have done first was build a version with no UI changes at all, to see whether the screen still worked.&lt;/p&gt;

&lt;p&gt;It worked perfectly. So the fault was my text, not the game.&lt;/p&gt;

&lt;p&gt;From there it was just bisection. Hold back the whole screen, works. Put the labels back, works. Put the nine job messages back but leave them exactly where they were, works.&lt;/p&gt;

&lt;p&gt;Put them back and let them move, locks.&lt;/p&gt;

&lt;p&gt;The pointers were never the problem. They all existed, and they were all real. The problem is that the code also reads forward from one message to the next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MSG_BASE&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// walk to the next one&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So message 4 has to physically sit directly after message 3. Move any of them somewhere roomier and the walk carries on into whatever bytes happen to be sitting there instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RELOCATABLE                      BLOCK-WALKED
  ptr --&amp;gt; "PLANS"                  base --&amp;gt; "msg 1"
  ptr --&amp;gt; "TRAVEL"                          "msg 2"   must follow
  ptr --&amp;gt; "PHONE"                           "msg 3"   must follow

  move it, rewrite the pointer     move one, break every one after it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having a pointer wasn't enough. Some of these strings are addressed twice, in two different ways, and only one of them is visible to a tool that scans for pointers.&lt;/p&gt;

&lt;p&gt;The rule I ended up with: message blocks stay where they are. Fit the English to the space rather than moving the space.&lt;/p&gt;




&lt;h2&gt;
  
  
  The padding that wasn't padding
&lt;/h2&gt;

&lt;p&gt;The executable appeared to contain spare space. There were gaps between strings filled with zeros which looked like enough space to fit the extra English.&lt;/p&gt;

&lt;p&gt;But it turns out the Saturn's processor expects certain data to be aligned correctly in memory.&lt;/p&gt;

&lt;p&gt;1,319 of the 1,321 strings begin on 4 byte boundaries. Those gaps are there to maintain alignment.&lt;/p&gt;

&lt;p&gt;Removing them causes the game to crash to a black screen.&lt;/p&gt;

&lt;p&gt;Three times, before I worked out what I was looking at.&lt;/p&gt;




&lt;h2&gt;
  
  
  Finding space that isn't there
&lt;/h2&gt;

&lt;p&gt;Looking back at the executable it appeared to have large sections of empty-looking data. Surely some of that must be unused?&lt;/p&gt;

&lt;p&gt;Not necessarily.&lt;/p&gt;

&lt;p&gt;Some areas are tables that are only ever read, so nothing writes to them and they look dead from the outside. Others are buffers belonging to one screen. One region I was confident about turned out to be the save screen's working memory, which I had simply never opened while testing.&lt;/p&gt;

&lt;p&gt;A region can look empty until the exact moment the game needs it.&lt;/p&gt;

&lt;p&gt;Proving something is unused is a much harder problem than it sounds.&lt;/p&gt;

&lt;p&gt;There was no reliable free space :(&lt;/p&gt;




&lt;h2&gt;
  
  
  Creating space instead
&lt;/h2&gt;

&lt;p&gt;The executable loads into memory just below the stack, and the stack grows downwards. Between them is a small gap.&lt;/p&gt;

&lt;p&gt;After measuring normal gameplay across a couple of sessions, the stack never came closer than about 1.8 KB. I extended the executable into that area, keeping 512 bytes clear as a margin, which gave 1,344 bytes of extra room.&lt;/p&gt;

&lt;p&gt;It is not a perfect solution, but the failure mode is better.&lt;/p&gt;

&lt;p&gt;If I accidentally overwrite game data, the result could be anything.&lt;/p&gt;

&lt;p&gt;If the stack ever reaches the new strings, the worst case is broken text.&lt;/p&gt;




&lt;h2&gt;
  
  
  Saving bytes with words
&lt;/h2&gt;

&lt;p&gt;The obvious space saving exercise would be modifying the translation itself. Some punctuation was removed, long labels were shortened, and words were changed to fit the available space.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Save to this file?&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;became:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Save here?&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;長崎駅&lt;/code&gt; is Nagasaki Station. It became:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Nagasaki&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;None of these changes mattered individually, but together they made the interface fit.&lt;/p&gt;




&lt;h2&gt;
  
  
  The screens that aren't menus
&lt;/h2&gt;

&lt;p&gt;A menu is the easy case. Most of the interface isn't one.&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%2Ftfqoxubcic7ixd2w8j6c.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%2Ftfqoxubcic7ixd2w8j6c.png" alt=" " width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This little window caused me more trouble than any menu. The script has location labels in it, I had translated all of them, and the window was still Japanese. There are two completely separate sets of place names in this game: the ones in the script, and a second list of 160 inside the executable. The window uses the second one. So "the labels are 100% translated" was perfectly true and completely useless.&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%2Ffhb4cwtnilia4n9l4app.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%2Ffhb4cwtnilia4n9l4app.png" alt="The travel map before and after: 金沢着 20日18時 becomes Kanazawa 20d 18h" width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The calendar and travel panels were the opposite, and a genuine relief. They aren't sentences at all, they're assembled from pieces at runtime: place name plus 着 for an arrival, day plus 日, hour plus 時. Translate the small joining pieces and the English just appends itself. A rare case of a 1998 design decision doing me a favour.&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%2F331f0ghpn69d2vq87kiq.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%2F331f0ghpn69d2vq87kiq.png" alt="The diary calendar in English" width="800" height="621"&gt;&lt;/a&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%2Fw4vrgw4ggw9xk2xyr1aa.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%2Fw4vrgw4ggw9xk2xyr1aa.png" alt=" " width="800" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now - I know the settings page isn't perfect, but it does the job.&lt;/p&gt;




&lt;h2&gt;
  
  
  The limits you cannot negotiate
&lt;/h2&gt;

&lt;p&gt;Some parts of the UI cannot be expanded.&lt;/p&gt;

&lt;p&gt;Character names are stored in fixed size records, 26 bytes each, and the name is only the first 7 of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;82 D9 82 CC 82 A9 00   ほのか + terminator   &amp;lt;- the name slot
02 BC 0A 04 2F         stats
00 00 00 00 00         padding
54 41 45 00            "TAE"                  &amp;lt;- voice stream tag
88 C0 92 42 00         安達                    &amp;lt;- surname
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The space immediately after the name is live game data. Making the name field larger would overwrite her stats.&lt;/p&gt;

&lt;p&gt;And these particular strings have no pointer anywhere. The game works out their address by arithmetic, so there is nothing to update and nowhere to move them to.&lt;/p&gt;

&lt;p&gt;So that leaves 6 usable bytes. Japanese text is 2 bytes per character, which the renderer requires even for English.&lt;/p&gt;

&lt;p&gt;Six divided by two.&lt;/p&gt;

&lt;p&gt;Hon.&lt;/p&gt;

&lt;p&gt;Tae.&lt;/p&gt;

&lt;p&gt;Emi.&lt;/p&gt;

&lt;p&gt;Three letters wasn't a style decision. It was long division.&lt;/p&gt;




&lt;h2&gt;
  
  
  The finished interface
&lt;/h2&gt;

&lt;p&gt;After all of that, the game became playable without knowing Japanese.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The menus work.&lt;/li&gt;
&lt;li&gt;The locations are readable.&lt;/li&gt;
&lt;li&gt;The profiles make sense.&lt;/li&gt;
&lt;li&gt;The save screen no longer requires guessing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was a different kind of translation.&lt;/p&gt;

&lt;p&gt;The dialogue was about language, the UI was about archaeology.&lt;/p&gt;

&lt;p&gt;Finding out what assumptions the original developers made, and then trying to work within them.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you want to try this yourself
&lt;/h2&gt;

&lt;p&gt;The latest version of the GitHub repo is here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/joebuckle-dev/sentimental-graffiti-translation-claude" rel="noopener noreferrer"&gt;https://github.com/joebuckle-dev/sentimental-graffiti-translation-claude&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The part I had not counted on
&lt;/h2&gt;

&lt;p&gt;Now then, the script, executable, fonts and graphics are only part of the disc.&lt;/p&gt;

&lt;p&gt;The rest is audio.&lt;/p&gt;

&lt;p&gt;I wasn't going to bother with the he voices. But I am quite a ways down this rabbit hole now and I'm wondering if I'd see light by at least attempting to get AI to do these voices for me, with my English text.&lt;/p&gt;

&lt;p&gt;Literally hundreds of megabytes of voices. 426 of them, to be exact, which is very nearly three quarters of the disc.&lt;/p&gt;

&lt;p&gt;Twelve characters and over twelve hours of spoken Japanese.&lt;/p&gt;

&lt;p&gt;That's next...&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>gamedev</category>
      <category>reverseengineering</category>
    </item>
    <item>
      <title>Translating a 1998 Japanese Visual Novel on Sega Saturn I Bought by Accident</title>
      <dc:creator>Joe Buckle</dc:creator>
      <pubDate>Sun, 02 Aug 2026 18:51:25 +0000</pubDate>
      <link>https://dev.to/joebuckle-dev/no-one-asked-for-this-translating-a-1998-japanese-dating-sim-i-bought-by-accident-2m1</link>
      <guid>https://dev.to/joebuckle-dev/no-one-asked-for-this-translating-a-1998-japanese-dating-sim-i-bought-by-accident-2m1</guid>
      <description>&lt;p&gt;This is part 1 of a 3 part series, probably...&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;Title&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;Translating a 1998 Japanese Visual Novel on Sega Saturn I Bought by Accident&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/joebuckle-dev/translating-a-japanese-sega-saturn-game-from-1998-the-user-interface-4o8"&gt;Translating a Japanese Sega Saturn Game From 1998: The User Interface&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;...incoming&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is all new to me.&lt;/p&gt;

&lt;p&gt;I haven’t done this kind of reverse engineering before, I don’t read Japanese, and I didn’t set out to translate a Sega Saturn game.&lt;/p&gt;

&lt;p&gt;I just bought something I didn’t understand and kept going long after it would have been reasonable to stop.&lt;/p&gt;

&lt;p&gt;There’s a trader at my local market who’d just got back from a trip to Japan.&lt;/p&gt;

&lt;p&gt;He doesn’t do it regularly. This was a one-off as far as I can tell. He had come back with boxes of games: SNES, N64, PS1, all of it in Japanese.&lt;/p&gt;

&lt;p&gt;Most of it had already been picked through by the time I got there.&lt;/p&gt;

&lt;p&gt;What was left were two Sega Saturn games.&lt;/p&gt;

&lt;p&gt;I assumed someone else had already taken anything worth having.&lt;/p&gt;

&lt;p&gt;I bought them anyway.&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%2Fq09td3u73ul21gb8sspi.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%2Fq09td3u73ul21gb8sspi.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One of them was &lt;strong&gt;Sentimental Graffiti (1998)&lt;/strong&gt;, what I later learned is a dating sim.&lt;/p&gt;

&lt;p&gt;The game never left Japan, it isn’t especially well regarded (I don't think?), and I don’t have any particular interest in the genre.&lt;/p&gt;

&lt;p&gt;Naturally, I tried to play it.&lt;/p&gt;

&lt;p&gt;I rummaged through boxes until I found my old Sega Saturn, dug out the cables, and dusted off the Trinitron CRT that had been sitting around waiting for exactly this sort of unnecessary exercise.&lt;/p&gt;

&lt;p&gt;There was something oddly satisfying about putting a 1998 Japanese game into the original hardware it was made for.&lt;/p&gt;

&lt;p&gt;Obviously I couldn’t read a single thing.&lt;/p&gt;

&lt;p&gt;The first thing the game asks you to do is enter your name.&lt;/p&gt;

&lt;p&gt;Every label on the screen is in Japanese, including the instructions for using the grid.&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%2F08ktc454ymmj1zj9oq0e.jpeg" 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%2F08ktc454ymmj1zj9oq0e.jpeg" alt=" " width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It goes without saying, this is as far as I got 😂.&lt;/p&gt;

&lt;p&gt;At that point, the sensible thing would have been to stop.&lt;/p&gt;

&lt;p&gt;Instead, I realised how much dialogue the game would likely contain and wondered whether it would be possible to extract it and translate it instead.&lt;/p&gt;

&lt;p&gt;It's not something I would have considered in the past, but with AI now available to assist us, I thought this would be an interesting experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Let Claude loose on the disc
&lt;/h2&gt;

&lt;p&gt;This genuinely surprised me.&lt;/p&gt;

&lt;p&gt;There isn’t a toolchain for this game. No documentation, no format notes, nothing describing how the data is organised.&lt;/p&gt;

&lt;p&gt;Normally, this is where you would start digging through forums, hunting for old reverse engineering notes, writing scripts, and slowly building up an understanding of the file formats.&lt;/p&gt;

&lt;p&gt;Instead, I copied the contents of the disc onto my Linux machine and asked Claude to start looking through it.&lt;/p&gt;

&lt;p&gt;I honestly didn’t expect much.&lt;/p&gt;

&lt;p&gt;But Claude started finding structure.&lt;/p&gt;

&lt;p&gt;It identified blocks of data that looked like text, repeated patterns that appeared to be framing information, and encodings that were likely to be Shift-JIS.&lt;/p&gt;

&lt;p&gt;If you know what Shift-JIS is, congratulations. You are already much further down this particular rabbit hole than I was.&lt;/p&gt;

&lt;p&gt;I had never heard of it.&lt;/p&gt;

&lt;p&gt;It turns out Shift-JIS is a character encoding that was widely used for Japanese text. In simple terms, it is a set of rules that tells a computer how to turn stored bytes into readable characters.&lt;/p&gt;

&lt;p&gt;The game doesn’t store text as something obvious like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hello, my name is...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It stores numbers that need to be interpreted correctly before they become the Japanese characters you see on screen.&lt;/p&gt;

&lt;p&gt;Without understanding that mapping, the dialogue is just a pile of meaningless bytes.&lt;/p&gt;

&lt;p&gt;From there Claude began separating large portions of the script from the rest of the disc contents.&lt;/p&gt;

&lt;p&gt;It reduced the problem from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Understand everything on this disc"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Find the parts that look like dialogue"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fact that I could copy a Japanese Sega Saturn disc onto a Linux machine, point an AI at it, and have it start making sense of the contents was, frankly, a little ridiculous.&lt;/p&gt;

&lt;p&gt;And that was the moment this particular idea started feeling possible, and the rabbit warren I was about to disappear into began to reveal itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Find the letters
&lt;/h2&gt;

&lt;p&gt;The game carries its own font - every character it is capable of drawing, stored as raw pixels with no header and nothing to say which picture belongs to which character.&lt;/p&gt;

&lt;p&gt;Before you can change any text, you have to work out that mapping.&lt;/p&gt;

&lt;p&gt;Claude worked it out, and we tested it in the most direct way available: take the five most common Japanese characters in the entire script, redraw them as big obvious Latin letters, rebuild the disc, and see what the console does.&lt;/p&gt;

&lt;p&gt;The screen we tested it on was, appropriately enough, the one that had stopped me on the first day: the name-entry grid.&lt;/p&gt;

&lt;p&gt;I turned the disc back on, reached the same screen that had been completely unreadable a few days earlier, and it came up on the CRT looking like this:&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%2Fo5tk5phcvvxhwv4shaz6.jpeg" 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%2Fo5tk5phcvvxhwv4shaz6.jpeg" alt=" " width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;B&lt;/code&gt;, a &lt;code&gt;D&lt;/code&gt; and a &lt;code&gt;C&lt;/code&gt; sitting exactly where three Japanese characters should be.&lt;/p&gt;

&lt;p&gt;It is not much to look at - but it meant the font mapping was correct, the disc could be rebuilt, and the Saturn was drawing something I had changed!&lt;/p&gt;

&lt;p&gt;A 30(odd)-year-old console, running a game that had never been designed to display English, was now showing my characters on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Pull the script out into JSON
&lt;/h2&gt;

&lt;p&gt;This was the point where the disc stopped looking like a mysterious black box.&lt;/p&gt;

&lt;p&gt;I knew, obviously, that games were made from code and data. I have been writing software for a long time.&lt;/p&gt;

&lt;p&gt;What I had never done was take a finished game from 1998 and start pulling apart the layers inside it.&lt;/p&gt;

&lt;p&gt;The script turned out to be stored in plain Shift-JIS, wrapped inside a small repeating structure in the game's bytecode.&lt;/p&gt;

&lt;p&gt;If that sentence makes you think "what the heck does that mean?", you are not alone.&lt;/p&gt;

&lt;p&gt;The important bit was that we could now extract it.&lt;/p&gt;

&lt;p&gt;The dialogue could be pulled out into ordinary JSON files. One file per script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;21,321 records in total.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's what the opening conversation looks like after extraction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COMMON.SCR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"records"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"i"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dialogue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"jp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$C：ご、ごめん、沢渡…$n　　　　　その、最近あまり会いにこれなくて"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"i"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dialogue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"jp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ほのか　：別に…　あなたに会えなくたって$n　　　　　私、なんとも思わないもん"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"voice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VO_HON0"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"i"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dialogue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"jp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ほのか　：嘘…$n　　　　　ホントはちょっと寂しかった"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"voice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VO_HON0"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;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 few things that I learned along the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$C&lt;/code&gt; is the player's name. The game substitutes it at runtime, so it appears wherever the protagonist speaks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;$n&lt;/code&gt; is a line break. The spaces after it are the original developers manually aligning the dialogue underneath the speaker's name.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;voice&lt;/code&gt; is the sound clip that plays with the line.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The voice information turned out to be particularly interesting.&lt;/p&gt;

&lt;p&gt;The filenames on the disc are not always telling the truth.&lt;/p&gt;

&lt;p&gt;Every dialogue file actually contains the person after the one named in the filename.&lt;/p&gt;

&lt;p&gt;That was discovered because Claude's translator looked at one of the extracted lines and said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"That isn't Honoka. That's Taeko in Aomori."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The voice files agreed.&lt;/p&gt;

&lt;p&gt;The translations are kept separately from the original Japanese:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COMMON"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"variant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$C: S- Sorry, Sawatari... I know I haven't been able to come see you much lately"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Honoka: It's fine... It's not like I miss you when you're not around or anything"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Honoka: That's a lie... The truth is, I was a little lonely"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping these separate turned out to be one of those things that seemed like a technical detail at the time, but became really important.&lt;/p&gt;

&lt;p&gt;The Japanese extraction is the source material. It can always be regenerated from the disc.&lt;/p&gt;

&lt;p&gt;The English is just another layer on top.&lt;/p&gt;

&lt;p&gt;That means a completely different translation, a different writing style, or even a full rewrite of the dialogue could exist alongside it without touching the original extraction.&lt;/p&gt;

&lt;p&gt;The English is stored as plain text. The line wrapping is handled later when the game is built.&lt;/p&gt;

&lt;p&gt;That turned out to be a very lucky decision for reasons that became obvious in the next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: English is too wide
&lt;/h2&gt;

&lt;p&gt;The game assumes every character fits into a fixed square.&lt;/p&gt;

&lt;p&gt;That works fine for Japanese.&lt;/p&gt;

&lt;p&gt;It doesn’t for English.&lt;/p&gt;

&lt;p&gt;The dialogue box supports 21 characters × 3 lines, and the renderer treats each character as the same width. So instead of flowing text, English gets forced into rigid blocks.&lt;/p&gt;

&lt;p&gt;I wasn’t going to play through the entire game just to find overflows, so I built a previewer.&lt;/p&gt;

&lt;p&gt;It renders the translated text using the game’s font and layout rules, then flags anything that spills over.&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%2Fem8pi74yr24sg0l7x60p.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%2Fem8pi74yr24sg0l7x60p.png" alt=" " width="784" height="390"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Every red section is text that no longer fits inside the original dialogue box.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The results weren’t great.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1,016 of 3,663 dialogue boxes overflowed&lt;/strong&gt; - about &lt;strong&gt;27% of the game&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At first glance, it looks like a translation problem.&lt;/p&gt;

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

&lt;p&gt;The issue is spacing.&lt;/p&gt;

&lt;p&gt;The original renderer treated every character as a fixed-width square. That works perfectly when every character is roughly the same size.&lt;/p&gt;

&lt;p&gt;Japanese:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;す み ま せ ん&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;English:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;S o r r y&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Each English letter is being given a full character cell. So a short word like “sorry” burns through five.&lt;/p&gt;

&lt;p&gt;The game doesn’t need shorter text.&lt;/p&gt;

&lt;p&gt;It needs variable-width characters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Making the letters narrower
&lt;/h2&gt;

&lt;p&gt;Fixing this meant changing the game's own code.&lt;/p&gt;

&lt;p&gt;Somewhere inside the Saturn executable was the routine responsible for drawing a character and moving the cursor ready for the next one.&lt;/p&gt;

&lt;p&gt;So Claude disassembled the executable and started looking for it.&lt;/p&gt;

&lt;p&gt;The disassembly revealed something important: the font was actually &lt;strong&gt;24×12 pixels&lt;/strong&gt;, stored in a different format than we first expected.&lt;/p&gt;

&lt;p&gt;The original CRT test had appeared to confirm the earlier assumption. We had changed glyph data, rebuilt the disc, and watched the Saturn draw the result.&lt;/p&gt;

&lt;p&gt;The chain worked, but the assumption underneath it was wrong.&lt;/p&gt;

&lt;p&gt;The test proved we could change the font. It didn’t prove we fully understood the font.&lt;/p&gt;

&lt;p&gt;Once the drawing code was understood, the fix was surprisingly small.&lt;/p&gt;

&lt;p&gt;A handful of bytes changed how far the cursor moved after each character - moving half a character width instead of a full one - and allowed twice as many characters per line.&lt;/p&gt;

&lt;p&gt;That almost solved it.&lt;/p&gt;

&lt;p&gt;Loaded into the emulator, the letters were finally the right size and the spacing was correct.&lt;/p&gt;

&lt;p&gt;But every line of dialogue still stopped halfway across the box.&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%2Fyk43rd09obrv52ln9vvq.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%2Fyk43rd09obrv52ln9vvq.png" alt=" " width="799" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The letters are right. The line isn't. The whole right half of that box is doing nothing.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The patch had changed how wide the letters were. It hadn't changed the code deciding where lines break.&lt;/p&gt;

&lt;p&gt;That was still wrapping English at 21 characters — because 21 was the correct limit when the box could only hold 21.&lt;/p&gt;

&lt;p&gt;It holds twice that now.&lt;/p&gt;

&lt;p&gt;This is where storing the English without any line breaks paid off. It looked like a fussy detail at the time, but it meant the wrapping could be fixed during the build process instead of rewriting thousands of translated lines.&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%2Fk11h5ofvwo9yijgvga4n.jpeg" 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%2Fk11h5ofvwo9yijgvga4n.jpeg" alt=" " width="798" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Same text, same box, using all of it.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The overflow measurement that previously showed &lt;strong&gt;27%&lt;/strong&gt; of boxes failing now shows &lt;strong&gt;0.4%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fifteen boxes in the entire game still need attention, and those are simple text adjustments.&lt;/p&gt;

&lt;p&gt;There was also one more useful discovery.&lt;/p&gt;

&lt;p&gt;Could longer dialogue simply continue into another text box?&lt;/p&gt;

&lt;p&gt;It turns out the game already knows how to do that.&lt;/p&gt;

&lt;p&gt;Consecutive lines naturally chain into another box without needing any extra code.&lt;/p&gt;

&lt;p&gt;So if a translation needs more space, the build process can split the line automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Translating all of it
&lt;/h2&gt;

&lt;p&gt;With the last technical unknown out of the way, all that was left was volume.&lt;/p&gt;

&lt;p&gt;Thirty-seven script files. Every line of dialogue, every choice menu, every location label.&lt;/p&gt;

&lt;p&gt;Every batch went through a validator before it was allowed back in. Control codes counted, choice menus checked for the right number of options, nothing over the box budget.&lt;/p&gt;

&lt;p&gt;The only Japanese left is 105 records belonging to the developers' own debug menu, which no player will ever see.&lt;/p&gt;

&lt;p&gt;I want to be careful with that sentence, though, because for a while I was quietly reading it as "the game is translated".&lt;/p&gt;

&lt;p&gt;It isn't. The script is the dialogue. The dialogue is not all of the text.&lt;/p&gt;

&lt;p&gt;I'll come back to that, and I promise it's funnier than it sounds.&lt;/p&gt;

&lt;p&gt;The problems that showed up at this scale were not the ones I expected.&lt;/p&gt;

&lt;p&gt;Nobody warns you that English runs out of ways to giggle.&lt;/p&gt;

&lt;p&gt;Twelve people share a handful of Japanese laugh sounds between them. By the seventh character, every natural English spelling had already been taken by somebody else.&lt;/p&gt;

&lt;p&gt;"Heehee". "Hehe". "Heheh". "Hehehe". "Ahaha". "Haha". "Teehee".&lt;/p&gt;

&lt;p&gt;Four translators working on the same person at the same time each invented a different laugh, and every one of them was a perfectly sensible decision made in isolation.&lt;/p&gt;

&lt;p&gt;So there is now a laugh registry in the style guide. An actual table, of who laughs how, that has to be consulted before anyone is allowed to invent a new giggle.&lt;/p&gt;

&lt;p&gt;I did not have "maintain a giggle database" on the list of things this project would involve.&lt;/p&gt;

&lt;p&gt;Then there was the thing you can only notice by translating every route at once.&lt;/p&gt;

&lt;p&gt;The entire premise of the game is a single unsigned letter that says "I want to see you", and the whole journey is about working out which of the twelve people sent it.&lt;/p&gt;

&lt;p&gt;Every single route reveals its own person as the sender.&lt;/p&gt;

&lt;p&gt;Two of them even share the same scene, word for word in the Japanese.&lt;/p&gt;

&lt;p&gt;A player in 1998, finishing one route, would simply have learned that it was her. Reading all twelve at once is the only way to find out that the game's central mystery was never meant to have an answer.&lt;/p&gt;

&lt;p&gt;Twenty-eight years, and possibly nobody noticed, because who would sit down and read all twelve?&lt;/p&gt;

&lt;p&gt;Well. Quite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Two seconds of the opening movie
&lt;/h2&gt;

&lt;p&gt;Then the translated script had to go back onto a disc, and the disc said no.&lt;/p&gt;

&lt;p&gt;The English script is bigger than the Japanese one it replaced, and a 1998 disc has no spare room on it. The files sit end to end, in the order the game expects to find them, with nothing wasted anywhere.&lt;/p&gt;

&lt;p&gt;Everything after the script had to shuffle along to make space, and even then it didn't quite fit.&lt;/p&gt;

&lt;p&gt;So the opening movie gave up half a megabyte out of 33, which works out at roughly &lt;strong&gt;two seconds off the end of the intro&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I did not expect this project to involve cutting two seconds off a video to make room for words.&lt;/p&gt;

&lt;p&gt;But it's a decent illustration of what a finished 1998 CD actually is. A fixed amount of physical space, entirely spent, by people who had no idea that somebody would be back twenty-eight years later asking for a bit more.&lt;/p&gt;

&lt;p&gt;Sorry about your ending, whoever animated that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8: The game reviewed our translation
&lt;/h2&gt;

&lt;p&gt;The first complete English build put a message on screen that we had not written.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;「インフォ窓のメッセージが溢れました」&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;The info window's message has overflowed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is not our error handling.&lt;/p&gt;

&lt;p&gt;That is the original developers' own assertion, left in the retail game, firing for what may well be the first time in its life.&lt;/p&gt;

&lt;p&gt;The info window is the small box that displays location names. It was sized for Japanese labels of at most nine characters.&lt;/p&gt;

&lt;p&gt;Ours included &lt;strong&gt;"Hiroshima Museum of Contemporary Art"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Thirty-six.&lt;/p&gt;

&lt;p&gt;Ninety-four location labels got shortened. The longest is now twelve characters, along the lines of "Bot. Garden" and "Mt. Hakodate", so they read like map pins, which is roughly what the Japanese ones were doing anyway.&lt;/p&gt;

&lt;p&gt;A debug check written in 1998 caught a defect in a translation written in 2026, and told us exactly what was wrong, in Japanese, the moment we broke it.&lt;/p&gt;

&lt;p&gt;We had been carefully validating every line against the dialogue box, because that was the box we knew about.&lt;/p&gt;

&lt;p&gt;Nobody had mentioned that the info window was a completely different and much smaller thing.&lt;/p&gt;

&lt;p&gt;Nobody except the game, which had been sitting on that error message for twenty-eight years waiting for somebody to be daft enough to trigger it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbh8z0bt8ko4ckoj29sqg.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%2Fbh8z0bt8ko4ckoj29sqg.png" alt=" " width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;The debug message&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 9: On the actual console
&lt;/h2&gt;

&lt;p&gt;Everything up to this point had been proven on a computer.&lt;/p&gt;

&lt;p&gt;The emulator is an enormous convenience. Rebuild the disc, reset, and you're looking at the result in seconds instead of burning for twenty minutes and walking across the room.&lt;/p&gt;

&lt;p&gt;But it is still a program agreeing with another program.&lt;/p&gt;

&lt;p&gt;The console is the part that doesn't care what you meant.&lt;/p&gt;

&lt;p&gt;So: burn the rebuilt disc, carry it over to the Saturn, put it in, and try not to hover.&lt;/p&gt;

&lt;p&gt;It works.&lt;/p&gt;

&lt;p&gt;The script, in English, running off a disc, on the original 1998 hardware, on the same CRT that had beaten me at the name-entry screen a few weeks earlier.&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%2Fuhrvhyksk5lbql12zmhx.jpeg" 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%2Fuhrvhyksk5lbql12zmhx.jpeg" alt=" " width="799" height="450"&gt;&lt;/a&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%2Ff2go72vkw2d4gbdkbzer.jpeg" 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%2Ff2go72vkw2d4gbdkbzer.jpeg" alt=" " width="799" height="450"&gt;&lt;/a&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%2Fhh2v90z856hz82tz4at3.jpeg" 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%2Fhh2v90z856hz82tz4at3.jpeg" alt=" " width="800" height="593"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Photographs of a television, because a screenshot wouldn't have felt like proof.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have been staring at these for considerably longer than is reasonable.&lt;/p&gt;

&lt;p&gt;Everything in this article is in those photographs. The handful of bytes that let letters be narrow. The script that came out as JSON and went back in&lt;/p&gt;

&lt;p&gt;And then you press a button, and half the menu is still in Japanese.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: The text that isn't in the script
&lt;/h2&gt;

&lt;p&gt;This is the bit I had wrong, and seeing it on a television is what corrected me.&lt;/p&gt;

&lt;p&gt;The dialogue is translated but the dialogue was never the whole game.&lt;/p&gt;

&lt;p&gt;Everything the player does &lt;em&gt;between&lt;/em&gt; conversations is interface. Travelling to another city. Taking a part-time job to afford the train fare. Reading a girl's profile. Checking what day it is.&lt;/p&gt;

&lt;p&gt;None of that text lives in the script files at all. It sits inside the game's executable as loose Japanese strings, scattered through the code that uses them.&lt;/p&gt;

&lt;p&gt;There are &lt;strong&gt;1,361 of them, about 13,659 Japanese characters&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I have translated seventeen.&lt;/p&gt;

&lt;p&gt;Those seventeen are the name-entry screen, which I did on the first afternoon, because it was the screen that had stopped me and I was taking it personally.&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%2F4g5mc8ron0liur0qswf5.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%2F4g5mc8ron0liur0qswf5.png" alt=" " width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Obviously I still needed to get past this screen, so hacked this together😂.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything else is untouched. The main menu. The job list, which is far more charming than it needed to be: apple orchard, ski resort, dishwashing, dock work, soba shop, takoyaki stand. Every girl's profile card, with her birthday, star sign, blood type and school.&lt;/p&gt;

&lt;p&gt;And then there's the travel guide.&lt;/p&gt;

&lt;p&gt;I didn't know there was a travel guide. Every location in the game has a written description attached to it, in the register of an actual guidebook. The one for the Saikai Bridge mentions that you can walk across it, but that people afraid of heights might prefer not to.&lt;/p&gt;

&lt;p&gt;Nobody in 1998 had to write that. Somebody did anyway, for a dating sim, and then it sat there unread for nearly three decades until somebody who can't read Japanese counted it.&lt;/p&gt;

&lt;p&gt;The dialogue, it turns out, was the easy text.&lt;/p&gt;

&lt;p&gt;It lives in a container built to hold lines of varying length, so English can be whatever size it likes. That was the whole point of the round-trip test right at the beginning.&lt;/p&gt;

&lt;p&gt;These strings have no container. Each one sits in a fixed hole in the executable with the next one immediately behind it, so a translation has to physically fit in the space the Japanese was using.&lt;/p&gt;

&lt;p&gt;"Part-time Job" is considerably longer than アルバイト.&lt;/p&gt;

&lt;p&gt;So what's left is a slot-by-slot fitting exercise across thirteen thousand characters of menus and travel writing.&lt;/p&gt;

&lt;p&gt;Less glamorous than cracking a script format. Just as necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to try this yourself
&lt;/h2&gt;

&lt;p&gt;There's one file on GitHub, and it's the one I'd actually point at:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/joebuckle-dev/sentimental-graffiti-translation-claude" rel="noopener noreferrer"&gt;https://github.com/joebuckle-dev/sentimental-graffiti-translation-claude&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's &lt;code&gt;CLAUDE.md&lt;/code&gt;, the guide the whole project runs on. Not the tools, not the extracted script, not the translation, and definitely not the disc.&lt;/p&gt;

&lt;p&gt;To be completely clear about that last part: there are no ROMs, no disc images, no BIOS files and no game data of any kind in there, and there never will be. It is a text file about how to think about a problem. Anyone wanting to do this to their own game would need their own disc and their own BIOS, which means owning the thing, which means finding it, which is its own adventure. Mine came from a folding table at a market.&lt;/p&gt;

&lt;p&gt;The guide itself isn't really instructions so much as everything we got wrong, written down so it couldn't happen twice.&lt;/p&gt;

&lt;p&gt;There's a table in it of mistaken conclusions. The disc that turned out not to be damaged. The USB port that was never the problem. The font format that was confidently recorded as verified and was wrong for a day and a half.&lt;/p&gt;

&lt;p&gt;It is essentially a list of things I was sure about and shouldn't have been.&lt;/p&gt;

&lt;p&gt;Which is probably the most transferable lesson here, and has very little to do with Sega Saturns.&lt;/p&gt;

&lt;h2&gt;
  
  
  It isn't finished
&lt;/h2&gt;

&lt;p&gt;I should be straight about where this actually stands, because "I translated a game" is doing a lot of work in that headline.&lt;/p&gt;

&lt;p&gt;The dialogue is done and it plays on the console. The menus, the profiles and the travel guide are all still in Japanese.&lt;/p&gt;

&lt;p&gt;The translation itself has never been played end to end. Twenty-one thousand lines were checked by a validator, not by a human reading them in context, and I am fairly confident there are lines in there that are wrong, or stiff, or coming out of the wrong girl's mouth.&lt;/p&gt;

&lt;p&gt;Two seconds of the opening movie are gone, and they're not coming back.&lt;/p&gt;

&lt;p&gt;A stock Saturn still can't boot it at all, because the copy protection is physically stamped into a pressed disc and there's no way to write it. Mine needs a boot cart to get past that. Everything after that point is the game itself, running our data.&lt;/p&gt;

&lt;p&gt;For the avoidance of doubt: I own the game. Both discs, bought from that market stall, sitting on the shelf next to the console. Everything here came off my own copy, and nothing is going anywhere else. This is a translation of a game I own, for the game I own, played on the hardware it shipped for.&lt;/p&gt;

&lt;p&gt;And there is a whole second disc I haven't touched.&lt;/p&gt;

&lt;p&gt;So it is playable in English, and it is not finished in English, both at the same time. I didn't expect that to be a distinction I'd have to make.&lt;/p&gt;

&lt;p&gt;I bought this game because it was the only thing left on the table.&lt;/p&gt;

&lt;p&gt;I still have no particular interest in dating sims, and I still can't read a word of Japanese.&lt;/p&gt;

&lt;p&gt;But there is a Sega Saturn in the corner of the room playing a game in English that has never been in English, and I now understand every single layer of how it got there.&lt;/p&gt;

&lt;p&gt;Which was not the plan. There was no plan.&lt;/p&gt;

&lt;p&gt;I'll write part two when disc two is done.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>gamedev</category>
      <category>reverseengineering</category>
      <category>programming</category>
    </item>
    <item>
      <title>Hardening an AI coding agent: the failures, and the code that fixed them</title>
      <dc:creator>Joe Buckle</dc:creator>
      <pubDate>Fri, 31 Jul 2026 11:33:04 +0000</pubDate>
      <link>https://dev.to/joebuckle-dev/hardening-an-ai-coding-agent-the-failures-and-the-code-that-fixed-them-g3c</link>
      <guid>https://dev.to/joebuckle-dev/hardening-an-ai-coding-agent-the-failures-and-the-code-that-fixed-them-g3c</guid>
      <description>&lt;p&gt;At &lt;a href="https://univoco.io" rel="noopener noreferrer"&gt;Univoco&lt;/a&gt; we build retrieval-augmented assistants over a customer's own documentation. One of them is a coding agent that writes code for a proprietary document layout engine.&lt;/p&gt;

&lt;p&gt;The useful thing about that engine is that there is effectively nothing about it on the public internet. The model cannot have learned it. Everything the agent knows comes from a private documentation index and the tools we give it to search that index.&lt;/p&gt;

&lt;p&gt;That makes it an unusually clean laboratory. When memorisation is off the table, you get to watch the problem-solving process itself, and watch it fail.&lt;/p&gt;

&lt;p&gt;It failed a lot, and the failures were rarely the ones I expected. What follows is the list, in the order we hit them, with the code that fixed each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Contents
&lt;/h3&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;Failure&lt;/th&gt;
&lt;th&gt;Fix&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;Search tools are literal&lt;/td&gt;
&lt;td&gt;filename matching, construct footprints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;It searches the same thing forever&lt;/td&gt;
&lt;td&gt;five layers of loop detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;One phrasing is a single point of failure&lt;/td&gt;
&lt;td&gt;query inflation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Goal words vs. index words&lt;/td&gt;
&lt;td&gt;a deterministic phrasebook&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;The retrieval tool invented APIs&lt;/td&gt;
&lt;td&gt;self-auditing lookups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Deciding an API is invented&lt;/td&gt;
&lt;td&gt;the checker stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Findings get ignored&lt;/td&gt;
&lt;td&gt;finish gates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;The wording of a correction is load-bearing&lt;/td&gt;
&lt;td&gt;the envelope and the silence trailer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;A deletion that grew the file&lt;/td&gt;
&lt;td&gt;take the generation out&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  1. Search tools are literal
&lt;/h2&gt;

&lt;p&gt;The standard shape is fetch-then-answer: embed the question, pull the top-k chunks, hand them to the model. It works for a great many tasks, and it has one structural problem. The folder gets assembled before anyone has understood the problem, so you are deciding what &lt;em&gt;might&lt;/em&gt; be useful before you know what is &lt;em&gt;needed&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Giving the agent search tools of its own fixes the ordering and immediately produces a different class of bug, because search tools are literal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Content search cannot see filenames
&lt;/h3&gt;

&lt;p&gt;The agent searched every file in the project for &lt;code&gt;json&lt;/code&gt;, found nothing, and offered to create a sample data file. &lt;code&gt;sampleData.json&lt;/code&gt; was sitting in the project. The word it needed was on the front of the folder, not inside it.&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;project_paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iter_project_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# A model hunting "the json"/"the xml" searches by TYPE, and content search can
&lt;/span&gt;    &lt;span class="c1"&gt;# never find a file whose content doesn't mention its own format (live failure:
&lt;/span&gt;    &lt;span class="c1"&gt;# sampleData.json was invisible to pattern "json" and the agent offered to create
&lt;/span&gt;    &lt;span class="c1"&gt;# a file that existed). Names are part of the searchable surface.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;name_hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (filename match)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A grep hit is a line, but the thing you edit is a construct
&lt;/h3&gt;

&lt;p&gt;Asked to remove a feature, the agent searched, found the one matching line, deleted it, and declared success. The five lines above it declared and configured the same object.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;find_in_files&lt;/code&gt; now returns grep-style context plus an automatic &lt;strong&gt;variable footprint&lt;/strong&gt;. A hit on &lt;code&gt;layout.blockStart(box)&lt;/code&gt; also lists &lt;code&gt;var box = new Block()&lt;/code&gt; and every property line of that variable. Only declared variables expand, so globals like &lt;code&gt;layout&lt;/code&gt; never blow the output up.&lt;/p&gt;

&lt;p&gt;The rule it encodes: &lt;em&gt;the construct is all of its lines, not the one that matched.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Neither fix is clever. Both exist because something shipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two layers of tools
&lt;/h3&gt;

&lt;p&gt;There are two layers of tools, and the split explains several of the fixes that follow.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;base tools&lt;/strong&gt; ship with the platform and are deliberately generic. &lt;code&gt;list_files&lt;/code&gt;, &lt;code&gt;read_file&lt;/code&gt;, &lt;code&gt;find_in_files&lt;/code&gt;, &lt;code&gt;write_file&lt;/code&gt;, &lt;code&gt;edit_file&lt;/code&gt;, &lt;code&gt;apply_edit&lt;/code&gt;, &lt;code&gt;delete_lines&lt;/code&gt;: every customer gets them, and not one of them knows anything about anybody's domain.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;client tools&lt;/strong&gt; are authored per customer and live in that customer's own directory. Here that means &lt;code&gt;lookup_docs&lt;/code&gt;, &lt;code&gt;resolve_topic&lt;/code&gt;, &lt;code&gt;find_construct&lt;/code&gt;, &lt;code&gt;check_symbols&lt;/code&gt; and &lt;code&gt;dry_run&lt;/code&gt;, none of which mean anything without this engine's documentation and this engine's object model.&lt;/p&gt;

&lt;p&gt;The mechanic that matters is that &lt;strong&gt;a client tool shadows a base tool of the same name&lt;/strong&gt;. Two do. The client &lt;code&gt;find_in_files&lt;/code&gt; adds something the generic one could not: when a literal search finds nothing, concept words like "block" or "the box" expand to the engine's real symbols, so the agent never has to translate a concept into code before it can search for it. And &lt;code&gt;rag_lookup&lt;/code&gt;, the knowledge lookup itself, is shadowed too, which is what makes the fix in section 5 possible.&lt;/p&gt;

&lt;p&gt;That split is the architecture. The generic layer is table stakes and everyone has it. The layer that knows what a "block" is called in this customer's world is the part that cannot be bought.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two knowledge tools
&lt;/h3&gt;

&lt;p&gt;These two need separating, because the gap between them drives a lot of what follows.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;What it costs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lookup_docs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;full-text search over the indexed documentation, returning the chunks that literally contain the term. No embeddings, no model, no generation.&lt;/td&gt;
&lt;td&gt;milliseconds, and it cannot invent anything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rag_lookup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;semantic retrieval, then a model composes an answer out of whatever came back&lt;/td&gt;
&lt;td&gt;ten to thirty seconds, and the composing step can invent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second one is what most people mean by RAG: retrieve some relevant text, hand it to a model, let the model write the answer. It is the right tool for "how do I build a table", and the wrong tool for "does &lt;code&gt;tableColumnRule&lt;/code&gt; exist", which is the distinction the agent is told to route on:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TWO knowledge tools, and the choice matters. &lt;code&gt;lookup_docs&lt;/code&gt; is ~1000x cheaper. Naming a symbol you already know is a FACT CHECK, not a research question.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That framing looks like a cost optimisation. It turns out to be a correctness one, for reasons section 5 gets to.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. It searches the same thing forever
&lt;/h2&gt;

&lt;p&gt;My favourite failure of the whole project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The agent ran the same knowledge lookup &lt;strong&gt;twelve times in one turn&lt;/strong&gt;, identical arguments each time, and the results themselves contained notes pointing out it had already asked. It made no difference.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The variant version was worse, and no exact-match check can catch it, because the arguments differ every time. One table turn ran &lt;strong&gt;12 steps and 170k input tokens&lt;/strong&gt;, and three of its five knowledge lookups re-confirmed the same three API names. Those names were already in its context, listed in the prompt fragment it had been handed at the start of the turn.&lt;/p&gt;

&lt;p&gt;It was not searching. It was checking something it already knew, at ten to thirty seconds a time. The operator's note in the ledger puts it better than I can:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;once it knows how to do stuff it shouldn't need to keep trying to find it at the rag end&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A human has an internal sense of going in circles. The model does not, and telling it so in the tool output does nothing, because that text is just more context. Worse, on a provider without prompt caching the fixed ~11k of system prompt and tool schemas is re-sent every step, so cost is roughly fixed times steps plus transcript. Fewer steps is the only real lever, and a redundant lookup adds steps.&lt;/p&gt;

&lt;p&gt;The fix is five layers, and &lt;strong&gt;the layering is the point&lt;/strong&gt;: each layer only catches what the one before it structurally cannot see.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: identical calls
&lt;/h3&gt;

&lt;p&gt;A counter in the tool loop, tripping at &lt;code&gt;_MAX_IDENTICAL_CALLS = 4&lt;/code&gt;. Past that the call is not executed, but it still has to be answered, because every tool call in the transcript needs a result.&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="c1"&gt;# Stuck-loop detector: the same call over and over is fixation, not progress.
# Answer it with a stub (every tool_call must be answered) and head for the
# forced final answer instead of burning the rest of the budget.
&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;call_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;call_counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&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;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;call_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_MAX_IDENTICAL_CALLS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;stuck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;tool&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;tool_call_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[not run - you have made this exact call &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;call_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; times this turn]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Layer 2: rewordings within a turn
&lt;/h3&gt;

&lt;p&gt;Reworded questions are distinct argument strings, so the identical-call counter never fires on them. Catching those needs a budget rather than a counter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Constant&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_MAX_LOOKUPS_PER_TURN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;live lookups across all wordings&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;_MAX_LIVE_LOOKUPS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per identical query, then the turn cache answers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Layer 3: rewordings across turns and sessions
&lt;/h3&gt;

&lt;p&gt;This one I like, because the obvious solution is wrong. You could embed every query and compare vectors. We do not, because that is a per-probe cost on the hot path for a problem that plain string handling solves.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Matching is deliberately embedding-free: normalise (lowercase, drop stopwords, light stemming with a double-consonant collapse so splitting to split), token-sort, then exact key hit or a &lt;code&gt;difflib&lt;/code&gt; close match. Deterministic, stdlib, zero per-probe cost, and it catches exactly the reworded-not-rethought queries above. Genuinely different questions stay distinct.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The similarity threshold is &lt;code&gt;0.8&lt;/code&gt;, and the calibration comment is the reason I trust it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the live pair whose only difference was the domain word scores &lt;strong&gt;0.833&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;two genuinely different questions, &lt;code&gt;create table&lt;/code&gt; vs &lt;code&gt;create box&lt;/code&gt;, score about &lt;strong&gt;0.73&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The window between them is the sweet spot, and it was measured rather than guessed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 4: concurrent duplicates
&lt;/h3&gt;

&lt;p&gt;Read-only calls get dispatched together, so three rewordings can all be in flight before any of them returns, and a dedupe that runs on completion never sees them. That needs a leader/follower collapse on the normalised query: the first runs, the rest wait and receive its answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 5: do not ask for what you were already given
&lt;/h3&gt;

&lt;p&gt;None of the above touches the failure this section opened with. A budget caps how much re-asking costs, but the agent re-confirming names it was handed at turn start should not be asking at all.&lt;/p&gt;

&lt;p&gt;Writes to domain code are normally refused unless a knowledge lookup has run in that turn, which is what forces grounding. That gate was also what forced the redundant lookups: the agent had the names, but the rule did not know that.&lt;/p&gt;

&lt;p&gt;So the prompt fragments now declare what they supply. Each construct fragment carries a &lt;code&gt;provides&lt;/code&gt; list, generated rather than hand-written and scoped to one family, so the table fragment declares the table companions and not the rule ones. The task brief unions those into &lt;code&gt;provided_symbols&lt;/code&gt;, and the gate exempts a write whose &lt;code&gt;new X()&lt;/code&gt; constructors are all covered. A write reaching for a construct that was never delivered still grounds, and &lt;code&gt;check_symbols&lt;/code&gt; remains the backstop before anything lands.&lt;/p&gt;

&lt;p&gt;The general shape: a rule that forces good behaviour will also force pointless behaviour unless it can see what has already been satisfied.&lt;/p&gt;

&lt;h3&gt;
  
  
  The counter-lesson
&lt;/h3&gt;

&lt;p&gt;This was the most useful thing in the section. The budget once cut a model off &lt;strong&gt;mid-way through correctly verifying eleven symbols&lt;/strong&gt; it was about to use. It was doing exactly the right thing and we throttled it.&lt;/p&gt;

&lt;p&gt;So budget is now charged per tool rather than per call:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;_MAX_LOOKUPS_PER_TURN&lt;/code&gt; caps LIVE lookups across all wordings; cached serves and cheap tools are free (&lt;code&gt;projects.budget_tools&lt;/code&gt;, default &lt;code&gt;rag_lookup&lt;/code&gt;): a millisecond &lt;code&gt;lookup_docs&lt;/code&gt; fact-check per symbol is diligence we demand, not cost to ration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;If your rate limit punishes the behaviour you are trying to encourage, the limit is wrong, not the behaviour.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One last piece of absurdity, included because it is a genuine class of bug and not just a funny story. An earlier nudge injected into the tool results said &lt;em&gt;"look up the interface's REAL members"&lt;/em&gt;. The model ran that sentence, verbatim, as a semantic search query. Twelve times.&lt;/p&gt;

&lt;p&gt;Nudges now name an action, and only ever suggest exact-name lookups, never a phrase, because anything phrase-shaped in an agent's context is a candidate query.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Query inflation
&lt;/h2&gt;

&lt;p&gt;One user phrasing is a single point of failure for recall. If the question happens to be worded unlike the corpus, retrieval returns nothing useful and the agent concludes the knowledge base is empty rather than that it asked badly.&lt;/p&gt;

&lt;p&gt;So before the lookups run, a forced tool call reformulates the question into several deliberately different queries. Not paraphrases: different &lt;strong&gt;angles&lt;/strong&gt;.&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="n"&gt;_QUERIES_TOOL&lt;/span&gt; &lt;span class="o"&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;name&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;search_queries&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;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;Reformulate the question into optimised knowledge-base search queries using the right &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domain keywords / API terms. For a how-to / implementation / &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;how do I ...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; question, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return 2-3 queries covering different angles (e.g. the core API/object, and a usage &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;example). Return a single query ONLY for a trivial factual lookup (e.g. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;what is X&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="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user message that drives it is where the actual behaviour lives. Almost every clause is there to stop a specific degenerate output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate {max_queries} search queries covering DIFFERENT angles so retrieval is thorough:
1. A focused how-to query for the specific task.
2. A BROAD keyword query - just the core nouns / concepts / terms (e.g.
   'user login session token'), to surface related material the specific query might miss.
3. If more are allowed, add a query aimed at a concrete example.
Do NOT return only a verbatim copy of the question. Only a trivial factual lookup gets a
single query.

Examples (apply the SAME pattern to the question's own domain):
- 'set up user authentication' -&amp;gt; ['how to set up user authentication',
  'authentication login session token configuration']
- 'export a report to CSV' -&amp;gt; ['how to export a report to CSV',
  'CSV export columns rows formatting']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system line is one sentence and does most of the work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You generate DISTINCT knowledge-base search queries using precise keywords / API terms. Never echo the question verbatim.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Without it you get the question back with the word order shuffled, which is the reworded-not-rethought problem from section 2, except now we are paying to generate it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three details that are not obvious
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Each query carries a human label.&lt;/strong&gt; The tool schema requires an &lt;code&gt;about&lt;/code&gt; field alongside the query: &lt;em&gt;"a short, natural human phrase for what this looks up, e.g. 'creating tables with borders'"&lt;/em&gt;. That is not for retrieval, it is for the activity line the user watches. Generating the explanation in the same call as the query is free; deriving it afterwards is another model call or a bad heuristic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The queries are not fanned out by code.&lt;/strong&gt; They are injected back into the prompt as an instruction, so the agent still owns sequencing and can stop early.&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_guided_prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_objs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adaptive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Wrap a question with guidance to search each expanded query, then answer (or dig deeper).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;qlist&lt;/span&gt; &lt;span class="o"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query_objs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_ADAPTIVE_TAIL&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;adaptive&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; Then answer fully.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;[Search the knowledge base for: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;qlist&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expansion without a brake is just a licence to over-search&lt;/strong&gt;, which is section 2 all over again. So &lt;code&gt;_ADAPTIVE_TAIL&lt;/code&gt; ships with it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Then judge the results: if they already answer the question, STOP and answer now, do not run extra, redundant, or tangential searches. Only if a genuine gap remains (or the results reference something specific you still need) search again for exactly that. Aim for the fewest searches that fully cover the question.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is a ceiling of &lt;code&gt;6&lt;/code&gt; regardless of configuration, and &lt;code&gt;max_queries: -1&lt;/code&gt; means the model decides within that ceiling.&lt;/p&gt;

&lt;p&gt;One clarification, since the two get conflated: this is multi-query rewriting, &lt;strong&gt;not HyDE&lt;/strong&gt;. No hypothetical answer document is generated. In a domain the model has never seen, a hypothetical document is a hallucination with extra steps, and embedding it just retrieves whatever is nearest to the invention.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. The agent's words are not the index's words
&lt;/h2&gt;

&lt;p&gt;This is the failure that cost the most and looked the least like a bug.&lt;/p&gt;

&lt;p&gt;The agent phrases things by goal: &lt;em&gt;"display the data in a table"&lt;/em&gt;, &lt;em&gt;"put the XML in a table"&lt;/em&gt;. The corpus is indexed by the engine's own vocabulary: &lt;code&gt;openStream&lt;/code&gt;, &lt;code&gt;xmlDoc&lt;/code&gt;, &lt;code&gt;selectNodes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To a human expert those are obviously the same topic. To a retriever they have almost nothing in common, so the lookups returned nothing, the agent concluded the capability did not exist, and it reached for browser JavaScript instead.&lt;/p&gt;

&lt;p&gt;Missing real content &lt;strong&gt;and&lt;/strong&gt; inventing names is the expensive combination, because the invention then looks like a hallucination and you go and debug the wrong component.&lt;/p&gt;

&lt;p&gt;Three layers, in increasing order of force.&lt;/p&gt;

&lt;h3&gt;
  
  
  A deterministic phrasebook
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;resolve_topic&lt;/code&gt; maps a goal to the real symbols, with no model involved at all:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The agent phrases requests by goal; the docs are indexed by object-model vocabulary. That mismatch made it MISS real content and INVENT names, spiralling. This reads the terminology datagraph (&lt;code&gt;_topics.json&lt;/code&gt;) and hands back the real symbols, a note on the pattern, and the exact next lookup, so the agent searches truth instead of guessing.&lt;/p&gt;

&lt;p&gt;Zero LLM, deterministic. Call it BEFORE &lt;code&gt;rag_lookup&lt;/code&gt; when you don't already know the API names.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The map is mined rather than hand-written:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;human phrasings come from query-pattern tags on the worked examples&lt;/li&gt;
&lt;li&gt;symbols come from a regex over the example code, validated against the authoritative API dictionary&lt;/li&gt;
&lt;li&gt;a curated overlay adds goal aliases for the high-value topics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Matching is stopword-filtered token overlap ranked by hit count. No embeddings anywhere in it.&lt;/p&gt;

&lt;h3&gt;
  
  
  A confidence ladder
&lt;/h3&gt;

&lt;p&gt;Because a phrasebook that is confidently wrong is worse than none. The first version applied full force to any match at all:&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="c1"&gt;# Confidence ladder (live miss: 'footer with page numbers' shared ONE word with the
# layers topic and got the full-strength 'do NOT invent' route, walling the agent off
# from the real constructs). A weak match is a HINT, not a law: strict only when the
# route explains at least half the goal's significant words, or 2+ of them.
&lt;/span&gt;&lt;span class="n"&gt;coverage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;goal&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;coverage&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WEAK MATCH - the goal only loosely fits a mapped topic; treat this as a &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hint, NOT the answer:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A guardrail with a false positive does not fail neutrally. It actively steers the agent away from the answer, and it does it with authority.&lt;/p&gt;

&lt;h3&gt;
  
  
  A de-nouning rewrite
&lt;/h3&gt;

&lt;p&gt;Users phrase goals in their own nouns (&lt;em&gt;"display parts from an XML file grouped by category"&lt;/em&gt;) and documentation indexes mechanisms (&lt;em&gt;"load an XML file, iterate elements into table rows, heading per group"&lt;/em&gt;). One cheap call normalises the retrieval query before the lookup stack sees it, layered outermost so that dedupe and the persistent cache both key on the normalised form and hit more often.&lt;/p&gt;

&lt;p&gt;The constraint on that rewriter is the interesting part:&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="c1"&gt;#: Operations only, never API names: the rewriter half-knows jargon, and an invented
#: symbol in the query would poison retrieval with exactly the hallucination the
#: lookup exists to prevent. API discovery stays retrieval's job.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It ships disabled, incidentally. The doctrine version in the prompt does most of the work and the mechanical version is held in reserve for when doctrine leaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the phrasebook is applied matters more than the phrasebook.&lt;/strong&gt; The terminology hint is prepended to every lookup result, whether or not the model asked for it, and whether or not retrieval returned anything, because a gap is exactly when the real names matter most.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. The tool was the liar
&lt;/h2&gt;

&lt;p&gt;The agent kept writing invented API calls into its work. It would be caught, it would look the correct ones up, and then it would do it again. Seven rounds of this in one session.&lt;/p&gt;

&lt;p&gt;I assumed the model was inventing them. That is the famous failure mode and I never questioned it.&lt;/p&gt;

&lt;p&gt;Then I read what the lookup tool had actually returned, rather than the model's account of what it returned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The retrieval pipeline was generating the invented APIs.&lt;/strong&gt; The style that answers conceptual questions composes an answer from retrieved examples, and that generation step had invented &lt;code&gt;layout.tableCellEnd&lt;/code&gt; and &lt;code&gt;Table.cellSpacing&lt;/code&gt;, neither of which exists, while asserting they came &lt;em&gt;"directly from the example in the provided context"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That is not really a bug, it is what generation does. Ask an obliging model for a worked example and it will produce a worked example, and where the retrieved material runs thin it fills the gap with the most plausible name available. The word "lookup" was what fooled me: I had been treating an endpoint that ends in a generate step as though it returned facts, when the only thing separating it from the agent was that its inventions arrived with a citation attached.&lt;/p&gt;

&lt;p&gt;The model was faithfully copying bad information from the one source it had every reason to trust. Worse, the loop was closed: the check flagged the file, the agent re-ran the same lookup to find the right answer, and got the same poison back.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The debugging rule that came out of it&lt;/strong&gt;, because I lost days to not knowing it: read the actual tool results in the log, not the model's narration of them. A large proportion of what looked like model failures turned out to be a tool feeding it poison.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fix is a client-side shadow that wraps the built-in lookup and audits its output with the same symbol checker the write path uses:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The examples style GENERATES its answers, and generation can invent APIs. A live run returned examples using APIs that do not exist, the agent wrote them, &lt;code&gt;check_symbols&lt;/code&gt; flagged the file, the agent re-ran the same lookup and got the same poison: a perfect loop. This shadow breaks it at the source: run the built-in lookup, then run the SAME symbol audit the write path uses over the returned example, and hand the agent the answer and the corrections in one result, so it learns the example is partly wrong BEFORE writing it, together with the interface's real members to build from instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The second-order bug, which is the one worth stealing
&lt;/h3&gt;

&lt;p&gt;The lookup cache had pickled the poison. Of &lt;strong&gt;137 cached entries, 126 carried warning text&lt;/strong&gt;. Every fuzzy cache hit was re-serving a known-bad answer, forever.&lt;/p&gt;

&lt;p&gt;Flagged answers are now uncacheable, and the cache module has a rule list at the top that reads like scar tissue, because it is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;never cache failures or empties, because those are the answers most legitimately retried, and a wrong fuzzy hit on a cached "returned nothing" is poison&lt;/li&gt;
&lt;li&gt;serve each entry at most once per turn, because a fuzzy match can be wrong and the corpus can change&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  One more thing, and I think it generalises
&lt;/h3&gt;

&lt;p&gt;Once the guardrails were in, we probed the same grounded prompt across three model tiers, all with identical retrieval and identical checks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model tier&lt;/th&gt;
&lt;th&gt;Grounded probe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;frontier&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mid&lt;/td&gt;
&lt;td&gt;2/5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cheap&lt;/td&gt;
&lt;td&gt;0/5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The verdict in the notes: &lt;em&gt;the guardrails make a weak model SAFE (bounced), not GROUNDED.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A gate stack stops bad output reaching the user. It does not turn a model that cannot follow grounding instructions into one that can. Budget for the model tier; do not expect the scaffolding to buy it back.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Deciding that an API is invented
&lt;/h2&gt;

&lt;p&gt;This is the check everything else depends on, and the first version was wrong in an instructive way. It asked an LLM to list the symbols in the agent's code, then full-text searched each one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every step leaned on the thing that was broken: an LLM summarising another LLM's hallucination, and a search whose only possible verdict was "not found", which tells an agent nothing about what to write instead, so it kept the invented call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That second clause is the part I underestimated. &lt;strong&gt;"Not found" is not actionable.&lt;/strong&gt; An agent that receives it has no better move available than the one it already made, so it makes it again.&lt;/p&gt;

&lt;p&gt;The reframe: the documentation is complete and enumerable, so the question is &lt;em&gt;decidable&lt;/em&gt;. Resolve what an object is, then ask whether the member exists on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;layout.tableCellEnd                       -&amp;gt;  layout is Layout    -&amp;gt;  no such member  -&amp;gt;  INVENTED
var cell = new TableCell(); cell.start()  -&amp;gt;  cell is TableCell   -&amp;gt;  check `start`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Where the index comes from
&lt;/h3&gt;

&lt;p&gt;"Complete and enumerable" is carrying a lot of weight in that paragraph, so here is where the index actually comes from. It is not hand-written, and that is the only reason the verdict is trustworthy. A hand-maintained list of what exists is just one more thing that can drift from what exists.&lt;/p&gt;

&lt;p&gt;The member index falls out of the documentation's own structure. Every documented member is its own chunk, titled &lt;code&gt;Interface-member&lt;/code&gt;, so parsing titles yields &lt;code&gt;{interface: {members}}&lt;/code&gt; for the entire surface. Two patterns in the prose supply the rest of what the resolver needs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern in the docs&lt;/th&gt;
&lt;th&gt;What it yields&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Interface-member&lt;/code&gt; chunk titles&lt;/td&gt;
&lt;td&gt;every member of every interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accessed using the global property "layout"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;layout&lt;/code&gt; resolves to &lt;code&gt;Layout&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Parent interface: Block&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;inherited members&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The middle row is what makes receiver typing possible at all. The set of documented globals, the thing that turns &lt;code&gt;layout.x&lt;/code&gt; into a decidable question rather than a guess, is extracted from a sentence the documentation happens to write about itself.&lt;/p&gt;

&lt;p&gt;A second datagraph describes &lt;strong&gt;constructs&lt;/strong&gt; rather than members: how each one is declared, and which calls open and close it. It is generated from the API definitions rather than curated, and the reason is the best argument in the codebase for never hand-writing ground truth:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Hand-curating it is error-prone: the first cut invented a &lt;code&gt;List&lt;/code&gt;/&lt;code&gt;listStart&lt;/code&gt; construct that does not exist.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We hallucinated an API while building the tool whose entire job is catching hallucinated APIs. The generator now derives the pairs mechanically: on a global-reachable interface, a &lt;code&gt;&amp;lt;base&amp;gt;Start&lt;/code&gt;/&lt;code&gt;&amp;lt;base&amp;gt;End&lt;/code&gt; pair whose Start takes a defined interface as its first argument. Pairs whose first argument is a primitive, like &lt;code&gt;keepStart(string)&lt;/code&gt;, are not constructs anyone instantiates, so they are skipped and reported rather than guessed at. Seven constructs survive that test.&lt;/p&gt;

&lt;p&gt;An entry looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"interface"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paired"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aliases"&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="s2"&gt;"table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grid"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"declare"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new Table"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"layout.tableStart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"layout.tableEnd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"no_close"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rows and cells end IMPLICITLY. There is NO tableCellEnd and NO tableRowEnd; those methods DO NOT EXIST."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last field is the interesting one, because it is a &lt;strong&gt;negative&lt;/strong&gt; fact. Some constructs close implicitly, so the end-calls a developer would reasonably expect are absent, and &lt;code&gt;tableCellEnd&lt;/code&gt; is precisely what the model kept reaching for. The fact is authored once and read by both the checker and the terminology builder, so the two can never drift into disagreeing about it.&lt;/p&gt;

&lt;p&gt;It also upgrades the finding. Instead of "no such member", which sends the agent hunting for a replacement that does not exist, the checker can say the call is unnecessary and the row closes on its own, so deleting it loses nothing. A checker that only knows what is absent can report; one that knows &lt;em&gt;why&lt;/em&gt; it is absent can tell the agent what to do instead.&lt;/p&gt;

&lt;p&gt;With the index in place, three further properties are what make the check survivable in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  Precision over coverage
&lt;/h3&gt;

&lt;p&gt;A member is only judged when the receiver's type is certain: a documented global, a &lt;code&gt;new X()&lt;/code&gt;, or an interface named directly. A parameter, a function's return value, a nested property, all skipped.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;a wrong accusation is worse than a miss: it sends the agent off to "fix" working code, and it teaches the user to ignore the check.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Eight check families, each of which exists because something landed
&lt;/h3&gt;

&lt;p&gt;A member check alone is not enough, and every gap here was found in production rather than in design.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What landed&lt;/th&gt;
&lt;th&gt;Why it slipped the previous checks&lt;/th&gt;
&lt;th&gt;Check added&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;engine.loadXML(...)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;undeclared receiver, so no type to resolve&lt;/td&gt;
&lt;td&gt;object check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loadXMLData()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a bare call defined nowhere&lt;/td&gt;
&lt;td&gt;function check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;const&lt;/code&gt;, arrow functions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;node --check&lt;/code&gt; passes what the target engine cannot run&lt;/td&gt;
&lt;td&gt;syntax check (ES3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;layout.blockEnd({ borderColor: 'red' })&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;an invented key inside an object literal, not a dotted member&lt;/td&gt;
&lt;td&gt;option check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;new ActiveXObject("Microsoft.XMLDOM")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;member check cannot type an unknown constructor&lt;/td&gt;
&lt;td&gt;constructor check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Object.keys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;valid JS, absent from an ES3-era engine&lt;/td&gt;
&lt;td&gt;engine check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;a referenced data file that does not exist&lt;/td&gt;
&lt;td&gt;not a symbol at all&lt;/td&gt;
&lt;td&gt;file check&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;option check&lt;/strong&gt; is the one I would not have thought of. An invented member on a dotted call is obvious. An invented &lt;em&gt;key&lt;/em&gt; smuggled through an options object is invisible to a member check, and at runtime it is silently ignored, so the behaviour you promised the user simply never happens and nothing anywhere reports an error.&lt;/p&gt;

&lt;h3&gt;
  
  
  A checker's false positive is worse than a miss
&lt;/h3&gt;

&lt;p&gt;We learned this twice, expensively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Once by accident.&lt;/strong&gt; A markdown fence in a knowledge-base snippet tripped a template-literal check and a real API was branded invented. The model then wrote that into its session memory: it learned that a real API does not exist, and carried that forward.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Once systematically.&lt;/strong&gt; The target engine implements a genuine W3C DOM, and our browser-API denylist had been written on the assumption that DOM methods were browser-only. Of the 34 names on it, &lt;strong&gt;23 were real APIs in this engine&lt;/strong&gt;. The check had been flagging the agent's &lt;em&gt;correct&lt;/em&gt; code, and it had helped cause the spiral in section 5.&lt;/p&gt;

&lt;p&gt;Every new check now ships with a known-good-input case for both shapes it audits, whole files and fenced snippets.&lt;/p&gt;

&lt;h3&gt;
  
  
  The runtime layer
&lt;/h3&gt;

&lt;p&gt;Static analysis has a hard limit: it must skip any receiver whose type is uncertain, which is precisely where dynamic code lives. So the last check generates a fake engine from the documentation index and actually runs the build against it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The docs are COMPLETE and enumerable. So a fake engine can be GENERATED from them: every documented global and constructor exists; every documented member is a callable, chainable stub; touching an UNDOCUMENTED member on a typed object records an INVENTED finding. &lt;code&gt;loadJS&lt;/code&gt;/&lt;code&gt;importJS&lt;/code&gt; resolve real project files, so wiring is exercised for real.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It catches syntax errors and crashes, loads of files that do not exist, and invented members reached through dynamic flow: parameters and return values, exactly the receivers the static pass must skip. It runs once at the end of a turn, never per write, because one process launch per draft is cheap and one per write is not.&lt;/p&gt;

&lt;p&gt;The limits are stated in the same docstring, which is the part I would want if I were adopting this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What it can NOT prove: rendering. Data APIs return permissive stubs (no real XML), so loops may run zero iterations and zero-iteration paths go unexercised. This is a dry run, "the code executes against the documented surface", not a render test.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  7. A finding that does not block is decoration
&lt;/h2&gt;

&lt;p&gt;I built the checker, wired it in, and watched it work. It caught the invented APIs and reported them accurately, and nothing whatsoever happened as a result:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Told "these APIs do not exist, fix them now", the model wrote the user a prose essay ("you may need to rely on...") and finished the turn with the invented symbols still on disk. So findings also gate the END of the turn: a &lt;code&gt;finish_gate&lt;/code&gt; the tool loop consults before accepting a final answer, which pushes the model back to work while anything is outstanding (same lesson as &lt;code&gt;lookup_before_write&lt;/code&gt;: enforce it, don't ask for it).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The gate contract fits in one line:&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="nf"&gt;gate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returning a string means the draft was &lt;strong&gt;not sent&lt;/strong&gt; and that string goes back into the model's context. Returning &lt;code&gt;None&lt;/code&gt; ships it.&lt;/p&gt;

&lt;p&gt;Everything else is about the budget, and the budget is where the design decisions are.&lt;/p&gt;

&lt;h3&gt;
  
  
  Per-gate bounds compound
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Own push budget&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;verify&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bail&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claim&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;checklist&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each is individually reasonable, and together they can bounce one turn six times, which is an unpredictable worst case for latency and cost. So there is a &lt;strong&gt;shared turn budget of 3&lt;/strong&gt; across every gate, and past it the composed gate goes silent and the turn ends with whatever honesty the pushes bought.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which produced the next bug
&lt;/h3&gt;

&lt;p&gt;Twice, every push was consumed by intermediate holds and a false final claim shipped unchallenged. Hence a reserve:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;reserve_final&lt;/code&gt; keeps the last (user-visible) draft policed after the shared budget is spent mid-turn. Past &lt;code&gt;max_pushes&lt;/code&gt;, up to &lt;code&gt;reserve_final&lt;/code&gt; further pushes may be spent, but ONLY on NEW material: a draft whose text materially changed since the last hold, or new deeds. A verbatim doubled-down draft has already ignored this correction once, repeating it buys nothing, so it ends the turn as before.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Refuse before the write, not after
&lt;/h3&gt;

&lt;p&gt;This was the single biggest behavioural improvement:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Auditing after the fact meant asking the model that just invented an API to un-invent it. It would rather explain, annotate, or stand by it. Refused content simply never becomes a file.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Police claims, not just code
&lt;/h3&gt;

&lt;p&gt;The gate that catches the most is the simplest. Asked to split a file, the model created three modules and reported four:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Changes made:
  1. Created xmlLoader.js ...
  2. Created styledBox.js ...
  3. Created tableCreator.js ...
  4. Updated template.js to import and use the new files.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Step 4 never happened. It had planned it and narrated the plan in the past tense. The check is mechanical: past-tense verbs only, scoped to a single line, matched against &lt;code&gt;ctx.files_changed&lt;/code&gt;, which every landed write funnels through.&lt;/p&gt;

&lt;p&gt;The tense restriction is not incidental, and the comment explaining why earns its length:&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="c1"&gt;# PAST TENSE ONLY, and deliberately so.
#   - "I will update template.js" is a plan, not a deed - the future tense must not trip this.
#   - The present tense describes what CODE does, not what the agent did: "template.js already
#     imports xmlLoader.js" and "createTable() creates a new Table" are statements of fact
#     about the project, and reading them as claims would gate honest, read-only answers.
# Only the past tense asserts "I did this", which is the thing the file system can contradict.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scope restriction matters just as much: matching is per line, because a wider window lets one "Created" three bullets up capture every filename mentioned afterwards.&lt;/p&gt;

&lt;p&gt;And the gate declares its own non-goal, which I think is why it works:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This checks only what the model VOLUNTEERS. It does not police silence... A false claim is wrong unconditionally; that is why this gate can be mechanical.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two smaller rules that took real failures to learn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strike caps&lt;/strong&gt; exist because the model can be wrong about how to fix something and must not be trapped re-trying forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Findings are split into introduced and inherited&lt;/strong&gt;, after a user complained: &lt;em&gt;"I only asked it to do 1 simple thing and it did it, but it also started talking about jsonData."&lt;/em&gt; Only a finding that a write in this turn introduced may hold the turn.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  8. How the system talks to itself
&lt;/h2&gt;

&lt;p&gt;Every gate above works by writing a message into the model's own context. Getting the &lt;em&gt;wording&lt;/em&gt; of those messages right turned out to be as load-bearing as the logic that triggers them, and it is the part I have seen discussed least.&lt;/p&gt;

&lt;p&gt;Start with the two constants that open and close every one of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  The envelope
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#: Opens every gate message. Injected corrections must not read as the user speaking, or the
#: model treats its own gated draft as user feedback.
&lt;/span&gt;&lt;span class="n"&gt;ENVELOPE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[automated pre-send check - NOT a message from the user] &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a stylistic choice. Without the marker the model reads its own blocked draft as something the user saw and objected to, and then reasons about it as if a conversation had happened.&lt;/p&gt;

&lt;h3&gt;
  
  
  The silence trailer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#: Closes every gate message. Without it the model apologises to the user for a draft they never
#: saw ("You are correct. I apologise..."), explains itself, and asks permission, turning a silent
#: retry into a wall of confusing chat. The gate is plumbing and must stay invisible.
&lt;/span&gt;&lt;span class="n"&gt;SILENCE&lt;/span&gt; &lt;span class="o"&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;Do not apologise. Do not mention this check, your draft, or any mistake - the user &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;never saw them and will not understand a reference to them. Do not ask permission: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;just do the work. Your reply must read as if you had done it right the first time. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
           &lt;span class="c1"&gt;# Live: a bail push made the model REWRITE a file smaller, deleting footer code
&lt;/span&gt;           &lt;span class="c1"&gt;# that had already passed the symbol check, and surrender in a comment instead.
&lt;/span&gt;           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Never respond to this check by deleting or stripping code that already passed &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;its checks - fix FORWARD (add, complete, correct), never retreat.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every sentence in that string is a bug, and the last one is the strangest: a push meant to stop the agent giving up made it delete working, already-verified code and write an apology in a comment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correction pressure has a direction, and if you do not specify it, the model will sometimes satisfy you by removing the problem rather than solving it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both constants live in one module because they did not, once, and a hand-copied version quietly drifted out of sync.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three of the messages, chosen for what each one teaches
&lt;/h3&gt;

&lt;p&gt;&lt;b&gt;Escalation on repetition&lt;/b&gt; - the second push stops being polite&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SECOND refusal: you have made ZERO writes this turn, the file system is the ground truth
and it shows no change, yet your draft still reports work as done. It is false.

Do ONE of these, nothing else: (1) make the change now with a write tool and then
describe it, or (2) answer exactly what is true: that you changed nothing, and what you
found in the file instead.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The first time the model claims work it did not do, it gets a straightforward correction. This is what it gets if it doubles down.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;Constraining what the model then tells the user&lt;/b&gt; - the budget message&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[lookup budget exhausted - {live} knowledge lookups have run this turn] No further
lookups will run. Searching again, in any wording, returns this same message. STOP
searching and WRITE... In that final answer, if information is missing, say the SEARCH
BUDGET ran out, do NOT claim the knowledge base lacks it; the budget ending says nothing
about what the documentation contains.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The obvious message is "stop searching". The last clause took a live failure to learn: without it, our own rate limit came out of the agent's mouth as a factual claim about the customer's documentation. The agent had no way to know the difference, so we had to tell it.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;A checker that argues&lt;/b&gt; - pre-empting the three moves the model actually makes&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This is not "unverified", the documentation is COMPLETE, and these were checked against
every member of the interface. You invented them just now, most likely because a similar
name exists (tableStart exists, so tableCellEnd feels like it must). It does not. The
file will fail on the real engine.

Rewrite those calls using the real members listed above. Do NOT re-run the same
rag_lookup that produced this code; it will return the same wrong example. If you need a
pattern, rag_lookup a DIFFERENT, more specific goal. Do NOT annotate the call and move
on; do NOT keep it because it looks right. If the interface genuinely has no member for
what you intended, leave that part out and say so in your final answer, never leave a
call to a method that does not exist.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Note that it names the failed retrieval explicitly. Without that line the agent re-runs the lookup that produced the bad code in the first place, which is the closed loop from section 5.&lt;/p&gt;

&lt;h3&gt;
  
  
  The trust boundary between agents
&lt;/h3&gt;

&lt;p&gt;When a plan is decomposed, each item runs as a bounded worker with its own context, and the worker's report comes back to the composer as a user message. Workers are told to end with what they built and what it exposes.&lt;/p&gt;

&lt;p&gt;The composer does not believe the "exposes" line:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;exposures&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&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="n"&gt;content&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="n"&gt;answer&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One line describing what a landed file exposes - MECHANICAL over the file itself
    (the file is a fact; a model-authored &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exposes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; line is a claim). Falls back to the
    worker&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s own short answer for non-JS / no matches.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That parenthesis is the whole article in one sentence. Everywhere the system has a choice between what a model says happened and what the file system says happened, it takes the file system, and where it cannot, it says so and degrades deliberately.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Some mistakes should not be catchable
&lt;/h2&gt;

&lt;p&gt;The fix for this last one runs against every instinct: it is to take intelligence away, not add more.&lt;/p&gt;

&lt;p&gt;Asked to remove a table, the agent removed one line, rewrote everything around it, and made the file &lt;strong&gt;bigger&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attempt&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;original&lt;/td&gt;
&lt;td&gt;1527 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"remove the table"&lt;/td&gt;
&lt;td&gt;1653 bytes, reported as success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retry&lt;/td&gt;
&lt;td&gt;1659 bytes, reported as success&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The diagnosis had two layers, and both are worth knowing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The removal instruction fell into a generative chunk-rewrite path, so "remove" became "rewrite this region, minus the thing".&lt;/li&gt;
&lt;li&gt;The forced pre-write lookup for &lt;em&gt;"how to remove a table"&lt;/em&gt; returned the documentation for &lt;strong&gt;creating&lt;/strong&gt; one, because that is what the corpus has. That poisoned the intent: the agent came away believing you remove a table by calling &lt;code&gt;tableEnd()&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The fix was to take the generation out of that step entirely. The model reads the file and emits &lt;code&gt;L&amp;lt;n&amp;gt;: DELETE&lt;/code&gt; markers, nothing else, and ordinary code drops those lines:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Delete-only pass: the model reads the file (or a &lt;code&gt;where=&lt;/code&gt;-scoped region) and marks which lines belong to the thing being removed; they are dropped deterministically. It can neither rewrite nor add, so a removal can only shrink the file. This is where EVERY pure deletion goes instead of the generative chunk rewrite (which deleted one line and grew the file live).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The same file went from 1527 bytes to &lt;strong&gt;678&lt;/strong&gt;, with the right construct gone and the one next to it untouched.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it all amounts to
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;filename matching, construct footprints&lt;/td&gt;
&lt;td&gt;literal search missing the obvious&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;identical-call breaker, turn budget, fuzzy cache, in-flight collapse&lt;/td&gt;
&lt;td&gt;fixation and rewording loops&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;query inflation with an adaptive brake&lt;/td&gt;
&lt;td&gt;one phrasing being a single point of failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;resolve_topic&lt;/code&gt;, confidence ladder, de-nouning rewrite&lt;/td&gt;
&lt;td&gt;goal words that miss an API-indexed corpus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;self-auditing lookup shadow, uncacheable flagged answers&lt;/td&gt;
&lt;td&gt;the retrieval tool inventing APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;check_symbols&lt;/code&gt; (8 families) + the &lt;code&gt;dry_run&lt;/code&gt; shim&lt;/td&gt;
&lt;td&gt;invented APIs, statically and at runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;composed finish gates, pre-write refusal, claim audit&lt;/td&gt;
&lt;td&gt;findings being ignored, work claimed but not done&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;envelope, silence trailer, escalating pushes&lt;/td&gt;
&lt;td&gt;the correction itself being misread or over-obeyed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delete-scan&lt;/td&gt;
&lt;td&gt;a destructive edit that could grow the file&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Across all of this, the model never got better. Every version ran on the same tier that failed the first time.&lt;/p&gt;

&lt;p&gt;What changed was the space of things it could get away with: what it could search without a limit, what it could claim without evidence, what it could write without being refused, and what it could remove by generating rather than by marking.&lt;/p&gt;

&lt;p&gt;That is unglamorous work, and it is almost all of the work.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>rag</category>
      <category>agents</category>
    </item>
    <item>
      <title>Creating a web API with Lua using Nginx OpenResty</title>
      <dc:creator>Joe Buckle</dc:creator>
      <pubDate>Sun, 02 Feb 2020 16:58:55 +0000</pubDate>
      <link>https://dev.to/joebuckle-dev/creating-an-api-with-lua-using-openresty-42mc</link>
      <guid>https://dev.to/joebuckle-dev/creating-an-api-with-lua-using-openresty-42mc</guid>
      <description>&lt;p&gt;I'm a bit of an old fashioned, pragmatic developer and tend to stick with frameworks and patterns that I know are reliable - a 'stick in the mud' if you will.  &lt;/p&gt;

&lt;p&gt;This dive is deviating from my usual path.&lt;/p&gt;

&lt;p&gt;I had never considered Lua to be a viable language to create a backend for a web application. Of course I'm not totally oblivious to the language itself - I do know it's used mostly for embedding in other software and game engines - but that's it really. &lt;/p&gt;

&lt;p&gt;I came across the &lt;a href="https://openresty.org" rel="noopener noreferrer"&gt;OpenResty&lt;/a&gt; project whilst on a learning journey of &lt;a href="https://www.nginx.com/" rel="noopener noreferrer"&gt;Nginx&lt;/a&gt; web server.&lt;/p&gt;

&lt;p&gt;Writing code directly on the web server rather than passing the request to some interpreter is a very interesting concept for me. &lt;br&gt;
Lua runs directly inside the Nginx worker which means a very small barrier between the webserver and the application code. &lt;br&gt;
Additionally, Lua is known for being immensely fast compared to interpreted languages particularly when using the LuaJIT compiler. &lt;/p&gt;

&lt;p&gt;Nginx is capable of handling a very high number of concurrent connections at a very low memory footprint.   &lt;/p&gt;

&lt;p&gt;However, there doesn't appear to be that much excitement about it. &lt;br&gt;
It's been around for some time (at least since 2016) but it does appear to be lacking in developer adoption. Saying that, some big names are using it, including Cloudflare and Tumbler.&lt;/p&gt;

&lt;p&gt;Despite that - the performance gain from this sort of backend has peaked my interest.  &lt;/p&gt;

&lt;p&gt;The easiest way (perhaps the only way) to get started with this is by installing &lt;a href="https://openresty.org/en/" rel="noopener noreferrer"&gt;OpenResty&lt;/a&gt;. &lt;br&gt;
This provides the Nginx web server with the Lua module.&lt;/p&gt;

&lt;p&gt;If you're familiar with Nginx this example will make sense to you. The location directive is being passed a Lua script as a file. &lt;br&gt;
The output from that script is expected to be in JSON in this configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;location ~ ^/api(.*)$ {
  default_type 'text/json';
  add_header 'Content-Type' 'application/json';
  content_by_lua_file /etc/openresty/sites/api.lua; # &amp;lt; Points to Lua file
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also pass arbitrary code into &lt;code&gt;content_by_lua&lt;/code&gt; and &lt;code&gt;content_by_lua_block&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Now, Lua is a reasonable simple language to pick up if you're used to Ruby or Python. I hadn't used it before but I was still able to quickly write a script that allows creation of API endpoints and parses the body and parameters. &lt;/p&gt;

&lt;p&gt;There is an object you can access in Lua called &lt;code&gt;ngx&lt;/code&gt;. This provides you with the data passed into Nginx from the request such as the body or the path etc...  &lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_method&lt;/span&gt; &lt;span class="c1"&gt;-- POST, GET.. whatever&lt;/span&gt;
&lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_body_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;-- The data passed in from the request&lt;/span&gt;
&lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="c1"&gt;-- The request path&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script I wrote allows me to set the Method and Endpoints that are allowed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="cm"&gt;--[[ api.lua --]]&lt;/span&gt;

&lt;span class="c1"&gt;-- Helper functions&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;strSplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;substr&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;string.gmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"[^"&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;delim&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="k"&gt;do&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;substr&lt;/span&gt; &lt;span class="o"&gt;~=&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;string.len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="nb"&gt;table.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;-- Read body being passed&lt;/span&gt;
&lt;span class="c1"&gt;-- Required for ngx.req.get_body_data()&lt;/span&gt;
&lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_body&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;-- Parser for sending JSON back to the client&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;cjson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"cjson"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;-- Strip the api/ bit from the request path&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;reqPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;"api/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;-- Get the request method (POST, GET etc..)&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;reqMethod&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_method&lt;/span&gt;
&lt;span class="c1"&gt;-- Parse the body data as JSON&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_body_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt;
        &lt;span class="c1"&gt;-- This is like a ternary statement for Lua&lt;/span&gt;
        &lt;span class="c1"&gt;-- It is saying if doesn't exist at least&lt;/span&gt;
        &lt;span class="c1"&gt;-- define as empty object&lt;/span&gt;
        &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;cjson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_body_data&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="n"&gt;Api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Api&lt;/span&gt;
&lt;span class="c1"&gt;-- Declare API not yet responded&lt;/span&gt;
&lt;span class="n"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Function for checking input from client&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="nc"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;-- If API not already responded&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responded&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
        &lt;span class="c1"&gt;-- KeyData = params passed in path&lt;/span&gt;
        &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;keyData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="c1"&gt;-- If this endpoint has params&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;string.find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;(.-)&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="c1"&gt;-- Split origin and passed path sections&lt;/span&gt;
            &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;splitPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strSplit&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="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;splitReqPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strSplit&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="n"&gt;reqPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;-- Iterate over splitPath&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;splitPath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
                &lt;span class="c1"&gt;-- If chunk contains &amp;lt;something&amp;gt;&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;string.find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;(.-)&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;then&lt;/span&gt;
                    &lt;span class="c1"&gt;-- Add to keyData&lt;/span&gt;
                    &lt;span class="n"&gt;keyData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;string.match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&amp;lt;(%a+)%&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;splitReqPath&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                    &lt;span class="c1"&gt;-- Replace matches with default for validation&lt;/span&gt;
                    &lt;span class="n"&gt;reqPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;string.gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reqPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;splitReqPath&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;end&lt;/span&gt;
            &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

        &lt;span class="c1"&gt;-- return false if path doesn't match anything&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reqPath&lt;/span&gt; &lt;span class="o"&gt;~=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;
        &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
        &lt;span class="c1"&gt;-- return error if method not allowed&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reqMethod&lt;/span&gt; &lt;span class="o"&gt;~=&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt;
        &lt;span class="k"&gt;then&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;cjson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="nb"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Method "&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;reqMethod&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt; &lt;span class="s2"&gt;" not allowed"&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

        &lt;span class="c1"&gt;-- Make sure we don't run this again&lt;/span&gt;
        &lt;span class="n"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;responded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;-- return body if all OK&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keyData&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I can call &lt;code&gt;Api.endpoint()&lt;/code&gt; following to create my endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="cm"&gt;--[[ api.lua --]]&lt;/span&gt;
&lt;span class="n"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'POST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/test'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;cjson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'/test/&amp;lt;id&amp;gt;/&amp;lt;name&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ngx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;cjson&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So this has got me started. Obviously the logic inside these endpoints would be much more complex and usually have to connect to databases - all of which &lt;a href="https://github.com/bungle/awesome-resty" rel="noopener noreferrer"&gt;OpenResty supports&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;One thing I did come across was the &lt;a href="https://leafo.net/lapis/" rel="noopener noreferrer"&gt;Lapis Framework&lt;/a&gt; created by &lt;a href="https://leafo.net/" rel="noopener noreferrer"&gt;Leafo&lt;/a&gt; who I know mostly from creating the PHP compilers for &lt;a href="https://leafo.net/lessphp/" rel="noopener noreferrer"&gt;Less&lt;/a&gt; and &lt;a href="https://leafo.net/scssphp/" rel="noopener noreferrer"&gt;SCSS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Github for code the code here - &lt;a href="https://github.com/bambattajb/openresty-api-example" rel="noopener noreferrer"&gt;https://github.com/bambattajb/openresty-api-example&lt;/a&gt;,&lt;/p&gt;

</description>
      <category>lua</category>
      <category>nginx</category>
      <category>openresty</category>
      <category>lapis</category>
    </item>
    <item>
      <title>Embedded UI Components with Preact</title>
      <dc:creator>Joe Buckle</dc:creator>
      <pubDate>Tue, 24 Dec 2019 10:34:14 +0000</pubDate>
      <link>https://dev.to/joebuckle-dev/embedded-ui-components-with-preact-2ik6</link>
      <guid>https://dev.to/joebuckle-dev/embedded-ui-components-with-preact-2ik6</guid>
      <description>&lt;p&gt;As a front-end developer it's quite common to be tasked with creating a new UI component for an application. &lt;/p&gt;

&lt;p&gt;You're given access to some backend API and you have to crack on and build a UI that sits somewhere inside the website as a widget, page or whatever.&lt;/p&gt;

&lt;p&gt;The component is complex enough that you need to manage view states, so you'd quite like a system for that. &lt;br&gt;
You also like using the cleaner ES6 syntax and you're already familiar with React and JSX and all that jazz.&lt;/p&gt;

&lt;p&gt;Now, you don't have access to the applications overarching build tools - you just need to find a way of passing some reasonably sized and efficiently written code over to the application maintainers who will factor it into their monolith 😊. &lt;/p&gt;

&lt;p&gt;I tend to see these UI components as their own small compartmentalised applications but I am fully conscious of the increased size when shipping seemingly unnecessary frameworks in packaged code. &lt;/p&gt;

&lt;p&gt;So React is out of the question because it's &lt;strong&gt;&amp;gt; 30kb&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I eventually discovered &lt;a href="https://preactjs.com/" rel="noopener noreferrer"&gt;Preact&lt;/a&gt; which claimed to come in at &lt;strong&gt;3kb&lt;/strong&gt; and supports all of the Virtual DOM and State Management features of React. &lt;/p&gt;

&lt;p&gt;I totally signed up and was not disappointed. Just set up a Webpack build environment and away you go. &lt;/p&gt;

&lt;h2&gt;
  
  
  How is Preact so small?
&lt;/h2&gt;

&lt;p&gt;React has its own &lt;a href="https://reactjs.org/docs/events.html" rel="noopener noreferrer"&gt;Synthetic Event System&lt;/a&gt; for handling events and Preact uses the browsers native &lt;code&gt;addEventListener&lt;/code&gt;. &lt;br&gt;
This could lead to a user experience penalty. &lt;/p&gt;

&lt;p&gt;React is trying very hard (30kb + hard) to ensure a consistent feel in whatever browser it's being run in; whereas Preact has chosen to use unpredictable browser-native event listeners for a massively reduced packaged size.&lt;/p&gt;

&lt;p&gt;More on the 'subtle' differences here - &lt;a href="https://preactjs.com/guide/v10/differences-to-react#main-differences" rel="noopener noreferrer"&gt;Main Differences&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's missing?
&lt;/h2&gt;

&lt;p&gt;Well, &lt;a href="https://preactjs.com/guide/v8/differences-to-react/#whats-missing" rel="noopener noreferrer"&gt;not much&lt;/a&gt;! &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Proptypes&lt;/code&gt; (they aren't used all the time anyway)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Children&lt;/code&gt; &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Preact is a great solution to my problem but it does look as though it might come with some UI performance trade-offs leading to an inconsistent UX. &lt;/p&gt;

&lt;p&gt;That said, if you chose to build and embedded UI component without a framework you most likely wouldn't create your own synthetic event system for seamlessly handling differences in native events.&lt;/p&gt;

&lt;p&gt;Preact is totally winning for me in this context 👍&lt;/p&gt;

</description>
      <category>react</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I love WordPress, but I don't like its ecosystem</title>
      <dc:creator>Joe Buckle</dc:creator>
      <pubDate>Sat, 21 Dec 2019 15:24:21 +0000</pubDate>
      <link>https://dev.to/joebuckle-dev/i-love-wordpress-but-i-don-t-like-its-ecosystem-36f8</link>
      <guid>https://dev.to/joebuckle-dev/i-love-wordpress-but-i-don-t-like-its-ecosystem-36f8</guid>
      <description>&lt;p&gt;Come gather round friends and I'll tell you a tale&lt;br&gt;
Of when I claimed to be a WordPress guru&lt;br&gt;
But was then made to use off-the-shelf themes and plugins&lt;br&gt;
And that claim I sought to undo&lt;/p&gt;

&lt;p&gt;I've been using WordPress to solve content management problems for clients in the digital publishing industry for around 10 years and know its core features pretty well having spent countless hours traversing through the codex and reading a plethora of problem-solving developer articles.    &lt;/p&gt;

&lt;p&gt;I am an advocate for the software and push it as a low-cost, easy to use, extensible and familiar content editing &amp;amp; publishing platform for digital publishing teams.&lt;/p&gt;

&lt;p&gt;From a developer perspective the core provides a very good and efficient Query Engine, Database Abstraction Layer, Rest API Framework, User / Roles management and loads more.&lt;/p&gt;

&lt;p&gt;It also provides a decent admin interface that is intuitive and adaptable enough that I can tailor it to specific business requirements. &lt;/p&gt;

&lt;h2&gt;
  
  
  What's not to like?
&lt;/h2&gt;

&lt;p&gt;Well, last year I assumed the role of "WordPress Guru". &lt;/p&gt;

&lt;p&gt;Up until then I had been using WordPress as a toolkit for building a unique set of features for my clients. I would be writing a lot of specific business logic around this toolkit just as you would a framework.&lt;/p&gt;

&lt;p&gt;At the time of accepting this role I didn't know which sites I'd be working on and just assumed that, armed with this knowledge, I should be able to handle it right?&lt;/p&gt;

&lt;h2&gt;
  
  
  But I was wrong!
&lt;/h2&gt;

&lt;p&gt;You see, there is, in my opinion, a "Dark Side" to WordPress - its Theme and Plugin culture.&lt;/p&gt;

&lt;p&gt;It's not like I was unfamiliar with this and whenever I'd read up or hear of this culture I thought it was individuals or small businesses on small budgets looking to get a website up as cheap as possible and that it shouldn't apply to serious businesses or organisations. &lt;/p&gt;

&lt;h2&gt;
  
  
  I was wrong again!
&lt;/h2&gt;

&lt;p&gt;I used the term "Dark Side" because I would be dealing with content managers that have a business requirement for something that is more flexible than the feature-restrictive theme or plugin. At some point this system was sold to them either by someone in-house or a developer that is now long gone. &lt;/p&gt;

&lt;p&gt;The first problem is you have a client that doesn't understand "why" this feature is difficult to implement (they're right - it shouldn't be, if this system was built properly). &lt;br&gt;
The second is the developer trying to shoehorn features into this system knowing it's not really fit for purpose thus making their ongoing maintenance work more difficult.&lt;/p&gt;

&lt;p&gt;Sometimes you have to do things like "modify core code" of plugins only to learn of a "major security update" some weeks later that you now have to factor in.&lt;/p&gt;

&lt;p&gt;Sites end up becoming a maintenance and security headache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson learned
&lt;/h2&gt;

&lt;p&gt;I will never claim to be a "WordPress Guru" again as putting me in an environment where I have to build or extend sites that use WooThemes or Divi (for example) then I am lost!&lt;/p&gt;

&lt;p&gt;Someone who is a "WordPress Guru" would understand the entire ecosystem of available WordPress products - not just its core system.  &lt;/p&gt;

&lt;p&gt;My advice for publishers is if you are looking at purchasing a Theme or Plugin to handle a feature you're needing make sure it does EVERYTHING you're ever going to need. &lt;/p&gt;

&lt;p&gt;Otherwise consult a developer - there are some good quality plugins that allow themselves to be extended by using custom hooks and filters, perhaps enough so to fill the void in requirements. &lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>php</category>
    </item>
  </channel>
</rss>
