<?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: Yuva Kunaal</title>
    <description>The latest articles on DEV Community by Yuva Kunaal (@yuva_kunaal).</description>
    <link>https://dev.to/yuva_kunaal</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%2F3920570%2F784c7f4b-9f9b-46a9-90bc-4fde7f65dde7.jpg</url>
      <title>DEV Community: Yuva Kunaal</title>
      <link>https://dev.to/yuva_kunaal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yuva_kunaal"/>
    <language>en</language>
    <item>
      <title>I built an AI agent skill that turns article concepts into weird, hand-drawn illustrations</title>
      <dc:creator>Yuva Kunaal</dc:creator>
      <pubDate>Fri, 17 Jul 2026 10:03:03 +0000</pubDate>
      <link>https://dev.to/yuva_kunaal/i-built-an-ai-agent-skill-that-turns-article-concepts-into-weird-hand-drawn-illustrations-1b5e</link>
      <guid>https://dev.to/yuva_kunaal/i-built-an-ai-agent-skill-that-turns-article-concepts-into-weird-hand-drawn-illustrations-1b5e</guid>
      <description>&lt;p&gt;Every technical article eventually hits the same wall: you've written something you're proud of, and now you need images. So you reach for the usual suspects — a stock photo of someone pointing at a whiteboard, a generic gradient banner, maybe a flowchart that looks like every other flowchart on the internet. Nobody remembers any of it.&lt;/p&gt;

&lt;p&gt;I wanted the opposite of that. I wanted illustrations that actually &lt;em&gt;say something&lt;/em&gt; — that take a judgment or a process buried in a paragraph and turn it into one small, strange, memorable picture.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;&lt;a href="https://github.com/Yuvakunaal/kunaal-illustrations" rel="noopener noreferrer"&gt;Kunaal's Illustrations&lt;/a&gt;&lt;/strong&gt; — an AI agent skill that does exactly this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;It's not a prompt. It's not a PPT template. It's a &lt;strong&gt;skill&lt;/strong&gt; you drop into your AI agent that teaches it a very specific way of thinking about illustrations.&lt;/p&gt;

&lt;p&gt;Instead of "add a picture here," the agent learns to ask: &lt;em&gt;what's the one cognitive action in this paragraph worth drawing?&lt;/em&gt; Then it draws that — in a consistent hand-drawn style, with a recurring cast of characters who are genuinely part of the scene rather than decoration standing in the corner.&lt;/p&gt;

&lt;p&gt;Here's the boy saying hi. He shows up a lot.&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%2F4jg2f3ka52z9wq2h3wty.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%2F4jg2f3ka52z9wq2h3wty.png" alt="A simple hand-drawn boy with short black hair and a light blue t-shirt, waving, inside a circle on a white background" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: draw the concept, not decoration
&lt;/h2&gt;

&lt;p&gt;This is the whole philosophy in one line: &lt;strong&gt;the character has to do the work.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you could remove the character from the picture and the picture would still make sense, then the character was just decoration — and that's a fail. Every illustration has to hang on a single idea, and a person (or occasionally a very unbothered cat) has to physically act it out.&lt;/p&gt;

&lt;p&gt;A few examples that came out of it:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Too many tasks, zero focus."&lt;/strong&gt; A boy hopping between floating paper islands, exhausted, never actually landing on any of them.&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%2Frt1610n4y2e0kkvybf9m.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%2Frt1610n4y2e0kkvybf9m.png" alt="A boy jumping between three floating paper islands labelled task A, task B, task C, with a label reading focus = 0" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Messy thought becomes structure."&lt;/strong&gt; A girl at her desk with a tangled cloud of thoughts above her head, and clean structured lines forming on the page under her pen.&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%2Fnxqibbhxuioou7b54svy.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%2Fnxqibbhxuioou7b54svy.png" alt="A girl writing at a desk, a tangled thought cloud above her, clean lines appearing below, with labels raw thought, writing begins, structure appears" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Preparation, meet reality."&lt;/strong&gt; A boy throwing his papers in the air because his grades didn't match the effort.&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%2F537x9t3l9lzk76ijco3d.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%2F537x9t3l9lzk76ijco3d.png" alt="A boy frustrated, throwing exam papers in the air in a room" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;None of these needed a caption to land. That's the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The visual DNA
&lt;/h2&gt;

&lt;p&gt;The style is deliberately restrained so everything looks like it came from the same hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pure white background&lt;/strong&gt; — no paper texture, no shadows, no gradients.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Black hand-drawn line art&lt;/strong&gt; with thin, slightly wobbly lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lots of white space&lt;/strong&gt; — the subject only fills about 40–60% of the frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sparse red / orange / blue handwritten labels&lt;/strong&gt;, and only when a label genuinely helps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One image = one idea.&lt;/strong&gt; No cramming a whole chapter into a single picture.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two default characters are a &lt;strong&gt;boy&lt;/strong&gt; (short black hair, light blue t-shirt, grey pants) and a &lt;strong&gt;girl&lt;/strong&gt; (shoulder-length black hair, yellow t-shirt, dark grey pants). They're not mascots — they're the workers doing the absurd job the concept requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it's for (and who it isn't)
&lt;/h2&gt;

