This article was originally written in Japanese and translated into English with AI assistance. Please note that some expressions may carry nuances from the original Japanese.
🇯🇵 日本語版はこちら / Japanese version
Series: utf8conv Development Journal (Part 9)
On the evening of September 14, 2026, the fixed utf8conv passed its acceptance criteria.
Acceptance criteria are the conditions for saying “it is done,” written out in a form that can be counted. This time there were 18 of them. The fixed code was run and checked against them, and not one failed. When I write “judgment” in this article, I do not mean detecting character encodings; I mean judging whether the criteria were passed.
But before that pass, a hole had already been found — on the side that does the judging.
The judging side assumed only one way of handling the screen. When the screen is handled some other way, it misses it. In fact, even a deliberately inappropriate implementation passed every acceptance criterion that existed at the time.
Last time was the story of how I wrote “it works.” This time is the story of that hole, which sat just before “it passed.”
First, Let Me Set Out Who Does What
Last time, I wrote that I leave both the development record and the article drafts to AI, and that I publish after reading through and correcting them myself. Here, let me explain that division of roles a little.
When I was developing this in March 2026, I was going back and forth between two AI tools.
One is Claude Code, a tool that runs inside the terminal (the screen where you type commands as text) and has AI write code. I had been using its name since the first installment of this series without ever saying what it was.
The other is Claude.ai, the chat version of Claude.
I consulted Claude.ai on how to proceed in general, and had it produce ideas on how to move forward and the actual instructions for implementation. I handed those instructions to Claude Code, had it do the implementation, and had it report the result. I passed the report to Claude.ai, which checked the contents and made a judgment. Sometimes it produced an instruction sheet for corrections; sometimes it gave an OK and we moved on to the next step.
The “separate the core from the GUI” story from the second installment of this series also came up this way — as an idea for the right way to implement things, while I was actually alternating between the two tools.
Time passed, and by September the way the work ran had changed. The work now revolves around a document called a brief. A brief is a document, written before the work is handed to the builder, that says what to build and what to check to count as a pass.
- The role that writes the brief. It writes the brief and checks what comes back. The AI in this role does not write code that goes into the product.
- The role that builds to the brief. It receives the brief and writes code or articles. The Claude Code in this role is what fixed utf8conv’s code.
- The role that reads with a different pair of eyes. An AI other than the one that made them reads the brief and the judging mechanism, and looks for holes.
Every role is held by an AI. The one who decides what to adopt is me.
What Judged the Pass Was a Mock Part
The acceptance criteria were not something a person checks by looking at the screen; they were in a form a machine runs automatically. What ran them was the working machine used by the AI in the role that writes the brief. The mechanism, too, was built by the AI in the role that writes the brief.
That machine did not have tkinter (the part that puts up the screen, which I wrote about in the third installment of this series), so a mock part was made: something that only imitates tkinter’s behavior and receives commands. Nothing appears on any screen. It is a made-up thing that merely records which command wrote what.
In utf8conv, as I wrote last time, the job of drawing the screen and the job of converting files proceed at the same time, in separate flows. The main flow is in charge of the screen; the conversion flow is in charge of converting. Inside the program, the conversion flow is called the worker.
As I wrote in the third installment, the program was built by the practice that the main flow is the only flow allowed to touch the screen’s parts with its own hands. When the conversion flow wants to put something on the screen, there are three ways it can do so.
- Write it by its own hand. The conversion flow bypasses the main flow and writes text into the log area, or changes the look of a button, itself. By command name, this means insert and config. It is the worst way, and the brief forbids it as worse than asking with a message.
- Ask with a message. The conversion flow does not touch the screen’s parts itself; it leaves a message for the main flow: “Later, please display this.” The main flow puts it up when it has a free moment. The
self.after(0, ...)that I described in the third installment as the way of asking “please display this” is this one. “after” is the name of the command for leaving such a message. This is the way my March self used, believing it to be safe. - Stack it up. The conversion flow only stacks what it wants displayed into a box, and the main flow comes by a little at a time, picks it up, and displays it. This is the correct answer, and the current rule.
The mock part had been given just one rule.
“An after() called from the worker thread simply vanishes; nothing happens.”
As I wrote last time, at midday on September 14, only the heading lines came out on my screen. The mock part’s rule is a direct copy of what the screen looked like at midday. That the messages were being silently discarded, without even raising an exception (notice of an error), was confirmed by the small test I wrote about last time — after the mock part had been made.
What Happens When There Is Only One Rule
The mock part discards displays that are asked for with a message. So when the criteria were run against the code before the fix, the displays did not arrive, and it failed, as it should have.
In the code before the fix, the command that leaves a message was written in 5 places. The newest version of the judging tool finds these 5 places and fails them.
But commands that write by their own hand were a different matter: the mock part accepted them as they were. The written text stays in the mock part, and the look of the button changes too. Seen from the judgment’s side, the display arrived properly, and the button came back.
In other words, if you run the criteria against an implementation in which the conversion flow writes to the screen by its own hand, without using messages, the judgment misses it. A way of building it that can behave even less stably than the code before the fix ends up passing.
It was the role that writes the brief that made the mock part. That role had not noticed this hole.
Found by a Different Pair of Eyes
What found the hole was the AI in the role that reads with a different pair of eyes.
At this point, the role that writes the brief was running two reading AIs, separately. What it showed them was the brief and the judging mechanism. The instruction was “Find reasons not to approve.” The viewpoints were split, and one of the two was given the job of attacking the judging mechanism itself.
That one wrote deliberately badly built code of its own, as a sample for testing the check itself. It was not meant to go into the body of utf8conv. It was an implementation in which the conversion flow does not use messages and writes to the screen by its own hand. When the criteria were run against it, every criterion that existed at the time went through.
After the Hole Was Closed, It Passed
The role that writes the brief added one more rule to the mock part.
The added rule was: “If the screen is touched from the conversion flow, record it.” Whether it was asked with a message or written by its own hand, if a record remains, it fails.
This addition, too, turned out to have an omission. The next readers — two AIs reading with a different pair of eyes — both found that dialogs, such as the small window that reports an error, were missing from the record. The role that writes the brief closed that as well.
After the criteria had been fixed, the role that builds to the brief fixed utf8conv’s code. This is the fix I wrote about last time: from asking, to picking up what has been stacked. The role that writes the brief did not take the report “It passed” at face value; it ran the criteria once more itself to check. The result was a pass on all 18.
Let me write the order down once more. The hole is found. The recording rule is added to the mock part, and the hole is closed. Only after that does the fixed code pass all of the criteria.
The code that passed in the end did not slip through the hole. It passed under the judgment made after the hole was closed.
Counting the Times
Because the recording rule was added, it also became possible to count how many times the screen had been touched from the conversion flow. What is counted is not the number of places where the command is written (5 places), but the number of times it was called.
Counted by the judgment, the code before the fix, over the course of running every one of the judgment’s scenes, left 3,590 messages for the screen from the conversion flow. Everything counted was a message. The number of times it wrote by its own hand was 0.
The code after the fix stopped both the messages and the direct writes; it only stacks things up. Counted the same way, the number of times the screen was touched is 0.
It Became a Rule on September 14
The practice of not touching the screen’s parts by hand from the conversion flow had, as I wrote in the third installment, been there since March. utf8conv’s current rule is written as that practice with one thing added.
Do not touch tkinter from the worker thread. after() counts as touching, too.
It was written in this form on September 14, 2026. What was added is the part that says asking with a message also counts as touching.
My March self used that way of asking. In the third installment, I described it as a safe practice. In March, the rule that counts even messages did not yet exist. Something that later became a rule, I believed at the time to be safe.
A Pass Was No Substitute for My Own Hands
Even after the pass, the role that writes the brief wrote in the judgment record something to the effect of: “Even so, there is no skipping the owner of this product running it with his own hands.” The reason is that the mock part is a made-up thing that only records the commands it receives; it is not the real screen.
That evening, I ran it from the screen with my own hands. The scene I wrote about at the end of last time — the first time the files in the folder I had prepared for checking were converted from the screen — came after this whole series of exchanges, and after the pass on the acceptance criteria had been confirmed.
About Soul Resonant Works
Soul Resonant Works is a solo venture developing seven local AI systems.
Starting from zero programming experience, the development is progressing through collaboration with AI.
🌐 Soul Resonant Works:
→ https://sr-works.net/en/index.html
📝 This blog publishes the entire development process as a serialized journal.
CubePlot (free version available)
CubePlot is the first product from Soul Resonant Works — it turns a CSV into a 3D scatter plot you can rotate and zoom. No install, no sign-up: it’s a single HTML file you open in your browser, and it works offline. CubePlot itself does not send the data you load outside your machine — external network traffic is blocked at the browser level (CSP). Start with the free version.
▶ Product page: https://sr-works.net/en/cubeplot/
▶ Get it (commercial license, USD $39 + tax where applicable): https://soulworks8.gumroad.com/l/pzcij
If you found this article useful, please share it.
Leave a Reply