<?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: Antonio Zhu</title>
    <description>The latest articles on DEV Community by Antonio Zhu (@antonio_zhu_e726fd856cd86).</description>
    <link>https://dev.to/antonio_zhu_e726fd856cd86</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%2F2014487%2Fcc893815-c4b2-48c1-901d-25b9b160ec74.png</url>
      <title>DEV Community: Antonio Zhu</title>
      <link>https://dev.to/antonio_zhu_e726fd856cd86</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/antonio_zhu_e726fd856cd86"/>
    <language>en</language>
    <item>
      <title>Six Laws for Talking to AI</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Thu, 02 Jul 2026 01:15:10 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/six-laws-for-talking-to-ai-4dan</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/six-laws-for-talking-to-ai-4dan</guid>
      <description>&lt;p&gt;I recently opened a SQLite file — the local session log from OpenCode, the AI coding tool I use every day. 192 sessions, 8,471 messages, 89 million input tokens. Total cost: \$518.&lt;/p&gt;

&lt;p&gt;But cost per token is the wrong metric. I wanted to know: how much of what I said was wasted?&lt;/p&gt;

&lt;p&gt;So I wrote some queries. I counted every message where I said "不对," "不行," "不是," "不不" — the Chinese equivalents of "no, wrong, not that, stop." I counted sessions where I forked the same conversation and started over. I looked at how many messages were shorter than ten characters. I had six sub-agents analyze the six longest sessions independently, pulling out every instance where I corrected the AI, repeated myself, or sent an instruction so vague the AI had to guess.&lt;/p&gt;

&lt;p&gt;The number that stopped me: &lt;strong&gt;60% of my sessions were forked from an older one.&lt;/strong&gt; I was restarting more conversations than I was finishing. For every two user messages in my worst session, I started a new session.&lt;/p&gt;