&lt;p&gt;It's a great fit if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write English articles, blogs, or Notion docs and want in-text illustrations&lt;/li&gt;
&lt;li&gt;create methodology, knowledge, or AI-workflow content&lt;/li&gt;
&lt;li&gt;like turning abstract judgments into concrete, slightly weird metaphors&lt;/li&gt;
&lt;li&gt;want a reusable visual language that stays consistent across everything you publish&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's &lt;strong&gt;not&lt;/strong&gt; for you if you want polished brand key visuals, corporate flat illustrations, dense architecture diagrams, cutesy emoji-style mascots, or strictly editable vector source files. That's a different tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;The skill runs a small, opinionated workflow rather than firing off a single image:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Digest the text&lt;/strong&gt; — read the article and find the cognitive anchors: the core judgments, the turning points, the before/after moments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan first, draw later&lt;/strong&gt; — output a shot list of 4–8 images. For each one it writes the paragraph it follows, the theme, the core meaning, the structure type, what the character is doing, and suggested labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate one image at a time&lt;/strong&gt; — never stitched together, each carrying exactly one idea.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA against a checklist&lt;/strong&gt; — is the background clean white? Is there enough white space? Is the character actually doing something? Does it accidentally look like a PowerPoint slide? If it fails, regenerate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save and report&lt;/strong&gt; — final PNGs land in a &lt;code&gt;Generated/&lt;/code&gt; folder, with a note on which images are solid and which are optional.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can also stop it at step 2 if you just want the illustration plan and want to draw things yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it out
&lt;/h2&gt;

&lt;p&gt;The fastest path (the way I use it):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;strong&gt;Antigravity&lt;/strong&gt; and select the &lt;strong&gt;Gemini 3.1 Pro (High)&lt;/strong&gt; model.&lt;/li&gt;
&lt;li&gt;Paste the repo's &lt;code&gt;.git&lt;/code&gt; URL into the chat and tell the agent to clone/install it.&lt;/li&gt;
&lt;li&gt;Start prompting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Or install it manually into your agent's skills directory:&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/Yuvakunaal/kunaal-illustrations.git
&lt;span class="nb"&gt;cd &lt;/span&gt;kunaal-illustrations
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; ./kunaals-illustrations ~/.gemini/config/skills/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the prompts are refreshingly simple. Whole-article mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use $kunaals-illustrations to generate 4 bizarre in-text illustrations for the article below.
Requirements: wide composition, pure white background, black hand-drawn line art,
sparse red/orange/blue English handwritten annotations.

&amp;lt;paste your article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Planning-only mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use $kunaals-illustrations, do not generate images yet.
Analyze where this article is worth illustrating and output a shot list of about 5 images.

&amp;lt;paste your article&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Single concept:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use $kunaals-illustrations to generate an in-text illustration for
"Trust is not shouted out, but built piece by piece with evidence."
Make it bizarre but clean, and the boy must carry the core action.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of things I've learned using it: keep the label text short (the fewer words in the image, the more stable the output), and let each picture stay about one idea. AI image models will still occasionally hallucinate a typo or drift on style — when that happens, cutting the number of labels and regenerating usually fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on licensing
&lt;/h2&gt;

&lt;p&gt;The repo is &lt;strong&gt;dual-licensed&lt;/strong&gt;: the skill code, prompt templates, and docs are MIT, and the example images, character IPs, and style references are &lt;strong&gt;CC BY 4.0&lt;/strong&gt;. If you use or adapt the characters or style, please credit &lt;em&gt;"Kunaal's Illustrations" by Kunaal&lt;/em&gt; and link back to the repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take it for a spin
&lt;/h2&gt;

&lt;p&gt;If you write anything technical and you're tired of forgettable stock imagery, I'd love for you to try this on your next post.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/Yuvakunaal/kunaal-illustrations" rel="noopener noreferrer"&gt;https://github.com/Yuvakunaal/kunaal-illustrations&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portfolio:&lt;/strong&gt; &lt;a href="https://kunaal-portfolio.vercel.app/" rel="noopener noreferrer"&gt;https://kunaal-portfolio.vercel.app/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something weird with it, drop it in the comments — I want to see what your agent draws. And a star on the repo genuinely helps.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>showdev</category>
      <category>opensource</category>
      <category>writing</category>
    </item>
    <item>
      <title>When Batching Your AI Agents Makes Them Read Each Other's Mail</title>
      <dc:creator>Yuva Kunaal</dc:creator>
      <pubDate>Thu, 16 Jul 2026 16:29:22 +0000</pubDate>
      <link>https://dev.to/yuva_kunaal/when-batching-your-ai-agents-makes-them-read-each-others-mail-2n33</link>
      <guid>https://dev.to/yuva_kunaal/when-batching-your-ai-agents-makes-them-read-each-others-mail-2n33</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/n8n-io/n8n" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; is an open-source workflow automation platform — think Zapier, but self-hostable, node-based, and built for developers. One of its most-used features is the &lt;strong&gt;AI Agent node&lt;/strong&gt;, which wraps an LLM in a tool-calling loop: the model can call other nodes in your workflow as "tools," see the results, and decide what to do next, across multiple items and multiple rounds.&lt;/p&gt;

&lt;p&gt;To keep costs down and stay under rate limits, the Agent node supports &lt;strong&gt;Batch Processing&lt;/strong&gt; — running several input items through the tool-calling loop concurrently instead of one at a time. That feature is where this bug lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The bug:&lt;/strong&gt; when Batch Processing is enabled with a batch size of 2 or more, and two items in the same batch both need to call a tool in the same round, one item's conversation history silently leaks into the other's from round 2 onward — or gets dropped outright. Concretely: item 0 calls &lt;code&gt;get_weather&lt;/code&gt; for Berlin, item 1 calls &lt;code&gt;get_stock_price&lt;/code&gt; for ACME. By round 3, item 1's rebuilt conversation context contains item 0's Berlin weather call instead of its own stock price call. The LLM processing item 1 is now reasoning from a completely different item's history.&lt;/p&gt;

&lt;p&gt;This isn't a cosmetic bug — it's silent data/context corruption in the exact feature (tool-calling memory) that determines whether an AI agent gives a correct answer. And it fails silently: no exception, no error in the UI, just a wrong answer built from someone else's data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause&lt;/strong&gt;, traced through the actual engine round-trip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;executeBatch.ts&lt;/code&gt; merges all items' outgoing requests for a round into one shared object. When it does, it keeps only the &lt;strong&gt;first&lt;/strong&gt; item's &lt;code&gt;metadata&lt;/code&gt; (which includes &lt;code&gt;previousRequests&lt;/code&gt; — that item's own tool-call history) and silently discards every other item's.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;buildSteps.ts&lt;/code&gt;, when rebuilding an item's conversation for the next round, splices whatever &lt;code&gt;previousRequests&lt;/code&gt; survived that merge into &lt;strong&gt;every&lt;/strong&gt; item's steps, completely unconditionally — no filtering by which item the history actually belongs to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put together: whichever item happens to go first in the batch "wins" the shared history slot, and every other item inherits it instead of its own.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Issue: &lt;a href="https://github.com/n8n-io/n8n/issues/34359" rel="noopener noreferrer"&gt;https://github.com/n8n-io/n8n/issues/34359&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PR: &lt;a href="https://github.com/n8n-io/n8n/pull/34360" rel="noopener noreferrer"&gt;https://github.com/n8n-io/n8n/pull/34360&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;I confirmed the bug with a deterministic (non-flaky) regression test before touching any source: mock two batch items each returning distinct tool-call data, run them through &lt;code&gt;executeBatch&lt;/code&gt;, and assert on the merged result. It failed exactly as the root-cause analysis predicted — item 1's data was gone, item 0's leaked everywhere.&lt;/p&gt;

&lt;p&gt;The fix has three small, surgical pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tag, don't guess.&lt;/strong&gt; Added an optional &lt;code&gt;itemIndex&lt;/code&gt; field to &lt;code&gt;ToolCallData&lt;/code&gt; (the type representing one tool-call step), and tag every step with it the moment it's created in &lt;code&gt;buildSteps.ts&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Merge, don't discard.&lt;/strong&gt; In &lt;code&gt;executeBatch.ts&lt;/code&gt;'s batch merge, instead of keeping only the first item's &lt;code&gt;previousRequests&lt;/code&gt;, concatenate every contributing item's array.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filter, don't splice blindly.&lt;/strong&gt; In &lt;code&gt;buildSteps.ts&lt;/code&gt;, when rebuilding an item's steps for the next round, filter the merged &lt;code&gt;previousRequests&lt;/code&gt; down to just that item's own tagged entries before splicing them in.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// buildSteps.ts — filter on the way back in&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;previousRequests&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previousRequests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;step&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;itemIndex&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;itemIndex&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="c1"&gt;// executeBatch.ts — merge instead of discarding&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;batchResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;otherPreviousRequests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;batchResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;previousRequests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;otherPreviousRequests&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&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="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;previousRequests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[...(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;previousRequests&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="nx"&gt;otherPreviousRequests&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One deliberate scoping call: this same root cause also means each item's &lt;code&gt;iterationCount&lt;/code&gt; (used for the "max iterations" circuit breaker) is shared across the batch rather than tracked per item. Fixing that properly would mean restructuring the whole shared-metadata shape — a bigger, riskier change. I documented it as a known follow-up instead of folding it into this PR, to keep the diff small, reviewable, and focused on the one bug with an unambiguous, demonstrable before/after.&lt;/p&gt;

&lt;p&gt;Verified: 2 new regression tests, both confirmed failing against the unfixed code and passing after the fix, plus all 338 existing tests in the Agent/agent-execution suite still green — no regressions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;I wanted evidence that wasn't just "trust me, I read the code" — so I wrote a small demo harness that runs the &lt;em&gt;real&lt;/em&gt; project code (the actual &lt;code&gt;executeBatch&lt;/code&gt;/&lt;code&gt;buildSteps&lt;/code&gt; functions, not a toy reproduction) through the exact two-item batch scenario, instrumented with Sentry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before the fix:&lt;/strong&gt; running the scenario produced two real Sentry warning events — &lt;em&gt;"item 1's conversation history contains item 0's tool-call data"&lt;/em&gt; and &lt;em&gt;"item 1's own tool-call history was dropped during batch merge"&lt;/em&gt; — plus a trace span tagged &lt;code&gt;cross_item_leak_detected: true&lt;/code&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%2Fjtpvhudk1a78t28akyoq.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%2Fjtpvhudk1a78t28akyoq.png" alt="Sentry Issues feed showing both warning events" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After the fix:&lt;/strong&gt; same scenario, same instrumentation — zero warning events, and the trace span flips to &lt;code&gt;cross_item_leak_detected: false&lt;/code&gt;, with both items' data intact.&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%2Fppqjbey16hr5fzqgzgk2.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%2Fppqjbey16hr5fzqgzgk2.png" alt="before trace span attributes" width="800" height="520"&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%2F1uzhb1iljx5tcqkxiv1y.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%2F1uzhb1iljx5tcqkxiv1y.png" alt="after trace span attributes" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I ran &lt;strong&gt;Seer&lt;/strong&gt; (Sentry's AI root-cause tool) on the "before" issue. What came back was remarkable — Seer's root cause analysis independently identified the &lt;em&gt;exact&lt;/em&gt; mechanism (the unconditional splice in &lt;code&gt;buildSteps&lt;/code&gt;, no per-item partitioning), citing the real files in my repo (&lt;code&gt;executeBatch.ts&lt;/code&gt;, &lt;code&gt;buildSteps.ts&lt;/code&gt; lines 316–395, &lt;code&gt;prepareItemContext.ts&lt;/code&gt;, &lt;code&gt;runAgent.ts&lt;/code&gt;) as evidence — meaning it actually read the connected repo, not just the event text.&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%2Fla1n5gtsi8zrwyv1h7j5.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%2Fla1n5gtsi8zrwyv1h7j5.png" alt="Seer Root Cause panel" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I let it go further: "Yes, make a plan" produced a 4-step plan that matches my actual fix &lt;strong&gt;step for step&lt;/strong&gt; — tag &lt;code&gt;ToolCallData&lt;/code&gt; with &lt;code&gt;itemIndex&lt;/code&gt;, filter in &lt;code&gt;buildSteps&lt;/code&gt;, merge in &lt;code&gt;executeBatch&lt;/code&gt; — down to the same line numbers. Then "Yes, write a code fix" produced a diff nearly identical to what I'd already shipped.&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%2Fzoolbma6qagqalbdkrvo.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%2Fzoolbma6qagqalbdkrvo.png" alt="Seer Plan panel" width="800" height="520"&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%2Fgcza76k8tardzkp59itl.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%2Fgcza76k8tardzkp59itl.png" alt="Seer generated code diff" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's four independent analyses — mine, Seer's root cause, Seer's plan, and (see below) Gemini's — all converging on the same diagnosis and the same fix. The one genuine difference is worth calling out honestly rather than glossing over: Seer's generated filter also let untagged (&lt;code&gt;itemIndex === undefined&lt;/code&gt;) entries pass through to every item as a backward-compat safety net, where mine is strict. In this codebase that difference never manifests (every step is unconditionally tagged going forward), but it's a real, spottable design choice, not a rubber stamp.&lt;/p&gt;