&lt;p&gt;The rest of this article is what I found. Six patterns. Six fixes. All measurable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Data Snapshot
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total sessions&lt;/td&gt;
&lt;td&gt;192&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total messages&lt;/td&gt;
&lt;td&gt;8,471 (7,109 AI, 1,366 me)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions with a parent (forked)&lt;/td&gt;
&lt;td&gt;115 (60%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit correction messages&lt;/td&gt;
&lt;td&gt;67 (4.9% of my messages)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Messages mentioning push/commit&lt;/td&gt;
&lt;td&gt;132&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Primary model&lt;/td&gt;
&lt;td&gt;deepseek-v4-pro (125 sessions)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions above 100 messages&lt;/td&gt;
&lt;td&gt;4 (capped at 164)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four sessions hit triple-digit message counts. The longest — a deployment migration to a new server — ran 164 messages. I said "get my approval first" four times in that session alone. I said "use GitHub Actions, not manual commands" five times. No one else was in the room. I was repeating myself to a machine.&lt;/p&gt;

&lt;p&gt;But that 60% fork rate cuts both ways, and I have to be honest about the other side of it. The human in me is lazy. I hate starting new sessions. A fresh session means I have to re-explain the project, re-establish the rules, re-load the mental model. So I don't. I cram unrelated tasks into one session until it becomes a junk drawer. Deployment config, CSS refactoring, database schema changes, and a React component all in the same thread. By message 80, the AI has no idea what we're working on anymore, and neither do I. The context window might technically hold 200K tokens, but attention is not a buffer — it's a spotlight, and my spotlight is painting six walls at once.&lt;/p&gt;

&lt;p&gt;The 60% fork rate isn't a disciplined practice. It's a symptom. I fork when I'm frustrated, and I don't fork when I should. Both are failures of context discipline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 1: The gap between knowing and writing is the whole cost.
&lt;/h2&gt;

&lt;p&gt;Everyone knows AI forgets. The interesting question is not &lt;em&gt;does it forget&lt;/em&gt; — it's &lt;em&gt;how long do you wait before fixing it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Across six sessions, I corrected the AI for the same rule violations 27 times before writing a single line to my config file. Not 27 times across months. 27 times where I already knew the pattern, already had the fix in my head, and just didn't stop to write it down.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Corrected in chat&lt;/th&gt;
&lt;th&gt;Messages between first correction and AGENTS.md write&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Get my approval before acting"&lt;/td&gt;
&lt;td&gt;4 times&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"All operations must use GitHub Actions"&lt;/td&gt;
&lt;td&gt;5 times&lt;/td&gt;
&lt;td&gt;52&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Talk business concepts, not code"&lt;/td&gt;
&lt;td&gt;4 times&lt;/td&gt;
&lt;td&gt;54&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Remove project-specific details"&lt;/td&gt;
&lt;td&gt;4 times&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The server migration session is the cleanest example. I told the AI "get my approval before acting" on message 1. By message 48, it had forgotten. I said it again. At message 62, the AI pushed code without confirmation — I said it a third time, with profanity. At message 63, ten minutes later, it happened again. I finally wrote the rule to AGENTS.md around message 80.&lt;/p&gt;

&lt;p&gt;The waste wasn't the 4 corrections. The waste was the 79 messages between "I know this should be a rule" and "this is now a rule."&lt;/p&gt;

&lt;p&gt;Every person who uses AI coding tools knows that writing rules fixes things. The thing I didn't know until I counted: &lt;strong&gt;I average 36 messages between knowing and writing.&lt;/strong&gt; I don't have a knowledge problem. I have an execution latency problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: After &lt;em&gt;every single correction&lt;/em&gt;, ask: does this rule apply to future sessions? If yes, write it now. Not after this task. Not after this session. Now. The cost of writing is ten seconds. The cost of not writing is the rest of the session.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 2: Assess the blast radius. Every time.
&lt;/h2&gt;

&lt;p&gt;Six sessions. Four tirades. Every one followed the same script: I give an instruction involving modification or deletion. The AI starts changing files without telling me what. Something breaks that I didn't expect. I discover the damage and lose my temper.&lt;/p&gt;

&lt;p&gt;Specifics: rewriting a website footer and deleting the social links in it. Deleting an entire RSS feed when I only asked to filter old articles. Manually running commands on a production server. Claiming a file exists when it doesn't.&lt;/p&gt;

&lt;p&gt;My AGENTS.md already says push and deploy require explicit user confirmation. I wrote that after the server migration disaster. But the rule is too narrow. It only covers git. It doesn't cover rewrites, bulk replacements, folder restructures, or production commands — all of which share the same asymmetry: three seconds to break, thirty minutes to fix.&lt;/p&gt;

&lt;p&gt;Here is the thing I missed: this is not a special AI rule. This is the same principle you apply before any production change with a blast radius larger than one file.&lt;/p&gt;

&lt;p&gt;We don't let teammates push to production without a diff. We don't approve a database migration without reviewing which tables it touches. We don't run &lt;code&gt;terraform apply&lt;/code&gt; without reading the plan first. The AI is no different — except that it's faster and has less judgment. An intern who can type at 10,000 WPM. The engineering discipline that keeps production safe is the same discipline that keeps an AI session from spiraling.&lt;/p&gt;

&lt;p&gt;Before I started counting, I thought "ask the AI to confirm" was about trust. It's not. It's about blast radius. The AI proposed a change to my website footer. One file. What's the worst that could happen? The footer appears on every page. That's 100% of the site. One file, full blast radius. The AI doesn't understand that. You do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Before any operation that touches more than one file or any file with a cross-cutting footprint, the AI must list every file it will touch and every change it will make. Then wait for confirmation. This is not a negotiation with a coworker. It's a pre-flight checklist. The same instinct that makes you read &lt;code&gt;terraform plan&lt;/code&gt; output should make you read the AI's change list. Same discipline. Same muscle.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 3: Send the full spec. Not one sentence at a time.
&lt;/h2&gt;

&lt;p&gt;My worst sessions all share a shape: I start with a rough idea, then refine it through seventeen messages. The AI follows each micro-adjustment, but the overhead of each round trip compounds fast.&lt;/p&gt;

&lt;p&gt;Here is a real example, anonymized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"I need a search feature."
"It should filter by name."
"Email too."
"Partial match, not exact."
"Show results in a dropdown."
"Debounce the input by 300ms."
"Start."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven messages. Here is the same spec as one message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Search feature: typeahead dropdown. Filter by name and email, partial match.
Debounce input at 300ms. Show results below the search bar.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two messages would have done it. I spent five extra messages hand-holding the specification because I hadn't completed it in my own head before typing.&lt;/p&gt;

&lt;p&gt;This happened in every long session. The paywall discussion took 18 messages (cancel → no, restrict → wait, AI is paid too → defer). The server migration plan changed direction three times (fully migrate → partially migrate → open a new VM instead). None of these decisions were bad. But each mid-flight change of direction forced the AI to recompute context that had already been settled, which meant it forgot things discussed earlier — which meant I had to re-explain them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Before sending a feature request, write the full spec in a text editor. Fields. Constraints. Edge cases. Interactions. Then send it once. The thirty seconds you spend typing to yourself are worth five rounds with the AI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 4: An instruction under ten characters is a puzzle.
&lt;/h2&gt;

&lt;p&gt;In my longest technical session — building a rules-checking tool — 72% of my messages were under fifty characters. 19% were under five characters.&lt;/p&gt;

&lt;p&gt;Here is what I actually sent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What I typed&lt;/th&gt;
&lt;th&gt;What the AI had to guess&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;change&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Change what? Code? Plan? Naming?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;clean up&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clean which directory? Which files?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;do it.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Do which option?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;symbolic link&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;From where to where?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;check-rules&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Run the tool? Check a file? Create it?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each two-word instruction cost one to three clarification rounds. The AI would propose options. I'd pick one. Total waste per instance: two to four messages. In a 74-message session, I estimate these cost me about fifteen rounds.&lt;/p&gt;

&lt;p&gt;The interesting thing is that I knew exactly what I meant when I typed "change." The context was clear to me. The problem is that context lives in my brain, not in the chat. The AI can only act on what's in the text.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: Before sending, ask: if someone read this instruction with zero prior context, could they execute it? If the answer is no, add a sentence. &lt;code&gt;"change"&lt;/code&gt; becomes &lt;code&gt;"Add input sanitization to the form submission handler."&lt;/code&gt; Same idea. Seven more words. Zero guessing rounds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 5: A bug in file A is a bug in files B through Z.
&lt;/h2&gt;

&lt;p&gt;A session where I was deploying multiple services taught me this. Each service had its own CI config file. Service A's deployment failed — missing a network setting. I pasted the error log. The AI fixed Service A.&lt;/p&gt;

&lt;p&gt;Service B's deployment failed. Same error. Different file. I pasted the log. The AI fixed Service B.&lt;/p&gt;

&lt;p&gt;Service C's deployment failed. Same error. I lost my patience. "You didn't listen," I said. "I told you to fix this."&lt;/p&gt;

&lt;p&gt;The AI had never been told to fix all config files. It had been told to fix the one I pasted, twice. I was angry at the AI for doing exactly what I asked.&lt;/p&gt;

&lt;p&gt;A similar thing happened in another project: a validation bug existed in both the frontend and backend. I fixed the backend, deployed, and the error came back. The frontend had the same logic, untouched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: After identifying a bug in one file, the next instruction should be: "Check all files in this category for the same issue." Not "fix this one." One sentence prevents the slow drip of identical errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Law 6: "This" and "all" are not the same thing. Say which.
&lt;/h2&gt;

&lt;p&gt;I asked the AI to add input validation to a module. "Validate the email field," I said. The AI validated email — and only email. I meant all fields on the form. But I had said "this field." The AI took me literally.&lt;/p&gt;

&lt;p&gt;Same thing with error handling: "add try-catch to this function" got me one function. I wanted it across the entire module. Three rounds to converge.&lt;/p&gt;

&lt;p&gt;In both cases I had said "this" — this field, this function — when I meant "every field in the module," "every function that calls an external API." The AI applied the constraint to the one thing I named. I thought the context made my intent obvious. It didn't.&lt;/p&gt;

&lt;p&gt;Another case: I told the AI to add rate limiting to one API endpoint, intending it as a pattern for the whole service. The AI added it to one endpoint. I said "no, all of them." Two rounds. If I had started with "add rate limiting to all endpoints," done in one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: If a constraint applies to more than one output, start with "all." If it applies to one, start with "this one." Explicit scope costs zero tokens and saves three corrections.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Compounding Asset
&lt;/h2&gt;

&lt;p&gt;None of these laws are about prompt engineering. They are about systematizing your own communication patterns. The real insight from 192 sessions is not that I needed better prompts. It's that I needed a process for not repeating myself.&lt;/p&gt;

&lt;p&gt;AGENTS.md is not a file you write once. It's a muscle you exercise after every correction. The engineer who talks to AI for a hundred sessions and writes nothing down has the same information as session ten. The one who writes a rule after every correction has a compounding asset. Every new session starts with the accumulated wisdom of every previous correction. The AI catches up to your intent faster. You repeat yourself less. The sessions get shorter, the output higher quality, and the four-letter words drop to zero.&lt;/p&gt;

&lt;p&gt;That's the asymptote worth chasing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>coding</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I Reverse Engineered a Closed-Format App. Everything Was in SQLite.</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Fri, 26 Jun 2026 16:47:14 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/i-reverse-engineered-a-closed-format-app-everything-was-in-sqlite-340g</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/i-reverse-engineered-a-closed-format-app-everything-was-in-sqlite-340g</guid>
      <description>&lt;p&gt;I had accumulated over two thousand notes in Youdao Cloud Note over several years. When I decided to move to Obsidian, the first thing I checked was the export feature. There wasn't one. No batch export, no single-note export, nothing in the Mac client.&lt;/p&gt;

&lt;p&gt;So I went looking for the local data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where The Data Was
&lt;/h2&gt;

&lt;p&gt;On macOS, Youdao Cloud Note stores its data here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~/Library/Containers/ynote-desktop/Data/Library/Application Support/ynote-desktop/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside that directory, organized by account email, were three SQLite databases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&amp;lt;&lt;span class="n"&gt;account&lt;/span&gt;&amp;gt;.&lt;span class="n"&gt;db&lt;/span&gt;         &lt;span class="c"&gt;# note metadata, folder hierarchy
&lt;/span&gt;&amp;lt;&lt;span class="n"&gt;account&lt;/span&gt;&amp;gt;-&lt;span class="n"&gt;content&lt;/span&gt;.&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="c"&gt;# note content (old editor)
&lt;/span&gt;&amp;lt;&lt;span class="n"&gt;account&lt;/span&gt;&amp;gt;-&lt;span class="n"&gt;search&lt;/span&gt;.&lt;span class="n"&gt;db&lt;/span&gt;  &lt;span class="c"&gt;# search index
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a &lt;code&gt;file/&lt;/code&gt; directory with 16 subdirectories arranged by the first character of each file ID, holding the new editor's local files. Everything was unencrypted.&lt;/p&gt;




&lt;h2&gt;
  
  
  What The Database Contained
&lt;/h2&gt;

&lt;p&gt;The main database had two critical tables:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;note&lt;/code&gt;&lt;/strong&gt; — the note catalog. Key columns: &lt;code&gt;fileId&lt;/code&gt; (UUID), &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;parentId&lt;/code&gt; (folder reference), &lt;code&gt;orgEditorType&lt;/code&gt; (0 for the new block editor, 1 for the old plain editor), &lt;code&gt;entryPath&lt;/code&gt; (path to the local file), &lt;code&gt;createTime&lt;/code&gt; (Unix timestamp in seconds), and &lt;code&gt;deleted&lt;/code&gt; (NULL meant not deleted).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;note_book&lt;/code&gt;&lt;/strong&gt; — the folder tree. Folders were not in &lt;code&gt;note&lt;/code&gt; at all. They lived in a separate table with their own &lt;code&gt;fileId&lt;/code&gt; and &lt;code&gt;parentId&lt;/code&gt; fields, forming a tree you could traverse with BFS.&lt;/p&gt;

&lt;p&gt;The content database held a &lt;code&gt;contenttable&lt;/code&gt; with a &lt;code&gt;content&lt;/code&gt; field, but the field was truncated to around 150 characters — just enough for search snippets. The real content for old-editor notes lived here too, with variable lengths.&lt;/p&gt;

&lt;p&gt;Where the content actually was depended on which editor wrote the note:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;orgEditorType&lt;/th&gt;
&lt;th&gt;Editor&lt;/th&gt;
&lt;th&gt;Content source&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;New (block editor)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;entryPath&lt;/code&gt; → local file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Old (plain)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contenttable.content&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The local files came in two formats: JSON (449 of them) and plain Markdown (1,832).&lt;/p&gt;




&lt;h2&gt;
  
  
  The JSON Block Tree
&lt;/h2&gt;

&lt;p&gt;The new editor stored notes as a block tree in JSON. Each block followed this structure, with the actual keys being cryptic integers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"6" → block type (p=paragraph, h=heading, co=code, t=table, im=image, l=list, q=quote, hr=divider)
"4" → properties (heading level, code language, image URL, etc.)
"5" → array of child blocks
"7" → inline text segments with "9" format markers (b=bold, i=italic, li=link, il=inline code, etc.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walking the &lt;code&gt;"5"&lt;/code&gt; array recursively converted the entire tree to Markdown. The core renderer was under 200 lines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Things That Wasted My Time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;__compress__&lt;/code&gt; flag.&lt;/strong&gt; Ten JSON files had &lt;code&gt;"__compress__": true&lt;/code&gt;. I spent an hour trying to uncompress them with LZString before realizing the &lt;code&gt;"5"&lt;/code&gt; array was still just a plain array — not a compressed string. The flag had been set but never used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deleted is NULL, not 0.&lt;/strong&gt; Every note with &lt;code&gt;deleted IS NULL&lt;/code&gt; had been a living note. &lt;code&gt;WHERE deleted = 0&lt;/code&gt; returned nothing. This is standard SQL but easy to miss when you're scanning thousands of rows for anomalies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The timestamp is in seconds.&lt;/strong&gt; &lt;code&gt;1484115955&lt;/code&gt; is January 11, 2017. If you treat it as milliseconds and divide by 1000, you land in 1970. I did that once.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Dead-End That Wasn't
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;contenttable.content&lt;/code&gt; field was consistently 150 characters. This looked like a deliberate truncation to defeat extraction. But the actual content was never supposed to be in that column — the new editor stored everything in local files referenced by &lt;code&gt;entryPath&lt;/code&gt;. The content column was just a search index. Once I followed the &lt;code&gt;entryPath&lt;/code&gt; trail, I had the full text of every note.&lt;/p&gt;




&lt;h2&gt;
  
  
  What The Script Does
&lt;/h2&gt;

&lt;p&gt;The whole thing is a single Python file. It connects to the three SQLite databases, walks the folder tree, maps every note to its content source, converts JSON blocks to Markdown when needed, and writes the output organized by folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/chncaesar/youdao-to-obsidian.git
&lt;span class="nb"&gt;cd &lt;/span&gt;youdao-to-obsidian
pip3 &lt;span class="nb"&gt;install &lt;/span&gt;beautifulsoup4
python3 youdao_migrate.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script auto-detects your account and data directory. Output goes to &lt;code&gt;~/Desktop/obsidian/&lt;/code&gt; by default. Each note gets a &lt;code&gt;.md&lt;/code&gt; file with YAML frontmatter preserving the original title, creation date, and source ID.&lt;/p&gt;

&lt;p&gt;I also bundled a Claude Code Skill in the repo — drop it into &lt;code&gt;~/.claude/skills/&lt;/code&gt; and saying "export my Youdao notes" triggers the whole pipeline without remembering flags.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Came Out
&lt;/h2&gt;

&lt;p&gt;The final export: 2,285 notes across 253 folders. Mixed Chinese, English, and code content with no encoding issues. Tables, code blocks, images, lists, and blockquotes all converted correctly from the JSON block tree. Some notes were empty bodies with attachments only, handled gracefully.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;~/Desktop/obsidian&lt;/code&gt; as an Obsidian vault and everything is there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Worked
&lt;/h2&gt;

&lt;p&gt;The most interesting part of this project was not the parser or the block converter.&lt;/p&gt;

&lt;p&gt;It was the realization that a closed-format application with no export feature had been storing all my data in plain, unencrypted SQLite files, with a documented-enough block structure that could be reverse-engineered in an afternoon.&lt;/p&gt;

&lt;p&gt;Two thousand notes. Years of writing. The application offered no way to take them out. They were never locked in. I just hadn't looked.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/chncaesar/youdao-to-obsidian" rel="noopener noreferrer"&gt;github.com/chncaesar/youdao-to-obsidian&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>productivity</category>
      <category>software</category>
      <category>sql</category>
    </item>
    <item>
      <title>I Don't Understand Embedded. I Was Still the Only One Who Could Ship It.</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:25:23 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/i-dont-understand-embedded-i-was-still-the-only-one-who-could-ship-it-24c2</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/i-dont-understand-embedded-i-was-still-the-only-one-who-could-ship-it-24c2</guid>
      <description>&lt;p&gt;The local test suite was green. Eleven of eleven. I told the agent we had test coverage, and I said commit and push. We shipped an SDK that had never once been compiled by the chip's real compiler.&lt;/p&gt;

&lt;p&gt;I write backend systems. I do not write firmware. The project generates C code for a microcontroller, and the generated code had to compile inside CDK — the vendor's build environment — on a Windows machine, against a RISC-V cross-compiler, inside a packaging system I had never used. None of that existed on my Linux box. The eleven passing tests ran under host gcc — the compiler that builds programs for the machine they run on. The code that mattered needed a different compiler, for a different chip, in a place I couldn't reach. Two compilers, two worlds. My tests lived in the wrong one.&lt;/p&gt;

&lt;p&gt;A day later the first error came back from the Windows machine. Then another. Then five more.&lt;/p&gt;




&lt;h2&gt;
  
  
  Seven shots
&lt;/h2&gt;

&lt;p&gt;The errors arrived one at a time, and the agent answered each the same way: a confident "root cause:" and a fix pushed to the repo. A header file was missing. A type name was wrong — bugs no real compiler had ever touched. Then the SDK wasn't being compiled at all: an obscure manifest setting had been left out. Then the chip's pin names were wrong — the same pin called one thing in the SDK the agent wrote against, and another thing in the SDK CDK actually shipped. Then an entire driver file silently compiled to nothing, because the switch that turned it on lived in a generated file the compiler couldn't see. Then the same wall again, because the last fix had patched a symptom and the real problem was the architecture.&lt;/p&gt;

&lt;p&gt;Seven rounds. Each one the agent called the root cause. Each one was real, and each one was only the outermost skin of the next.&lt;/p&gt;

&lt;p&gt;What I want to point at is not the bugs. It is the shape of the loop. The agent fired a fix in about three seconds. Verifying it cost me minutes to hours: pull the code on Windows, run the full CDK build, read the error output, copy it back. The actual feedback loop, the one that touched ground truth, ran at human speed across two machines that could not talk to each other, with me manually shuttling errors between them. I had built an elaborate multi-agent workflow, and the real oracle in it was a person doing copy-paste.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why it couldn't stop
&lt;/h2&gt;

&lt;p&gt;A confident "root cause:" at every step is not a sign of convergence. It is a sign of nothing. The agent emitted the prose of certainty regardless of whether it was getting closer or flailing, and at one point the fix it proposed was to simply rip out the switch that had silenced the driver file, so the thing would compile — a hack I shut down with a flat "that's too dirty." Only after that did the real fix surface: let the driver be chosen by which file you compile in, not by a switch buried in a file the compiler never sees. A structural change, not another patch.&lt;/p&gt;

&lt;p&gt;Here is the part worth sitting with. The agent never said the one true sentence: &lt;em&gt;I cannot see the target. Every fix I am giving you is a guess. Stop pulling — go install the RISC-V toolchain, and then I'll be useful.&lt;/em&gt; It never said it because each guess cost the agent three seconds and cost me an afternoon, and the agent does not pay that bill. An agent that doesn't bear the cost of a wrong guess will always prefer firing one more shot over asking for the instrument that would let it aim. The economics are silent, and they run entirely against the person at the keyboard.&lt;/p&gt;




&lt;h2&gt;
  
  
  The briefing that wouldn't have helped
&lt;/h2&gt;

&lt;p&gt;The obvious lesson is the disappointing one: I should have explained the new component's mechanics to the agent up front. Tell it how the build environment wants its manifest written, how it pulls components into a build, where it expects the pieces to sit. Brief the new role, wire it to the existing ones, and the seven rounds collapse to zero.&lt;/p&gt;

&lt;p&gt;Except briefing the agent wouldn't have helped. The agent was not ignorant of CDK — it knew the build environment well enough to scaffold a component from scratch, write the manifest, lay out the search paths. At one point it explained, fluently and at length, how CDK resolves dependencies. The explanation was wrong, but it was not uninformed. The trap is exactly this: general knowledge of CDK is not the same as knowing how this version, wired to this version of the SDK, would actually behave when the build ran. The setting that had been left out, the manifest format, the pin names — those were facts about the runtime behavior of a target neither of us could observe. Its real knowledge ran right up to the edge of the part that bites, with no seam to mark where one ended and the other began.&lt;/p&gt;

&lt;p&gt;Here is where an experienced embedded engineer would have done something the agent could not. The port layer was written against one copy of the chip's SDK; CDK shipped a different copy. To a veteran, "two sources of the same SDK" trips a reflex — same chip, two SDKs, the symbols will not match, go diff them now. It's not magic. Given the right prompt, a smart model can produce the same checklist. The gap is not capability, it's posture: an engineer's scar rewrites their default to suspicion; a model's default is confidence — it charges. Someone has to steer it. In that session nobody did, because neither of us knew there was a trap to steer toward. And the second reason is blunter, and it doesn't care how smart the model is: the second SDK wasn't in its context. It lived on the Windows machine. The agent never had both files. You cannot glance at a trap, or be prompted to look for one, that is sitting in another room.&lt;/p&gt;

&lt;p&gt;So the question stops being "how do I brief the agent" and becomes "what does anyone do when the ground truth is unknown to everyone and lives on a machine I'll never see."&lt;/p&gt;




&lt;h2&gt;
  
  
  The scout
&lt;/h2&gt;

&lt;p&gt;You don't translate the wall. You admit it's dark, and you send the cheapest possible probe to light it up once — before writing the design, before writing the code. The move I should have made on day one was to put the RISC-V compiler on my own machine and run a syntax-only pass over the generated code — compile it just far enough to surface errors, without building a real binary. Seconds, locally, instead of symbol mismatches arriving through someone's clipboard a week later. Not because it would tell me everything. Because it would drag a piece of that unreachable world into a place I could actually touch.&lt;/p&gt;

&lt;p&gt;This does not abolish trial-and-error. Trial-and-error is how you explore the genuinely unknown; there is no version of this work where you think hard enough up front to skip it. What you get to choose is where you hit the wall, whose time you spend hitting it, and whether you hit it once or seven times. A scout doesn't avoid the dark. A scout pays the smallest price to make a piece of it known, and does it before committing the expedition.&lt;/p&gt;




&lt;h2&gt;
  
  
  Second act: what I actually built
&lt;/h2&gt;

&lt;p&gt;Diagnosis without a fix is just another complaint about AI. So after the firmware finally linked, I went back and changed the distance between the agent and the truth it couldn't reach. Three changes, and they sit at three very different levels — it's worth being honest about which ones actually closed the gap and which ones only narrowed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tear the wall down.&lt;/strong&gt; The strongest fix was to make the code generator emit the CDK project file itself — the list of source files, the search paths, the memory layout, all of it. Two of the seven errors came from a human hand-configuring CDK and missing something. The fix wasn't to verify that configuration more carefully. It was to delete the manual step entirely, so there was nothing left to misconfigure. This is the move I'd never made in any earlier post: not adding a layer of verification, but removing the thing that needed verifying. When you can make a stretch of unknown terrain simply cease to exist, that beats any amount of careful scouting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Raise a telescope — and admit where it points.&lt;/strong&gt; I added a check that runs at generation time, doing a syntax-only pass over the core SDK code, wired into the test suite. It pulls the type-name and missing-value class of bug from "your clipboard, days later" to "a local test, seconds later." That is real. But it runs under host gcc — my machine's compiler — not the RISC-V one, and so the pin-name mismatches, the manifest setting, the switch the compiler couldn't see — none of those are catchable by it, because they live in a world host gcc can never enter. The telescope got closer. It is still not aimed at the mountain that actually matters. The probe that would close that boundary — the RISC-V compiler running the same syntax-only pass locally on every build — I still have not installed. I know exactly what it is. It is sitting on the ground next to me. That is the honest state of this system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Draw a map for the next one.&lt;/strong&gt; I wrote the hard-won facts down as rules: the manifest format, the missing setting, a table of which pin names differ. This froze the intelligence the seven shots cost me. These rules are loaded at the start of every session — they do take effect. But unlike the other two fixes, which are automated and need no further human attention, written rules depend on maintenance: someone has to keep them accurate, scope them correctly, and phrase them precisely enough that the next agent doesn't misinterpret them. It's the most human-dependent fix of the three.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a harness actually is
&lt;/h2&gt;

&lt;p&gt;I used to think the missing step in AI-assisted work was verification — check the behavior, check the spec, check for the same bug elsewhere. That's still true, and I've written it before. This time taught me the step underneath it.&lt;/p&gt;

&lt;p&gt;An agent only thinks inside its context. That isn't a limitation you fix; it's the shape of the thing. It knew the build environment in general — enough to sound, and be, genuinely competent. What it could not know was how this specific target would behave when the build actually ran, and it could not feel the seam between the two. Its real knowledge ran right up to the edge of the part that bites. So it didn't say "I'm blind." It fired seven confident shots at a wall, and the confidence came from the knowledge that was real. The danger was never that it couldn't see. The danger was that it couldn't tell where its sight ended.&lt;/p&gt;

&lt;p&gt;A person has to stand at that boundary and say the wall is dark. The person doesn't need to understand what's behind it — I didn't, and I still don't. What the person does is the one thing the agent structurally won't: bear the cost of being blind, and spend it deliberately on making the dark known. That is what a harness is. Not a test suite. A harness is the work of shortening the distance between an agent and a ground truth it will never walk toward on its own — because it doesn't know the truth is out there, and it doesn't pay the price of not knowing.&lt;/p&gt;

&lt;p&gt;I can't write firmware. I couldn't brief the agent on a single thing about that chip. And I was still the only one who could ship it, because shipping it had nothing to do with understanding embedded development. It had to do with standing on the boundary, admitting it was dark, and dragging the far machine one step closer. When the agent writes more of the code than you can read, that is the job that's left. Not knowing the domain. Knowing which wall nobody has hit yet, and being willing to pay to hit it first.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>harness</category>
    </item>
    <item>
      <title>Your Agent Checked Everything. It Was Still Wrong.</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Mon, 22 Jun 2026 01:51:52 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/your-agent-checked-everything-it-was-still-wrong-18kd</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/your-agent-checked-everything-it-was-still-wrong-18kd</guid>
      <description>&lt;p&gt;I have been running a multi-agent development workflow for months now — one model writes the design, another generates the code, a third reviews the implementation, and I approve the result. It works well most of the time. But recently, three failures went through this pipeline undetected, and they share a pattern I had not been able to articulate until all three were on the table.&lt;/p&gt;

&lt;p&gt;They are not impressive bugs. The individual root causes are straightforward once you see them. What makes them worth writing about is what they reveal about the structure of this kind of failure — and what kind of verification could have caught them before they reached production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Case 1: The retry loop that never fired
&lt;/h2&gt;

&lt;p&gt;An ETL pipeline synced data from a third-party ERP API to a PostgreSQL warehouse. Every 5 to 15 minutes, it pulled incremental changes using a session-based authentication mechanism. The code had a standard retry loop: three attempts, exponential backoff, and a &lt;code&gt;resetLogin()&lt;/code&gt; call on failure to re-authenticate before the next attempt. It had worked for weeks.&lt;/p&gt;

&lt;p&gt;Then one afternoon, every sync job started failing with the same error: &lt;code&gt;COPY from stdin failed: expected N values, got 1&lt;/code&gt;. The pipeline reported the error and retried. And retried. And kept failing, for hours, until someone noticed.&lt;/p&gt;

&lt;p&gt;The retry loop existed in the code. &lt;code&gt;resetLogin()&lt;/code&gt; existed in the code. Neither had ever triggered. The reason took some investigation. When the ERP session expired after roughly 8.5 hours, the API did not return a 401 or a 403 or any HTTP error. It returned an HTTP 200 with a body that decoded to &lt;code&gt;[["error message"]]&lt;/code&gt; — a structurally valid JSON array containing one row with one column. The Go JSON decoder had no reason to produce an error. The retry condition checked &lt;code&gt;if err != nil&lt;/code&gt;, found nothing, and moved on with what it believed was a single-row result set.&lt;/p&gt;

&lt;p&gt;The fix was not to improve the retry logic. The fix was a background goroutine that proactively refreshes the session on a 4-hour ticker — a keepalive that renders the reactive recovery path irrelevant. The retry loop, the piece of the system that was supposed to handle this exact scenario, had been dead on arrival.&lt;/p&gt;




&lt;h2&gt;
  
  
  Case 2: The coordinates that never existed
&lt;/h2&gt;

&lt;p&gt;A code generator in a desktop tool produced C source files for an embedded microcontroller. Given a visual UI design, it emitted header files with widget macro definitions, a context file with event dispatch functions, and resource indices mapping image addresses in external flash memory. Developers compiled the output into their firmware, flashed the chip, and saw something strange: dynamic text widgets rendering at position (0, 0) regardless of where they had been placed in the design.&lt;/p&gt;

&lt;p&gt;The generated code compiled without warnings. The linker found all referenced symbols. No function returned an error. Everything looked complete. But the runtime widget table — a separate generated file that maps each widget's ID to its x, y, width, and height — had never been written. Nobody had called the method that produced it. The header file declared the widget struct type with x/y fields. The resource manifest carried coordinate information. The design document described the widget table format. The code generator simply never emitted it.&lt;/p&gt;

&lt;p&gt;The generated output was reviewed for content correctness. The reviewer confirmed that the header macros pointed to the right resource addresses, that the event routing was correct, that the page definitions matched the design. All of that was true. Nobody asked: are we generating all the files we need to generate?&lt;/p&gt;




&lt;h2&gt;
  
  
  Case 3: Read the instructions, not the comment
&lt;/h2&gt;

&lt;p&gt;I was integrating an SDK for a new microcontroller. The vendor had provided a zip file with the chip's SDK, and the directory structure looked right: startup file, linker scripts for the specific chip model, peripheral drivers for SPI, DMA, GPIO — everything I expected.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;startup.S&lt;/code&gt; file opened with a comment header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;; GCC for CSKY Embedded Processors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;CSKY is a different embedded CPU architecture, not the one this chip used. The datasheet clearly stated the chip was RISC-V. But the SDK directory was named for this specific chip family, the subdirectories matched, and the startup file was sitting right where it belonged. The comment said one thing. The directory name said another. Which was correct?&lt;/p&gt;

&lt;p&gt;Two facts resolved it. First, the vendor had accidentally delivered the SDK for a different chip in the same product family — one that used CSKY. The directory name was misleading. Second, when the correct SDK arrived, its &lt;code&gt;startup.S&lt;/code&gt; had the same CSKY comment in the header, because the vendor had copied the template from a third-party toolchain and never cleaned it up. The actual instructions told a different story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;csrw    mtvec, a0        ; RISC-V: write machine trap vector
la      sp, g_top_irqstack
jal     main             ; RISC-V: jump-and-link to main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simplest verification was a grep: &lt;code&gt;grep -c "csrw\|mret" startup.S&lt;/code&gt; returned 3. It was RISC-V. The comment was a fossil.&lt;/p&gt;




&lt;h2&gt;
  
  
  Not an Intelligence Problem
&lt;/h2&gt;

&lt;p&gt;All three agents in my workflow — the designer, the implementer, the reviewer — did something that, in isolation, was correct. The designer wrote a spec that matched the task. The implementer produced code that matched the spec. The reviewer confirmed the match. Every verification step passed. The system was consistent. It was consistently wrong.&lt;/p&gt;

&lt;p&gt;This is not an intelligence problem. It is a boundary problem. The agent does exactly what it is asked to do, within the context it is given. No one told it where the context ends.&lt;/p&gt;

&lt;p&gt;In Case 1, the context was "check if the HTTP call returned an error." No one said "also check whether the successful response actually contains data."&lt;/p&gt;

&lt;p&gt;In Case 2, the context was "check if the generated files are correct." No one said "also check whether all required output files exist."&lt;/p&gt;

&lt;p&gt;In Case 3, the context was "trust the SDK directory structure and documentation to be correct." No one said "verify the architecture claim against the actual instruction stream."&lt;/p&gt;

&lt;p&gt;In every case, the agent performed a complete verification of what it understood to be the scope of the task. The scope was wrong. And that gap — between what the agent checks and what actually needs to be true — is the failure mode that an AI-assisted workflow is structurally blind to.&lt;/p&gt;

&lt;p&gt;This is not a bug in the agent. It is a missing layer in the workflow. The agent will verify the things you tell it to verify. It will not independently discover new categories of things to verify. That discovery is still engineering.&lt;/p&gt;




&lt;h2&gt;
  
  
  What belongs on the checklist
&lt;/h2&gt;

&lt;p&gt;Here is what they look like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Semantic output validation: check what the success means, not just that it returned success.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An API call, a function return, a system call — each has a success path and a failure path. But some systems encode failure inside success. After every external call, ask: does the returned data look like data, or does it look like an error in disguise? Check the row count. Check the structure. Scan for error keywords. If the caller received 1 row when the expected number of columns is much larger, the data is already suspect regardless of what &lt;code&gt;err&lt;/code&gt; says.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Output completeness: verify that all required artifacts exist, not just that the ones that exist are correct.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Code generation produces multiple files. Some are visible in the output directory. Some should be but are not. Before accepting a generation result, enumerate the expected output files and confirm each one exists as a non-empty file. Do not accept "the generated files look correct" as a substitute for "all generated files are present."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Ground-truth verification: for any technical claim that can be tested by a machine, test it by a machine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Comments can lie. File names can lie. Directory structures can lie. The CPU architecture of a &lt;code&gt;.S&lt;/code&gt; file is not whatever the header comment says — it is whatever instructions the assembler would emit. The format of a binary file is not whatever the README claims — it is whatever the bytes at specific offsets contain. Before any agent workflow makes a design decision based on a technical assumption, verify that assumption with a command that reads the artifact directly. Do not pass claims forward unchecked.&lt;/p&gt;




&lt;p&gt;None of these three checkpoints is dramatic. Each is small enough to be added to a task definition or a review stage without substantially changing the workflow. Together, they cover the three failure modes I have now seen multiple times: undetected failure inside a successful response, missing output from a code generator, and a false claim repeated by a toolchain artifact.&lt;/p&gt;

&lt;p&gt;The agent is not going to ask itself "what should I also verify?" That question — what else could be wrong even when everything I checked is right — is not something an agent can generate from within its own context. It has to come from outside. It has to be designed. When you set up a multi-agent workflow, you are not just designing a pipeline for code. You are designing the boundaries of what each agent will treat as ground truth. If you do not place verification checkpoints at those boundaries, no one will.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>AI Fixed The Bug. Then I Found Two More Just Like It.</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Sun, 21 Jun 2026 12:55:14 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/ai-fixed-the-bug-then-i-found-two-more-just-like-it-1e5b</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/ai-fixed-the-bug-then-i-found-two-more-just-like-it-1e5b</guid>
      <description>&lt;p&gt;I reported a bug. AI fixed it. Three days later, the same bug appeared somewhere else. I reported it again, AI fixed it again, and two weeks after that a third instance surfaced — same root cause, different location, different symptom. At some point I realized the problem was not that AI kept making the mistake. The problem was that I kept treating each occurrence as a separate incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Bug
&lt;/h2&gt;

&lt;p&gt;Early in the project I had established a rule: all monetary values must be stored as integers representing cents. Never floating point, never doubles, always integers. The rule existed because floating-point arithmetic on money produces invisible precision errors that accumulate silently over time. It was documented. The codebase had utility functions for converting between display values and stored integers.&lt;/p&gt;

&lt;p&gt;Despite all of this, the same mistake appeared three times across different parts of the codebase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="c1"&gt;// first occurrence&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;controller&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// second occurrence, different feature&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priceInput&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// third occurrence, different feature again&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;currentPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiResponse&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'price'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each time, AI had been asked to implement a new feature. Each time, it reached for &lt;code&gt;double.tryParse&lt;/code&gt; because that is the natural way to parse a number from text input — it's what most Dart code does, and it's what the training data overwhelmingly shows. Each time, a code review caught the specific instance that was reported. And each time, nobody checked whether the same pattern existed elsewhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  How AI Fixes Bugs
&lt;/h2&gt;

&lt;p&gt;When you show AI a bug, it focuses on that bug. It reads the surrounding code, understands the problem, and proposes a fix — and the fix is usually correct for the code it looked at. What it does not do is search the rest of the project for other places where the same mistake might exist. This is not a failure of intelligence. It is a failure of scope. AI responds to what you show it, and it does not independently decide to audit your entire codebase for related patterns.&lt;/p&gt;

&lt;p&gt;The difference becomes clear when you compare it to how an experienced engineer handles the same situation. Someone who has worked in a codebase for months develops an instinct for where the same patterns get repeated. When they fix a bug, they often think: I've seen this structure elsewhere, let me check those places too. That instinct comes from having written and read most of the code themselves. AI does not have this map. Every conversation starts fresh, and even if you have fixed the same mistake twice before in previous sessions, AI has no memory of having seen the pattern in your codebase. It does not know to be suspicious.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Broader Pattern
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;double.tryParse&lt;/code&gt; issue was one example, but the same dynamic played out several more times. After fixing a race condition in one part of the UI, I found two more places with the same structure. After fixing an error message that was leaking internal exception details to users, I found four more places doing the same thing. After fixing a database write operation that was missing an invalidation call before navigation, I found five more. Each time, AI fixed the reported instance correctly. Each time, the other instances were waiting quietly.&lt;/p&gt;

&lt;p&gt;The reason this happens more with AI than it did when I wrote most of the code myself is straightforward. When I introduce a bug, I at least know roughly where that pattern appears, because I wrote it. With AI generating large blocks of implementation that I review but don't write line by line, the mental map is thinner. And AI will consistently apply the same habits across multiple features, because it is generating from the same set of assumptions and defaults — which means when it has a bad habit, that habit is likely distributed across every place it was asked to do something similar.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Changed
&lt;/h2&gt;

&lt;p&gt;After the third occurrence of the &lt;code&gt;double.tryParse&lt;/code&gt; bug, I changed how I approach AI-assisted fixes. Now, before I show AI the specific fix, I do one step first: search the entire codebase for the pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"double.tryParse"&lt;/span&gt; lib/
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"double.parse"&lt;/span&gt; lib/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I make a list of every occurrence, then fix all of them in one pass. This sounds obvious, but in practice it is easy to skip when AI is handling the fix and everything feels handled. The reported instance gets resolved, the review looks clean, and the invisible copies wait for their turn to surface.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Role That Has Not Changed
&lt;/h2&gt;

&lt;p&gt;Code generation is increasingly handled by AI. Code review is increasingly assisted by AI. Architecture can increasingly be drafted by AI. But the role that seems to be not just surviving but becoming more important is the engineer who holds the global view — who knows what patterns are repeated and where, who knows what invariants must hold across the entire system, and who, when a bug appears in one place, asks whether the same mistake exists somewhere else.&lt;/p&gt;

&lt;p&gt;AI does not have that view. It cannot. Every session starts fresh, and every conversation focuses on what was shown. The global view of your codebase lives in your head, and when AI is writing code faster than you can track, that global view is exactly what you need to protect. When you ask AI to fix a bug, the fix will likely be correct for the code you showed it. Before you accept that fix, take a minute and search for the same pattern. That question — where else could this be? — used to be instinctive. With AI writing more of the code, it needs to become deliberate.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The Design Doc Was Wrong. AI Trusted It Anyway.</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Sun, 21 Jun 2026 12:54:18 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/the-design-doc-was-wrong-ai-trusted-it-anyway-4309</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/the-design-doc-was-wrong-ai-trusted-it-anyway-4309</guid>
      <description>&lt;p&gt;I was building a new module for a side project, and the design document looked thorough. Claude had written it, I had reviewed it, and everything seemed reasonable. One section described how to calculate a derived value from two stored fields — both multiplied by 100 to preserve two decimal places — and gave the formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Result = fieldA × 100 × fieldB × 100 / 10000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logic made sense on the surface. Both fields carried 100x precision, so the divisor cancels them out. DeepSeek implemented it. The review agent verified it. I approved it. Then I opened the simulator and saw a value that should have been 1,200 displayed as 120,000.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Formula Was Wrong By A Factor Of 100
&lt;/h2&gt;

&lt;p&gt;The correct divisor was 1,000,000, not 10,000. When you multiply two fields that are each inflated by 100x, the product is inflated by 10,000x — so you need to divide by 1,000,000 to get back to base units, not 10,000. The design document had gotten this wrong, and everything downstream had faithfully reproduced that mistake.&lt;/p&gt;

&lt;p&gt;That part was easy to fix once I saw it. What I kept thinking about afterward was the process that had failed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Systems Read The Same Wrong Formula
&lt;/h2&gt;

&lt;p&gt;A human engineer reading that formula would probably do something instinctive: substitute real numbers. Two quantities, each stored as integer × 100. Say fieldA = 10000 and fieldB = 1200. Multiply: 12,000,000. Divide by 10,000: 1,200. But the expected result should be 12. That doesn't add up. The error surfaces in about five seconds, not through formal verification, just through the habit of running a quick sanity check before trusting a formula.&lt;/p&gt;

&lt;p&gt;None of the three systems in my workflow did this. DeepSeek read the design document and implemented the formula exactly as written. The review agent checked whether the implementation matched the design and confirmed that it did. I read both and approved them. Every statement was technically correct. The document was the source of truth, the code faithfully implemented the document, and the review verified the match. Nobody ran the numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Assumption I Had Been Making
&lt;/h2&gt;

&lt;p&gt;After I fixed the divisor, I started thinking about what had actually failed — and it wasn't the math. The math error was trivial once I looked at it carefully. What failed was a deeper assumption I had been making about how AI-assisted development works.&lt;/p&gt;

&lt;p&gt;I had been treating design documents as verified artifacts. The workflow in my head was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Design Document → AI Implementation → AI Review → Human Approval
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implicit assumption embedded in this workflow was that the design document itself was correct. If it wasn't, the whole chain would faithfully reproduce the error — which is exactly what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  Documents Are Not Inputs. They Are Claims.
&lt;/h2&gt;

&lt;p&gt;A design document is not a verified fact. It is a claim made by whoever wrote it, and in this case that was Claude. Claude is very good at generating coherent, plausible-looking technical documentation. It is considerably less good at verifying whether the math embedded in that documentation is actually correct. I had been asking it to do both things at once — reason about architecture and verify arithmetic — without recognizing that it handles those two responsibilities very differently. It did the first well. It did the second poorly. And I hadn't thought to separate them.&lt;/p&gt;

&lt;p&gt;The fix I've settled on is simple: any formula in a design document is treated as unverified until someone has substituted concrete numbers and checked the result by hand. Not because AI gets formulas wrong frequently, but because when it does, the error propagates cleanly and invisibly through every downstream step. The formula is wrong, the implementation is wrong, the review confirms the implementation matches the formula, and everything is consistent. Everything is wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Code Did Exactly What It Was Supposed To Do
&lt;/h2&gt;

&lt;p&gt;Most bugs in AI-generated code are implementation bugs — a condition is inverted, a null check is missing, an edge case is unhandled. Those are failures where the code doesn't implement the intent correctly. This was different. The code implemented the intent perfectly. The intent was wrong. That category of error is much harder to catch with code review, because code review asks whether the code does what it's supposed to do. The answer here was yes. Nobody asked whether what it was supposed to do was actually correct.&lt;/p&gt;

&lt;p&gt;That question — is the specification right, not just the implementation — is increasingly where I think human judgment matters most in an AI-assisted workflow. AI is getting quite good at turning specifications into working code. It is getting reasonably good at catching implementation bugs. But it tends to treat the specification itself as ground truth, which means the work of questioning whether the spec makes sense in the first place is still entirely yours. AI will implement whatever you give it, with increasing reliability. The question is whether what you gave it was right.&lt;/p&gt;

&lt;p&gt;And right now, that part is still on you.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>LLMs Are Lowering Coding Cost — But They May Be Increasing Debugging Complexity</title>
      <dc:creator>Antonio Zhu</dc:creator>
      <pubDate>Wed, 17 Jun 2026 03:51:11 +0000</pubDate>
      <link>https://dev.to/antonio_zhu_e726fd856cd86/llms-are-lowering-coding-cost-but-they-may-be-increasing-debugging-complexity-3j8k</link>
      <guid>https://dev.to/antonio_zhu_e726fd856cd86/llms-are-lowering-coding-cost-but-they-may-be-increasing-debugging-complexity-3j8k</guid>
      <description>&lt;p&gt;Over the last few months, I have gradually built a fairly stable AI-assisted development workflow for my side projects.&lt;/p&gt;

&lt;p&gt;The workflow looks roughly like this.&lt;/p&gt;

&lt;p&gt;Architecture and design are handled by Claude Sonnet / Opus. I maintain a set of engineering skills and design templates for feature planning. Once the design document is finalized, DeepSeek V4 is responsible for implementation. After coding is complete, I run an independent review workflow inside OpenCode to perform security review, architecture validation, data integrity checks, performance inspection, and test coverage analysis.&lt;/p&gt;

&lt;p&gt;The process is structured enough that I have become fairly confident in it.&lt;/p&gt;

&lt;p&gt;Recently, I decided to redesign the data storage architecture of one of my iOS projects.&lt;/p&gt;

&lt;p&gt;The first version of the application was relatively simple. Data lived entirely in local SQLite storage. Users could manually export and import backup files, but data was stored unencrypted and there was no cloud synchronization.&lt;/p&gt;

&lt;p&gt;During an AI-assisted security review, one issue was raised immediately.&lt;/p&gt;

&lt;p&gt;Sensitive financial data was stored locally without encryption.&lt;/p&gt;

&lt;p&gt;The feedback was correct.&lt;/p&gt;

&lt;p&gt;This triggered a second architecture redesign.&lt;/p&gt;

&lt;p&gt;The new design introduced AES-GCM encryption for all persisted data, and automatic backup to iCloud Drive. The goal was simple: keep user data private, avoid maintaining backend storage infrastructure, and allow users to restore data across devices without trusting a third-party server.&lt;/p&gt;

&lt;p&gt;From a technical perspective, the stack became more complicated.&lt;/p&gt;

&lt;p&gt;Flutter handled the application layer. SQLite remained the local storage engine. Flutter MethodChannel bridged into Swift native code. Swift then interacted with iCloud Documents API for cloud persistence.&lt;/p&gt;

&lt;p&gt;Implementation was generated almost entirely through my AI workflow.&lt;/p&gt;

&lt;p&gt;Everything looked fine.&lt;/p&gt;

&lt;p&gt;Then I ran real-device testing.&lt;/p&gt;

&lt;p&gt;The logs were clean.&lt;/p&gt;

&lt;p&gt;JSON backup generation succeeded.&lt;/p&gt;

&lt;p&gt;Encryption succeeded.&lt;/p&gt;

&lt;p&gt;Flutter successfully invoked native Swift code.&lt;/p&gt;

&lt;p&gt;Swift returned successful file write.&lt;/p&gt;

&lt;p&gt;The application printed the following path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="sr"&gt;/private/var/mobile/Library/Mobile Documents/&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything suggested success.&lt;/p&gt;

&lt;p&gt;Then I opened Files.app on the iPhone.&lt;/p&gt;

&lt;p&gt;Nothing.&lt;/p&gt;

&lt;p&gt;No folder.&lt;/p&gt;

&lt;p&gt;No backup file.&lt;/p&gt;

&lt;p&gt;At first, I assumed iCloud synchronization was delayed.&lt;/p&gt;

&lt;p&gt;I waited ten minutes.&lt;/p&gt;

&lt;p&gt;Still nothing.&lt;/p&gt;

&lt;p&gt;At this point debugging began.&lt;/p&gt;

&lt;p&gt;I started checking the usual suspects.&lt;/p&gt;

&lt;p&gt;Entitlements configuration.&lt;/p&gt;

&lt;p&gt;iCloud container identifier.&lt;/p&gt;

&lt;p&gt;Sandbox permissions.&lt;/p&gt;

&lt;p&gt;Capability configuration.&lt;/p&gt;

&lt;p&gt;Provisioning profile.&lt;/p&gt;

&lt;p&gt;Everything looked correct.&lt;/p&gt;

&lt;p&gt;I went back to AI-assisted debugging.&lt;/p&gt;

&lt;p&gt;I used OpenCode with Claude Opus to review the native Swift implementation.&lt;/p&gt;

&lt;p&gt;No issue found.&lt;/p&gt;

&lt;p&gt;I switched models and repeated the review with DeepSeek.&lt;/p&gt;

&lt;p&gt;No issue found.&lt;/p&gt;

&lt;p&gt;I spent roughly two hours going through logs and rechecking assumptions.&lt;/p&gt;

&lt;p&gt;Eventually I found the problem.&lt;/p&gt;

&lt;p&gt;The Swift implementation used direct file writing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;atomically&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works perfectly in a normal filesystem.&lt;/p&gt;

&lt;p&gt;But iCloud Documents API is not a normal filesystem.&lt;/p&gt;

&lt;p&gt;Direct write does not properly trigger iCloud synchronization.&lt;/p&gt;

&lt;p&gt;The correct implementation requires &lt;code&gt;NSFileCoordinator&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There was a second issue.&lt;/p&gt;

&lt;p&gt;When restoring backup on a new device, the code directly attempted file reading.&lt;/p&gt;

&lt;p&gt;However, files stored in iCloud may not be downloaded locally yet.&lt;/p&gt;

&lt;p&gt;The restore logic needed to call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="nf"&gt;startDownloadingUbiquitousItem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;before attempting to read.&lt;/p&gt;

&lt;p&gt;After fixing both issues, synchronization finally worked.&lt;/p&gt;

&lt;p&gt;At that point I started thinking less about the bug itself, and more about the development process that produced it.&lt;/p&gt;

&lt;p&gt;What I realized is this.&lt;/p&gt;

&lt;p&gt;Throughout the entire development pipeline, every participant was validating &lt;em&gt;code correctness&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Claude validated architecture.&lt;/p&gt;

&lt;p&gt;DeepSeek generated implementation.&lt;/p&gt;

&lt;p&gt;AI review agents inspected security and code quality.&lt;/p&gt;

&lt;p&gt;I manually checked logs and exception handling.&lt;/p&gt;

&lt;p&gt;But nobody validated &lt;em&gt;system behavior&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This distinction feels increasingly important.&lt;/p&gt;

&lt;p&gt;Code correctness and system correctness are not the same thing.&lt;/p&gt;

&lt;p&gt;Historically, software development looked roughly like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Engineers write code.

Engineers review code.

Engineers understand code.

When bugs happen, engineers debug systems they understand.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But modern AI-assisted development increasingly looks different.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;LLM writes code.

Another LLM reviews code.

Code volume expands rapidly.

Engineers understand less of the implementation details.

When bugs happen, debugging cost rises dramatically.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part is that none of the code involved in this bug was obviously incorrect.&lt;/p&gt;

&lt;p&gt;The code compiled.&lt;/p&gt;

&lt;p&gt;The APIs returned success.&lt;/p&gt;

&lt;p&gt;The logs were clean.&lt;/p&gt;

&lt;p&gt;Review passed.&lt;/p&gt;

&lt;p&gt;And yet the system behavior was wrong.&lt;/p&gt;

&lt;p&gt;I suspect this problem will become increasingly common.&lt;/p&gt;

&lt;p&gt;LLMs are very good at generating syntactically correct code.&lt;/p&gt;

&lt;p&gt;They are very good at code review.&lt;/p&gt;

&lt;p&gt;But they remain weak at understanding complex runtime environments.&lt;/p&gt;

&lt;p&gt;Especially when code interacts with operating system boundaries.&lt;/p&gt;

&lt;p&gt;iOS sandbox behavior.&lt;/p&gt;

&lt;p&gt;iCloud synchronization semantics.&lt;/p&gt;

&lt;p&gt;Browser event loops.&lt;/p&gt;

&lt;p&gt;Native runtime state.&lt;/p&gt;

&lt;p&gt;Database isolation.&lt;/p&gt;

&lt;p&gt;OS-level permission systems.&lt;/p&gt;

&lt;p&gt;These are not code generation problems.&lt;/p&gt;

&lt;p&gt;These are system behavior problems.&lt;/p&gt;

&lt;p&gt;The industry currently spends a lot of time discussing how AI improves developer productivity.&lt;/p&gt;

&lt;p&gt;I think we are missing the other half of the equation.&lt;/p&gt;

&lt;p&gt;AI is clearly lowering coding cost.&lt;/p&gt;

&lt;p&gt;But it may also be quietly increasing debugging complexity.&lt;/p&gt;

&lt;p&gt;And as LLM-generated code becomes more common, I increasingly suspect one skill will become more valuable than coding itself.&lt;/p&gt;

&lt;p&gt;Understanding what actually happens &lt;em&gt;after the code runs&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Because in the AI era:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code correctness does not guarantee system correctness.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;A small iCloud synchronization bug reminded me of something I probably should have already known.&lt;/p&gt;

&lt;p&gt;The faster AI helps us write software, the more important system-level verification becomes.&lt;/p&gt;

&lt;p&gt;AI can write code.&lt;/p&gt;

&lt;p&gt;But engineers still need to understand systems.&lt;/p&gt;

&lt;p&gt;And when production breaks, understanding systems matters far more than generating code.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