&lt;p&gt;And the most useful gap Seer's autofix exposed: its generated diff shipped with &lt;strong&gt;zero tests&lt;/strong&gt;. n8n's own contribution guidelines require tests on every PR (unlabeled PRs without them auto-close after 14 days) — so even a diff this close to correct wouldn't be a mergeable submission on its own. That's the concrete difference between an AI-generated patch and a human-verified fix: mine ships with regression tests confirmed failing before, passing after.&lt;/p&gt;

&lt;p&gt;I also let Seer draft its own PR from the plan — it created &lt;a href="https://github.com/Yuvakunaal/n8n/pull/2" rel="noopener noreferrer"&gt;PR #2 on my fork&lt;/a&gt;, a nice piece of evidence that Sentry can go all the way from a captured error to an autonomous, code-aware pull request. My actual submission (&lt;a href="https://github.com/n8n-io/n8n/pull/34360" rel="noopener noreferrer"&gt;PR #34360&lt;/a&gt;) is the tested, human-verified version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I ran a three-message debugging session with Gemini, feeding it the real (unfixed) code and the symptom, without telling it my own conclusions first, to see if it would independently arrive at the same root cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt 1&lt;/strong&gt; asked it to diagnose the bug from the &lt;code&gt;executeBatch.ts&lt;/code&gt; merge logic and &lt;code&gt;buildSteps.ts&lt;/code&gt; splice, given the symptom (item 1 inheriting item 0's history by round 3). Gemini correctly identified the reference-hijacking merge and the unconditional splice as the mechanism. It also flagged a "secondary bug" — that &lt;code&gt;request.actions.push.apply(...)&lt;/code&gt; mutates item 0's original actions array by reference. Worth being honest about this one: it's &lt;em&gt;technically&lt;/em&gt; true (same array object), but I don't think it's actually a separate bug here — nothing else reads that reference afterward, and merging all items' actions into one array is the intended behavior anyway. Good reminder that AI analysis needs to be checked against real usage, not just taken at face value.&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%2Fk9ndfqwi219vsmajmyp2.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%2Fk9ndfqwi219vsmajmyp2.png" alt="Gemini prompt 1 response" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt 2&lt;/strong&gt; asked what the smallest fix would be, and specifically what breaks if you patch &lt;em&gt;only&lt;/em&gt; &lt;code&gt;buildSteps.ts&lt;/code&gt; without also fixing the merge in &lt;code&gt;executeBatch.ts&lt;/code&gt;. Gemini got this right and explained it well: if you only filter in &lt;code&gt;buildSteps&lt;/code&gt;, item 1's data isn't there to filter — it was already discarded two rounds earlier. Filtering a dataset that no longer exists doesn't help; you'd trade cross-contamination for outright data loss (and, plausibly, an agent that repeats tool calls because it's forgotten it already made them). This matches exactly what I found empirically in the Sentry demo.&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%2Fnf973q17vd3ukphe58uq.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%2Fnf973q17vd3ukphe58uq.png" alt="Gemini prompt 2 response" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prompt 3&lt;/strong&gt; shared my actual shipped diff and asked Gemini to sanity-check it, including the deliberate &lt;code&gt;iterationCount&lt;/code&gt; scoping decision. It confirmed the fix closes the conversation-history gap, and caught something I hadn't stated as sharply: because the merge spreads &lt;code&gt;request.metadata&lt;/code&gt; (item 0's) and only explicitly overwrites &lt;code&gt;previousRequests&lt;/code&gt;, &lt;em&gt;any other&lt;/em&gt; per-item metadata field beyond &lt;code&gt;previousRequests&lt;/code&gt; — not just &lt;code&gt;iterationCount&lt;/code&gt; — still silently reflects only the first item's value. That's a more precise, general framing of the same follow-up I'd already scoped out, and it made its way into my PR description as a result.&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%2F3bsqv5wnrb2mphx8ae5j.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%2F3bsqv5wnrb2mphx8ae5j.png" alt="Gemini prompt 3 response" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Across all three prompts, Gemini converged on the same diagnosis as my own analysis and Sentry's Seer — but the value wasn't in blind agreement, it was in the two places I could point at and say "that's not quite right" (the array-mutation non-bug) or "that's sharper than what I'd written" (the general metadata-field caveat). That back-and-forth is what actually made the PR description better.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Open-Sourced an AI Skill for Generating Minimalist Hand-Drawn Illustrations</title>
      <dc:creator>Yuva Kunaal</dc:creator>
      <pubDate>Tue, 23 Jun 2026 07:00:22 +0000</pubDate>
      <link>https://dev.to/yuva_kunaal/i-open-sourced-an-ai-skill-for-generating-minimalist-hand-drawn-illustrations-1395</link>
      <guid>https://dev.to/yuva_kunaal/i-open-sourced-an-ai-skill-for-generating-minimalist-hand-drawn-illustrations-1395</guid>
      <description>&lt;h1&gt;
  
  
  I Open-Sourced an AI Skill for Generating Minimalist Hand-Drawn Illustrations
&lt;/h1&gt;

&lt;p&gt;Most AI-generated illustrations today look highly polished, glossy, and over-rendered.&lt;/p&gt;

&lt;p&gt;While that's impressive, I wanted something different.&lt;/p&gt;

&lt;p&gt;I wanted illustrations that feel simple, human, playful, and intentionally imperfect — the kind of sketches you'd find in a notebook or on a whiteboard.&lt;/p&gt;

&lt;p&gt;So I built and open-sourced &lt;strong&gt;Kunaal's Illustrations&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal
&lt;/h2&gt;

&lt;p&gt;The objective was straightforward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimalist black line art&lt;/li&gt;
&lt;li&gt;Lots of whitespace&lt;/li&gt;
&lt;li&gt;Wobbly hand-drawn aesthetics&lt;/li&gt;
&lt;li&gt;Consistent character style&lt;/li&gt;
&lt;li&gt;Easy generation through AI prompts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of creating photorealistic artwork, the focus is on communication and storytelling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Can It Generate?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cozy Lifestyle Scenes
&lt;/h3&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Boy reading a book with a coffee&lt;/li&gt;
&lt;li&gt;Girl working on a laptop&lt;/li&gt;
&lt;li&gt;Friends chatting at a dining table&lt;/li&gt;
&lt;li&gt;Someone studying late at night&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Technical Concepts
&lt;/h3&gt;

&lt;p&gt;It can also visualize abstract ideas such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex server architectures&lt;/li&gt;
&lt;li&gt;API workflows&lt;/li&gt;
&lt;li&gt;System design concepts&lt;/li&gt;
&lt;li&gt;Developer productivity themes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Documentation &amp;amp; Presentations
&lt;/h3&gt;

&lt;p&gt;The illustrations work particularly well in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notion pages&lt;/li&gt;
&lt;li&gt;Blog posts&lt;/li&gt;
&lt;li&gt;Technical documentation&lt;/li&gt;
&lt;li&gt;Slide decks&lt;/li&gt;
&lt;li&gt;Educational content&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Built-in Character Style
&lt;/h2&gt;

&lt;p&gt;The skill includes a consistent visual identity through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reference Boy&lt;/li&gt;
&lt;li&gt;Reference Girl&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These characters provide style consistency while still allowing generation of completely new scenes and concepts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Prompt
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Generate an illustration: A boy and a girl sitting opposite each other at a dining table saying hi and hello.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why Open Source?
&lt;/h2&gt;

&lt;p&gt;I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anyone to use it&lt;/li&gt;
&lt;li&gt;Anyone to improve it&lt;/li&gt;
&lt;li&gt;A reusable illustration style for creators and developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open-source tools become far more useful when the community can adapt and extend them.&lt;/p&gt;

&lt;p&gt;GitHub Repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Yuvakunaal/kunaal-illustrations" rel="noopener noreferrer"&gt;@Yuvakunal/kunaal-illustrations&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Contributions, feedback, and suggestions are welcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you find it useful, consider giving the repository a ⭐.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Happy building!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>art</category>
      <category>github</category>
    </item>
    <item>
      <title>"Wrong Answer" is the worst feedback you can give a SQL learner — so I built something better</title>
      <dc:creator>Yuva Kunaal</dc:creator>
      <pubDate>Mon, 18 May 2026 04:16:54 +0000</pubDate>
      <link>https://dev.to/yuva_kunaal/wrong-answer-is-the-worst-feedback-you-can-give-a-sql-learner-so-i-built-something-better-3pka</link>
      <guid>https://dev.to/yuva_kunaal/wrong-answer-is-the-worst-feedback-you-can-give-a-sql-learner-so-i-built-something-better-3pka</guid>
      <description>&lt;p&gt;You submit a query. It's wrong.&lt;/p&gt;

&lt;p&gt;The platform says: &lt;strong&gt;Incorrect result.&lt;/strong&gt;&lt;/p&gt;

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

&lt;p&gt;No diff. No hint. No explanation of which rows are wrong or why. Just a red banner telling you to try again.&lt;/p&gt;

&lt;p&gt;I've used enough SQL learning tools to know this is the standard. And I think it's the single biggest reason people get stuck and give up — not because SQL is hard, but because the feedback loop is broken.&lt;/p&gt;

&lt;p&gt;So when I built &lt;a href="https://sqlumina.com" rel="noopener noreferrer"&gt;SQLumina&lt;/a&gt;, I made the debug experience the core feature. Here's exactly how it works under the hood.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with "wrong answer"
&lt;/h2&gt;

&lt;p&gt;When a SQL query returns incorrect results, there are maybe 15 different reasons why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wrong JOIN type (INNER when you needed LEFT)&lt;/li&gt;
&lt;li&gt;Joining on the wrong columns (PK to PK instead of FK to PK)&lt;/li&gt;
&lt;li&gt;Missing GROUP BY column&lt;/li&gt;
&lt;li&gt;WHERE clause firing before aggregation&lt;/li&gt;
&lt;li&gt;HAVING threshold off by one&lt;/li&gt;
&lt;li&gt;LIKE missing a wildcard&lt;/li&gt;
&lt;li&gt;NULL comparison written as &lt;code&gt;col = NULL&lt;/code&gt; instead of &lt;code&gt;IS NULL&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these produces a different shape of wrong output. A generic "incorrect" banner treats them all the same. That's useless.&lt;/p&gt;

&lt;p&gt;What a learner actually needs is: &lt;strong&gt;which rows are wrong, and why.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Building the row-level diff
&lt;/h2&gt;

&lt;p&gt;Every challenge in SQLumina has a reference solution stored server-side. When a user submits, the backend runs both queries concurrently against the live PostgreSQL database:&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;user_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;run_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;run_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference_sql&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it computes three sets:&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;correct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_rows&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;reference_rows&lt;/span&gt;
&lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reference_rows&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;user_rows&lt;/span&gt;
&lt;span class="n"&gt;extra&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_rows&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;reference_rows&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These map directly to the three columns in the debug panel: &lt;strong&gt;Correct&lt;/strong&gt;, &lt;strong&gt;Missing&lt;/strong&gt;, &lt;strong&gt;Extra&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The diff table shows full row values — all columns, no truncation. If a row is missing because your JOIN dropped it, you can see exactly which row it was.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Accuracy scoring with Jaccard similarity
&lt;/h2&gt;

&lt;p&gt;Once you have the three sets, you need a single accuracy number.&lt;/p&gt;

&lt;p&gt;I use Jaccard similarity:&lt;br&gt;
accuracy = |correct| / (|correct| + |missing| + |extra|)&lt;/p&gt;

&lt;p&gt;The reason this matters: a simple "percentage correct" would reward over-fetching. If the expected result is 10 rows and you return 10,000, you'd technically have 100% of the correct rows — you'd just also have 9,990 extra ones.&lt;/p&gt;

&lt;p&gt;Jaccard penalises both under-fetching and over-fetching. Getting 15 rows when 10 were expected is scored lower than getting exactly 10 right. This matches how production SQL correctness actually works.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3 — Parsing the query AST
&lt;/h2&gt;

&lt;p&gt;Showing the diff is useful. Explaining &lt;em&gt;why&lt;/em&gt; the diff exists is what makes it genuinely educational.&lt;/p&gt;

&lt;p&gt;This is where it gets interesting.&lt;/p&gt;

&lt;p&gt;I built a 30+ rule heuristic engine that parses the user's actual SQL to extract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Table names and aliases&lt;/li&gt;
&lt;li&gt;JOIN conditions (specifically the ON clause columns)&lt;/li&gt;
&lt;li&gt;SELECT columns and whether they're wrapped in aggregates&lt;/li&gt;
&lt;li&gt;WHERE predicates and their operators&lt;/li&gt;
&lt;li&gt;GROUP BY columns&lt;/li&gt;
&lt;li&gt;HAVING conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then it matches those parsed elements against the diff to produce specific diagnosis cards.&lt;/p&gt;

&lt;p&gt;Example rule — wrong JOIN key detection:&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;check_join_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;diff&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;join&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;joins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;left_col&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;
        &lt;span class="n"&gt;right_col&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;
        &lt;span class="n"&gt;left_ref&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_table_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;right_ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_table_pk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&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;left_col&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;left_ref&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;right_col&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;right_ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;DiagnosisCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;severity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;re joining &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&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;left_col&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;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table&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;right_col&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;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Both are primary keys. You probably want the foreign key instead.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;fix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;generate_corrected_join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The card surfaces in the UI with a severity badge, confidence percentage, the specific columns involved, a corrected SQL fragment, and a plain-language explanation of the underlying concept.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — The zero-rows suite
&lt;/h2&gt;

&lt;p&gt;A separate set of 7 rules fires specifically when the user's query returns zero rows against a non-empty expected result. These are the most frustrating bugs to debug because the diff table is just... empty.&lt;/p&gt;

&lt;p&gt;The rules detect:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Issue&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;col = NULL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Should be &lt;code&gt;IS NULL&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LIKE 'foo'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Missing wildcard — should be &lt;code&gt;LIKE '%foo%'&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BETWEEN hi AND lo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bounds are reversed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;NOT IN (...)&lt;/code&gt; with NULL in list&lt;/td&gt;
&lt;td&gt;NULL silently excludes every row&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date_col &amp;gt; CURRENT_DATE + INTERVAL '30 days'&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Future filter — should subtract&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WHERE clause over-filtered&lt;/td&gt;
&lt;td&gt;Too many AND conditions, no rows survive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No WHERE when one is required&lt;/td&gt;
&lt;td&gt;Missing filter entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each zero-row rule names the exact predicate from the user's query and suggests the corrected version. No generic messaging.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5 — The pgBouncer bug that took me days
&lt;/h2&gt;

&lt;p&gt;This one isn't about the debugger directly. It's about the infrastructure that makes sandboxed query execution reliable — and it's the bug that cost me the most time.&lt;/p&gt;

&lt;p&gt;SQLumina runs on Neon's serverless PostgreSQL with asyncpg. Each user query runs inside a schema — &lt;code&gt;ecommerce&lt;/code&gt;, &lt;code&gt;social_media&lt;/code&gt;, or &lt;code&gt;banking&lt;/code&gt; — and sandbox queries run in an isolated &lt;code&gt;sandbox_{user_id}&lt;/code&gt; schema.&lt;/p&gt;

&lt;p&gt;To route queries to the right schema, I was setting the search path before executing:&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;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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;SET search_path TO &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In development: worked perfectly.&lt;/p&gt;

&lt;p&gt;In production under any real load: randomly failing with &lt;code&gt;relation "products" does not exist&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Not consistently. Not reproducibly. Just sometimes, on concurrent requests.&lt;/p&gt;

&lt;p&gt;The root cause: &lt;strong&gt;Neon uses pgBouncer in transaction mode.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In transaction mode, pgBouncer doesn't guarantee that two consecutive statements from the same application connection hit the same backend PostgreSQL connection. The &lt;code&gt;SET search_path&lt;/code&gt; runs on connection A. The actual query lands on connection B — which has a fresh search path pointing at &lt;code&gt;public&lt;/code&gt;, where your tables don't exist.&lt;/p&gt;

&lt;p&gt;The fix:&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;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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;SET LOCAL search_path TO &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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;SET LOCAL statement_timeout = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;5000&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SET LOCAL&lt;/code&gt; scopes the change to the current transaction. Since the transaction is atomic, both statements are guaranteed to execute on the same backend connection. pgBouncer can't split them.&lt;/p&gt;

&lt;p&gt;Two lines. Took days to find. Now it never fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're building on Neon and hitting "relation does not exist" errors randomly under load — this is your fix.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6 — Sandboxing safely
&lt;/h2&gt;

&lt;p&gt;A few things that keep user queries from doing damage:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comment stripping&lt;/strong&gt; — SQL comments (&lt;code&gt;--&lt;/code&gt; and &lt;code&gt;/* */&lt;/code&gt;) are stripped before parsing so users can't hide keywords inside comments to bypass validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Destructive operation blocking&lt;/strong&gt; — &lt;code&gt;DROP DATABASE&lt;/code&gt;, &lt;code&gt;TRUNCATE&lt;/code&gt; outside sandbox scope, and &lt;code&gt;GRANT&lt;/code&gt; are blocked at the parser level before execution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-LIMIT&lt;/strong&gt; — &lt;code&gt;LIMIT 1000&lt;/code&gt; is appended when absent so a &lt;code&gt;SELECT *&lt;/code&gt; on a large table doesn't flood the frontend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeout&lt;/strong&gt; — &lt;code&gt;SET LOCAL statement_timeout = '5000'&lt;/code&gt; kills queries at the DB level after 5 seconds. A Python &lt;code&gt;asyncio.wait_for&lt;/code&gt; at 6 seconds is a belt-and-suspenders fallback — PostgreSQL cancels cleanly before Python times out, which prevents dirty pool connections.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read-only enforcement&lt;/strong&gt; — The main &lt;code&gt;ecommerce&lt;/code&gt;, &lt;code&gt;social_media&lt;/code&gt;, and &lt;code&gt;banking&lt;/code&gt; schemas are strictly read-only from the Playground. Only the user's own sandbox schema allows DML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-user isolation&lt;/strong&gt; — Every user gets their own &lt;code&gt;sandbox_{user_id}&lt;/code&gt; schema. Other users' tables are completely invisible. Schema names are case-folded and sanitised before interpolation so two Clerk IDs that differ only in case can never collide.&lt;/p&gt;




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

&lt;p&gt;Building this taught me more SQL than taking any course did.&lt;/p&gt;

&lt;p&gt;You can't write a rule that detects "you're joining on two primary keys" without understanding deeply why that's wrong and what the correct foreign key relationship should look like. You can't write the zero-rows suite without knowing every way a query can silently exclude all its data.&lt;/p&gt;

&lt;p&gt;The debug engine is now 30+ rules. It's also the thing I'm most proud of in the entire codebase.&lt;/p&gt;

&lt;p&gt;If you're building anything where users write queries against real data — especially in a learning context — invest in the feedback layer before anything else. The SQL execution part is easy. Telling someone &lt;em&gt;specifically&lt;/em&gt; what they got wrong is the hard problem.&lt;/p&gt;




&lt;p&gt;SQLumina is about to launch...&lt;/p&gt;

</description>
      <category>sql</category>
      <category>python</category>
      <category>beginners</category>
      <category>backend</category>
    </item>
    <item>
      <title>CommitAI — Local AI-Powered Git Assistant Using Gemma 4</title>
      <dc:creator>Yuva Kunaal</dc:creator>
      <pubDate>Fri, 08 May 2026 20:56:09 +0000</pubDate>
      <link>https://dev.to/yuva_kunaal/commitai-local-ai-powered-git-assistant-using-gemma-4-5027</link>
      <guid>https://dev.to/yuva_kunaal/commitai-local-ai-powered-git-assistant-using-gemma-4-5027</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-gemma-2026-05-06"&gt;Gemma 4 Challenge: Build with Gemma 4&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;As developers, we repeatedly write commit messages, maintain changelogs, and switch context during development. I wanted to build a lightweight developer tool that could automate this workflow completely offline without relying on cloud APIs or external AI services.&lt;/p&gt;

&lt;p&gt;So I built CommitAI — a local AI-powered Git assistant using Gemma 4 + Ollama.&lt;/p&gt;

&lt;p&gt;The workflow is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read staged Git diff&lt;/li&gt;
&lt;li&gt;Send it to a local Gemma 4 model&lt;/li&gt;
&lt;li&gt;Generate a clean Conventional Commit message&lt;/li&gt;
&lt;li&gt;Update changelog automatically&lt;/li&gt;
&lt;li&gt;Execute the Git commit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also added Git hook integration so the workflow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the AI handles the rest.&lt;/p&gt;

&lt;p&gt;The project is built entirely in Python and runs locally on my MacBook Air M2 (8GB RAM).&lt;/p&gt;

&lt;h3&gt;
  
  
  Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;AI-generated Conventional Commit messages&lt;/li&gt;
&lt;li&gt;Automatic changelog generation&lt;/li&gt;
&lt;li&gt;Git hook integration&lt;/li&gt;
&lt;li&gt;Fully local/offline workflow&lt;/li&gt;
&lt;li&gt;Rich CLI interface using the Rich library&lt;/li&gt;
&lt;li&gt;No external APIs&lt;/li&gt;
&lt;li&gt;Privacy-friendly local inference&lt;/li&gt;
&lt;li&gt;Uses staged Git diffs as context&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;a href="https://drive.google.com/file/d/1-vuOSi-TYU3a_f5aVssizppuhXGfzslf/view?usp=sharing" rel="noopener noreferrer"&gt;Demo Video&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/Yuvakunaal/CommitAI" rel="noopener noreferrer"&gt;CommitAI Repo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Used Gemma 4
&lt;/h2&gt;

&lt;p&gt;I used the gemma4:e2b model through Ollama for all AI-powered functionality inside CommitAI.&lt;/p&gt;

&lt;p&gt;I specifically chose the E2B model because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it runs efficiently on local hardware&lt;/li&gt;
&lt;li&gt;lightweight enough for my MacBook Air M2 with 8GB RAM&lt;/li&gt;
&lt;li&gt;low-latency responses for CLI workflows&lt;/li&gt;
&lt;li&gt;strong enough reasoning for summarizing Git diffs&lt;/li&gt;
&lt;li&gt;ideal for privacy-focused offline tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gemma 4 powers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;commit message generation&lt;/li&gt;
&lt;li&gt;changelog summarization&lt;/li&gt;
&lt;li&gt;Git workflow automation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Git Diff
   ↓
CommitAI
   ↓
Ollama API
   ↓
Gemma 4 (E2B)
   ↓
AI-generated commit/changelog
   ↓
Git commit execution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;Ollama&lt;/li&gt;
&lt;li&gt;Gemma 4 (gemma4:e2b)&lt;/li&gt;
&lt;li&gt;Rich&lt;/li&gt;
&lt;li&gt;Git Hooks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Future Improvements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;PR description generation&lt;/li&gt;
&lt;li&gt;Multi-model support&lt;/li&gt;
&lt;li&gt;Better diff summarization&lt;/li&gt;
&lt;li&gt;VSCode extension&lt;/li&gt;
&lt;li&gt;Team changelog modes&lt;/li&gt;
&lt;li&gt;Interactive commit editing&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;One of my biggest goals with this project was proving that useful AI developer tooling can run fully locally without depending on cloud APIs.&lt;/p&gt;

&lt;p&gt;Thanks for reading 🚀&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemmachallenge</category>
      <category>gemma</category>
      <category>python</category>
    </item>
    <item>
      <title>I built a free AI Flowchart Studio — just launched today! 🚀</title>
      <dc:creator>Yuva Kunaal</dc:creator>
      <pubDate>Fri, 08 May 2026 18:27:40 +0000</pubDate>
      <link>https://dev.to/yuva_kunaal/i-built-a-free-ai-flowchart-studio-just-launched-today-46bl</link>
      <guid>https://dev.to/yuva_kunaal/i-built-a-free-ai-flowchart-studio-just-launched-today-46bl</guid>
      <description>&lt;p&gt;Hey DEV community! 👋&lt;/p&gt;

&lt;p&gt;Just launched AI Flowchart Studio today — a tool that converts &lt;br&gt;
plain English into professional flowcharts using a Multi-Agent &lt;br&gt;
Gemini AI pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Existing tools like Lucidchart are too heavy for quick diagrams.&lt;br&gt;
I wanted something where you just DESCRIBE and it builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the AI Works (4-Stage Pipeline)
&lt;/h2&gt;

&lt;p&gt;This is the part I'm most proud of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Orchestrator Agent&lt;/strong&gt; — Validates if the prompt can 
logically become a flowchart&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic Parser Agent&lt;/strong&gt; — Breaks text into a structured 
JSON graph (nodes + edges)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generator Agent&lt;/strong&gt; — Converts the graph into Mermaid.js 
syntax&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Syntax Validator&lt;/strong&gt; — Final check to guarantee perfect 
visual output&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pipeline ensures "Garbage In" never becomes "Garbage Out" 🎯&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Frontend:&lt;/strong&gt; Vanilla JS, Mermaid.js, html2canvas, CSS3 &lt;br&gt;
Glassmorphism — deployed on Vercel&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI (Python), Google GenAI SDK, &lt;br&gt;
Server-Sent Events for real-time streaming — deployed on Render&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🤖 Multi-Agent AI generation&lt;/li&gt;
&lt;li&gt;✏️ Manual builder (click &amp;amp; type, no coding)&lt;/li&gt;
&lt;li&gt;🔄 Undo/Redo engine with memory buffer&lt;/li&gt;
&lt;li&gt;📤 Export PNG (3x super-scaled), SVG, or Mermaid code&lt;/li&gt;
&lt;li&gt;💾 5 concurrent saved projects&lt;/li&gt;
&lt;li&gt;📱 Mobile-responsive with bottom-sheet menus&lt;/li&gt;
&lt;li&gt;🔍 Hardware-accelerated canvas zoom (+/- 300%)&lt;/li&gt;
&lt;li&gt;🔑 BYOK — your API key never leaves your browser&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Privacy First (BYOK)
&lt;/h2&gt;

&lt;p&gt;Your Gemini API key is stored only in your browser's &lt;br&gt;
LocalStorage. Our backend acts purely as a secure proxy — &lt;br&gt;
zero data retention.&lt;/p&gt;

&lt;p&gt;🔗 Try it: &lt;a href="https://ai-flowchart-studio.vercel.app/" rel="noopener noreferrer"&gt;AI Flowchart Studio&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🚀 Also live on Product Hunt today — support appreciated!&lt;br&gt;
&lt;a href="https://www.producthunt.com/products/ai-flowchart-studio?utm_source=other&amp;amp;utm_medium=social" rel="noopener noreferrer"&gt;Product Hunt Launch&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love feedback from devs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What would make you use this daily?&lt;/li&gt;
&lt;li&gt;Any architecture improvements you'd suggest?&lt;/li&gt;
&lt;li&gt;Would you pay for a hosted version (no API key needed)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading! 🙏&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>ai</category>
      <category>buildinpublic</category>
    </item>
  </channel>
</rss>
