<?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: Yuvraj Angad Singh</title>
    <description>The latest articles on DEV Community by Yuvraj Angad Singh (@yuvrajangadsingh).</description>
    <link>https://dev.to/yuvrajangadsingh</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%2F3757408%2Fbeedae41-8e6f-4c97-a05c-19c16dfebfa6.jpeg</url>
      <title>DEV Community: Yuvraj Angad Singh</title>
      <link>https://dev.to/yuvrajangadsingh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yuvrajangadsingh"/>
    <language>en</language>
    <item>
      <title>I scored 15 AI coding tools with a linter. The gap between them is 8x.</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Thu, 06 Aug 2026 18:07:07 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/i-scored-15-ai-coding-tools-with-a-linter-the-gap-between-them-is-8x-3lp9</link>
      <guid>https://dev.to/yuvrajangadsingh/i-scored-15-ai-coding-tools-with-a-linter-the-gap-between-them-is-8x-3lp9</guid>
      <description>&lt;p&gt;I build vibecheck, a CLI that catches what AI coding tools leave behind. Dead scaffolding, invented benchmarks, fake attribution comments, &lt;code&gt;catch&lt;/code&gt; blocks that log and swallow. 39 rules, runs in CI.&lt;/p&gt;

&lt;p&gt;Last month I added a score. One number for how much of that a codebase carries, per thousand lines, severity weighted, capped so no single rule can dominate. 50 means typical, calibrated against a corpus of real repos.&lt;/p&gt;

&lt;p&gt;Then it occurred to me that I had never pointed it at the tools that write the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Fifteen AI coding tools and agent frameworks, scanned at HEAD on August 6:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;repo&lt;/th&gt;
&lt;th&gt;kloc&lt;/th&gt;
&lt;th&gt;findings/kloc&lt;/th&gt;
&lt;th&gt;score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;langchain&lt;/td&gt;
&lt;td&gt;328&lt;/td&gt;
&lt;td&gt;6.1&lt;/td&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;crewAI&lt;/td&gt;
&lt;td&gt;251&lt;/td&gt;
&lt;td&gt;13.8&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;adk-python&lt;/td&gt;
&lt;td&gt;398&lt;/td&gt;
&lt;td&gt;16.8&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dspy&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;td&gt;18.6&lt;/td&gt;
&lt;td&gt;65&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;litellm&lt;/td&gt;
&lt;td&gt;1864&lt;/td&gt;
&lt;td&gt;20.2&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AutoGPT&lt;/td&gt;
&lt;td&gt;694&lt;/td&gt;
&lt;td&gt;20.5&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;llama_index&lt;/td&gt;
&lt;td&gt;400&lt;/td&gt;
&lt;td&gt;23.3&lt;/td&gt;
&lt;td&gt;58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;codex&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;td&gt;27.1&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenHands&lt;/td&gt;
&lt;td&gt;219&lt;/td&gt;
&lt;td&gt;28.8&lt;/td&gt;
&lt;td&gt;51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cline&lt;/td&gt;
&lt;td&gt;573&lt;/td&gt;
&lt;td&gt;37.2&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LibreChat&lt;/td&gt;
&lt;td&gt;763&lt;/td&gt;
&lt;td&gt;38.6&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gemini-cli&lt;/td&gt;
&lt;td&gt;591&lt;/td&gt;
&lt;td&gt;39.3&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aider&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;44.0&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;autogen&lt;/td&gt;
&lt;td&gt;119&lt;/td&gt;
&lt;td&gt;44.1&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;continue&lt;/td&gt;
&lt;td&gt;276&lt;/td&gt;
&lt;td&gt;50.3&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;Median 53. The calibration says 50 is typical. So as a group these are ordinary. Not cleaner than the average codebase, not worse.&lt;/p&gt;

&lt;p&gt;I expected that, honestly. The thing I did not expect is the spread.&lt;/p&gt;

&lt;h2&gt;
  
  
  8x, in the same room
&lt;/h2&gt;

&lt;p&gt;6.1 to 50.3 findings per thousand lines. These projects are doing broadly the same work, in the same window, against the same problem. One of them carries eight times the density of another.&lt;/p&gt;

&lt;p&gt;That is the part worth staring at. It means this is not a property of the domain. Nothing about building an agent framework forces you to the bottom of that table. langchain sits at 6.1 across 328,000 lines and 2,536 files, and I checked that number carefully before believing it: 2,538 Python files in the repo, 2,536 scanned, no config suppressing anything. It is just clean.&lt;/p&gt;

&lt;p&gt;Where the findings actually land, across all fifteen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;code-quality      52,569   69%
ai-tell           18,842   25%
error-handling     2,412    3%
security           1,847    2%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two thirds is code quality, and one rule carries a lot of it. &lt;code&gt;no-deep-nesting&lt;/code&gt; hit the per-rule cap in 4 of 15 repos, meaning it alone produced more than 20 weighted findings per thousand lines before the cap clipped it. The cap exists precisely so the score does not quietly become a nesting-depth meter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not measure
&lt;/h2&gt;

&lt;p&gt;It measures what 39 rules detect. That is not the same as quality, and I would not hire or fire a library over it.&lt;/p&gt;

&lt;p&gt;Language mix moves it. Python repos trip &lt;code&gt;no-py-print&lt;/code&gt;, TypeScript repos trip &lt;code&gt;no-ts-any&lt;/code&gt;. A repo is partly being scored on which rules its language exposes it to.&lt;/p&gt;

&lt;p&gt;And three of these fifteen (codex, gemini-cli, adk-python) were in the corpus I used to calibrate what "typical" means. So the median being 53 is partly circular. I could have quietly dropped them and shown you a cleaner story. Leaving them in and saying so is more useful than a number I have already tuned to itself.&lt;/p&gt;

&lt;p&gt;Run it yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @yuvrajangadsingh/vibecheck &lt;span class="nt"&gt;--score&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The run found a bug in my own tool
&lt;/h2&gt;

&lt;p&gt;I piped each scan into a script to collect the scores. Five of fifteen came back as JSON parse errors.&lt;/p&gt;

&lt;p&gt;The offsets were 65536 and 73628. That is not a coincidence, that is a buffer.&lt;/p&gt;

&lt;p&gt;stdout to a pipe is asynchronous in node. &lt;code&gt;process.exit()&lt;/code&gt; does not wait for it to drain. I had three exits that run after the report is printed, so &lt;code&gt;vibecheck --format json . | jq&lt;/code&gt; on a large repo returned truncated, invalid JSON. Silently. Exit code unchanged.&lt;/p&gt;

&lt;p&gt;Scanning adk-python:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;output target&lt;/th&gt;
&lt;th&gt;bytes&lt;/th&gt;
&lt;th&gt;valid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;redirected to a file&lt;/td&gt;
&lt;td&gt;2,685,887&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;through a pipe&lt;/td&gt;
&lt;td&gt;131,072&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I introduced it three days ago, in a release whose entire purpose was removing silent failures. The exits were added alongside a feature that reports files the scanner could not read, so that people stop getting a clean bill of health for code nobody looked at. That feature shipped with a new way to lose output quietly.&lt;/p&gt;

&lt;p&gt;Every one of my CLI tests already runs through &lt;code&gt;spawnSync&lt;/code&gt;, which is a pipe. The mechanism was covered. But every fixture produced output small enough to fit in the buffer, so nothing ever had to drain, and the bug sat in a blind spot the tests had walked past dozens of times.&lt;/p&gt;

&lt;p&gt;It is fixed in 1.20.1. The new fixture emits 1,800 findings on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from this
&lt;/h2&gt;

&lt;p&gt;The leaderboard is the less interesting half.&lt;/p&gt;

&lt;p&gt;I set out to measure other people's code and the measurement apparatus broke, in a way that had been shipping for three days, in the exact area I had just spent a week hardening. Not because the tests were missing. Because they all sat on one side of a boundary I had never thought about.&lt;/p&gt;

&lt;p&gt;If you want to find out where your blind spots are, run your thing against something large and unfamiliar. Fixtures you wrote share your assumptions. A stranger's 400,000-line repo does not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yuvrajangadsingh/vibecheck" rel="noopener noreferrer"&gt;github.com/yuvrajangadsingh/vibecheck&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tooling</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I ran my own linter against my own app. All 19 findings were wrong.</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Tue, 04 Aug 2026 20:02:54 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/i-ran-my-own-linter-against-my-own-app-all-19-findings-were-wrong-146f</link>
      <guid>https://dev.to/yuvrajangadsingh/i-ran-my-own-linter-against-my-own-app-all-19-findings-were-wrong-146f</guid>
      <description>&lt;p&gt;I build vibecheck, an npm CLI that catches what AI coding tools leave behind. Dead scaffolding, invented benchmarks, fake attribution comments, that sort of thing. 39 rules, runs in CI.&lt;/p&gt;

&lt;p&gt;Last week I pointed it at a Vite and React app I had been building on the side. Not a fixture. Real code I actually ship.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;no-console-pollution&lt;/code&gt; fired 19 times.&lt;/p&gt;

&lt;p&gt;I went to clean them up and stopped after the first one. It was fine. So was the second. I checked all 19.&lt;/p&gt;

&lt;h2&gt;
  
  
  All 19 were wrong
&lt;/h2&gt;

&lt;p&gt;Thirteen looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;rawBrowse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;browseId&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="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEV&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[playlist] browse response&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;browseId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&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;That block does not exist in production. Vite evaluates &lt;code&gt;import.meta.env.DEV&lt;/code&gt; at build time, the branch becomes &lt;code&gt;if (false)&lt;/code&gt;, and the minifier deletes it. The console call being warned about is not in the bundle I ship. It never was.&lt;/p&gt;

&lt;p&gt;The other six were in &lt;code&gt;scripts/test-player.mjs&lt;/code&gt;, a CLI I run by hand. Printing to stdout is the entire point of that file. Calling it pollution is like warning that a print statement is going to print.&lt;/p&gt;

&lt;p&gt;So the rule was not slightly noisy. On this codebase it had a zero percent hit rate, and every one of those 19 warnings was asking me to make my code worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it could not see the guard
&lt;/h2&gt;

&lt;p&gt;Here is the rule as it shipped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-console-pollution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.(&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;,
&lt;/span&gt;  &lt;span class="nx"&gt;antiPattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;eslint&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;disable&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nx"&gt;keep&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nx"&gt;logger&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The obvious fix is to add &lt;code&gt;import.meta.env.DEV&lt;/code&gt; to that &lt;code&gt;antiPattern&lt;/code&gt;. It does not work, and the reason is one line in the scanner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;antiPattern&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;antiPattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;continue&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;line&lt;/code&gt;. Singular. The engine hands each rule one line at a time, so &lt;code&gt;antiPattern&lt;/code&gt; can only ever see the line the match is on. The guard is one to three lines above. An antiPattern would have caught this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEV&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ready&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and missed all thirteen real ones.&lt;/p&gt;

&lt;p&gt;This is the part I think generalises. A single-line regex rule cannot express "is this inside something". Any rule about context needs to see the block, and if your engine is line-scoped you will keep reaching for the anti-pattern escape hatch and keep almost fixing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and the case that made it interesting
&lt;/h2&gt;

&lt;p&gt;I moved the rule to the multiline path, where the detector gets the whole file. Track brace depth, note the depth when a build-time guard opens a block, stay quiet until that block closes.&lt;/p&gt;

&lt;p&gt;Then this test failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEV&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;this one actually ships&lt;/span&gt;&lt;span class="dl"&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;The &lt;code&gt;else&lt;/code&gt; branch is not dev-only. It ships. That console call is a real finding and my fix was swallowing it.&lt;/p&gt;

&lt;p&gt;The reason is that &lt;code&gt;} else {&lt;/code&gt; starts and ends at the same brace depth. If you only compare depth before the line to depth after, nothing changed, so the guard still looks open. You have to track the lowest depth the line passes through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scanBraces&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maskedLine&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;maskedLine&lt;/span&gt;&lt;span class="p"&gt;)&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;ch&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&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;ch&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="o"&gt;--&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;depth&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;depth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;min&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;On &lt;code&gt;} else {&lt;/code&gt; the depth dips below the guard's depth mid-line and comes back. The low-water mark is the only thing that reveals the block closed.&lt;/p&gt;

&lt;p&gt;One more detail: braces are counted on a lexer-masked copy of the line, with strings, template literals, regex bodies and comments blanked first. Otherwise a &lt;code&gt;}&lt;/code&gt; inside a string closes a block that was never open, and every rule downstream drifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  It happened again two days later
&lt;/h2&gt;

&lt;p&gt;I shipped a scoring feature and CI went red. One error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/score.ts:12  error  no-eval  eval() or new Function() allows arbitrary code execution.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line 12 of that file is a comment. It says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt; &lt;span class="o"&gt;*&lt;/span&gt;   &lt;span class="nx"&gt;Severity&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;weighted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nx"&gt;An&lt;/span&gt; &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;call&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;chatty&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt; &lt;span class="nx"&gt;should&lt;/span&gt; &lt;span class="nx"&gt;not&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;
 &lt;span class="o"&gt;*&lt;/span&gt;   &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;same&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;averaging&lt;/span&gt; &lt;span class="nx"&gt;them&lt;/span&gt; &lt;span class="nx"&gt;hides&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My security rule flagged my documentation for containing the word it warns about. Writing about eval is not calling eval.&lt;/p&gt;

&lt;p&gt;Same root cause, different shape: the rule matched raw text when it should have matched code. The fix was a &lt;code&gt;codeOnly&lt;/code&gt; flag that runs the pattern against the lexer-masked line, so strings and comments are invisible to rules that target a code construct.&lt;/p&gt;

&lt;p&gt;Deliberately not applied to every rule. &lt;code&gt;no-ai-todo&lt;/code&gt; and &lt;code&gt;no-ai-attribution&lt;/code&gt; exist to match comment text. Mask comments globally and they never fire again. Both directions have tests now.&lt;/p&gt;

&lt;p&gt;That fix also let me delete an ignore line from my own CI config, which had been quietly excluding the rule directory because the rules describing &lt;code&gt;eval&lt;/code&gt; kept matching themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from this
&lt;/h2&gt;

&lt;p&gt;The console rule had been shipping since v1.0. Nobody filed a bug. It has real users.&lt;/p&gt;

&lt;p&gt;The reason nobody complained is that a false positive in a linter does not look like a bug. It looks like your code being wrong. Most people add the suppression and move on, and the tool never hears about it.&lt;/p&gt;

&lt;p&gt;The only reason I found it is that I ran it against something I cared about, where I knew the right answer for every single line.&lt;/p&gt;

&lt;p&gt;Dogfooding gets talked about as a discipline thing, a virtue. It is not. It is the cheapest way to get a test set where you already know the labels.&lt;/p&gt;

&lt;p&gt;Both fixes are in &lt;code&gt;@yuvrajangadsingh/vibecheck&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @yuvrajangadsingh/vibecheck &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/yuvrajangadsingh/vibecheck" rel="noopener noreferrer"&gt;github.com/yuvrajangadsingh/vibecheck&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tooling</category>
      <category>opensource</category>
    </item>
    <item>
      <title>5 heuristic bugs that made my design-token tool call a bright cream page "dark and moody"</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Mon, 15 Jun 2026 07:36:19 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/5-heuristic-bugs-that-made-my-design-token-tool-call-a-bright-cream-page-dark-and-moody-3b7e</link>
      <guid>https://dev.to/yuvrajangadsingh/5-heuristic-bugs-that-made-my-design-token-tool-call-a-bright-cream-page-dark-and-moody-3b7e</guid>
      <description>&lt;p&gt;i maintain a small cli called &lt;a href="https://github.com/yuvrajangadsingh/brandmd" rel="noopener noreferrer"&gt;brandmd&lt;/a&gt;. it points a headless browser at any url, reads the computed css, and writes a &lt;code&gt;DESIGN.md&lt;/code&gt; that AI coding agents (Claude Code, Cursor, Gemini CLI) can read so they build on-brand UI instead of guessing.&lt;/p&gt;

&lt;p&gt;last week i ran it on cognition.ai's FrontierCode blog post and the output was embarrassing. the page is cream and bright, basically a magazine layout. here is what brandmd said about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;mood: "Dark and moody"&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;#F7F6F5&lt;/code&gt; got named "Light Muted Orange"&lt;/strong&gt; (it is off-white)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;#2200FF&lt;/code&gt; got named "Dark Blue"&lt;/strong&gt; (it is the most electric blue you have ever seen)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;border radii: &lt;code&gt;4px, 3.35544e+07px&lt;/code&gt;&lt;/strong&gt; (yes, scientific notation in a design doc)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;type scale: &lt;code&gt;11px, 11.05px, 11.5px, 12px, 12.5px, 12.75px, 13px&lt;/code&gt;&lt;/strong&gt; (seven sizes that are really three)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;every single one was a different heuristic bug. here is the anatomy of each, because they are the kind of mistakes that look fine in a unit test and fall apart on a real site.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. mood from averaging luminance
&lt;/h2&gt;

&lt;p&gt;the mood line came from averaging the luminance of the whole palette, then bucketing: high average is "light and airy", low is "dark and moody".&lt;/p&gt;

&lt;p&gt;the problem is averaging treats a tiny dark footer the same as a full-viewport cream background. cognition has a dark footer and a few dark chips. they dragged the average down far enough to flip the whole verdict, even though 90% of the screen is cream.&lt;/p&gt;

&lt;p&gt;the fix was to stop averaging and anchor to the dominant background instead. find the background color that covers the most viewport area, judge the mood from that. one color, weighted by how much of the page it actually owns.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bright, high contrast; off-white background dominates with black text and vivid blue accents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that line is evidence-based now. it names the dominant color and what it does, instead of a vibe word.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. color names from nearest-RGB
&lt;/h2&gt;

&lt;p&gt;color naming used nearest-neighbor in RGB space against a list of named colors. cream (&lt;code&gt;#F7F6F5&lt;/code&gt;) has a slight warm cast, so the closest named color by raw RGB distance landed on a muted orange. technically the nearest point. completely wrong to a human.&lt;/p&gt;

&lt;p&gt;RGB distance is a bad model for how we name colors. switched to HSL rules instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lightness above ~90% is a near-white. if the hue is warm it reads as cream, if cool it reads as off-white. it never gets a chromatic name.&lt;/li&gt;
&lt;li&gt;high saturation in the mid lightness band gets "vivid".&lt;/li&gt;
&lt;li&gt;everything else maps hue ranges to names.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;now &lt;code&gt;#F7F6F5&lt;/code&gt; is "off-white" and &lt;code&gt;#2200FF&lt;/code&gt; is "vivid blue".&lt;/p&gt;

&lt;h2&gt;
  
  
  3. blue is "dark" by luminance
&lt;/h2&gt;

&lt;p&gt;this one is the most counterintuitive. &lt;code&gt;#2200FF&lt;/code&gt; is a bright electric blue, but brandmd called it "dark blue". why? because the tone word was derived from luminance, and blue barely contributes to luminance.&lt;/p&gt;

&lt;p&gt;the standard luminance formula weights the channels roughly:&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;luminance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2126&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.7152&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.0722&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;B&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;blue is only 7% of the weight. so a fully saturated blue scores darker than a muted brown. by luminance, &lt;code&gt;#2200FF&lt;/code&gt; genuinely is "dark", even though your eye reads it as loud and bright.&lt;/p&gt;

&lt;p&gt;the fix is to take tone words from HSL lightness, not luminance. lightness treats the channels evenly, so a mid-lightness blue reads as a mid-tone, and the saturation check adds "vivid".&lt;/p&gt;

&lt;h2&gt;
  
  
  4. scientific notation in a design doc
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;3.35544e+07px&lt;/code&gt;. that is what &lt;code&gt;getComputedStyle&lt;/code&gt; returns for &lt;code&gt;border-radius: 9999px&lt;/code&gt; on a pill-shaped button (the browser resolves the "make it fully round" intent into a giant pixel value).&lt;/p&gt;

&lt;p&gt;brandmd was printing the raw computed value straight into the markdown. worse, it was feeding that giant number into the shape-language inference, so a sharp-edged site with one pill button got described as "rounded and friendly".&lt;/p&gt;

&lt;p&gt;two fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;normalize any radius above ~999px to &lt;code&gt;9999px (pill)&lt;/code&gt; for display, keep it CSS-valid.&lt;/li&gt;
&lt;li&gt;exclude pill values from the "is this site rounded or sharp" judgment. judge shape from the largest non-pill radius.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. sub-pixel font sizes are noise
&lt;/h2&gt;

&lt;p&gt;the type scale had seven sizes between 11 and 13px because sub-pixel rendering and zoom produce values like &lt;code&gt;11.05px&lt;/code&gt; and &lt;code&gt;12.75px&lt;/code&gt;. those are not distinct steps in a design system, they are rendering artifacts.&lt;/p&gt;

&lt;p&gt;the fix is a clustering pass: round every size to the nearest 0.5px before ranking, then dedupe. seven noisy values collapse to the three real ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  the lesson that actually mattered
&lt;/h2&gt;

&lt;p&gt;the bug that taught me the most was not in this list directly. while fixing the palette i found that &lt;strong&gt;element count is a terrible proxy for visual dominance.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;brandmd was ranking colors by how many elements used them. a 6% alpha black overlay sat on dozens of tiny chips, so by element count it outranked the actual page background that lived on one big element. the "primary" color was an overlay nobody consciously sees.&lt;/p&gt;

&lt;p&gt;weighting by viewport area share instead of element count fixed the palette ordering in one line. how much of the screen a color owns is a far better signal than how many nodes reference it.&lt;/p&gt;

&lt;h2&gt;
  
  
  try it
&lt;/h2&gt;

&lt;p&gt;all of this shipped in brandmd v0.12. you can reproduce the before and after yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx brandmd@0.11.1 https://cognition.ai/blog/frontier-code   &lt;span class="c"&gt;# the bad output&lt;/span&gt;
npx brandmd@0.12.0 https://cognition.ai/blog/frontier-code   &lt;span class="c"&gt;# the fixed output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the &lt;a href="https://github.com/yuvrajangadsingh/brandmd/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;full changelog&lt;/a&gt; has the side by side. repo is &lt;a href="https://github.com/yuvrajangadsingh/brandmd" rel="noopener noreferrer"&gt;here&lt;/a&gt; if you want to point your coding agent at a real design system instead of letting it guess.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>css</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Gave Claude Stripe's DESIGN.md. The UI Stopped Looking Generic.</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Thu, 21 May 2026 14:11:49 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/i-gave-claude-stripes-designmd-the-ui-stopped-looking-generic-4l13</link>
      <guid>https://dev.to/yuvrajangadsingh/i-gave-claude-stripes-designmd-the-ui-stopped-looking-generic-4l13</guid>
      <description>&lt;p&gt;AI coding agents generate generic UI when they do not know your brand: indigo gradients, lavender backgrounds, rounded cards, default checkmarks.&lt;/p&gt;

&lt;p&gt;I made a controlled before/after: generic AI-style pricing page vs the same page rebuilt using a &lt;code&gt;DESIGN.md&lt;/code&gt; extracted from stripe.com.&lt;/p&gt;

&lt;p&gt;This is not a benchmark of Claude Code auto-loading the skill. It is a demo of what changes when the model has the extracted &lt;code&gt;DESIGN.md&lt;/code&gt; as design context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Without brand context
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fytfmqm2q0sj26qs65q60.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.amazonaws.com%2Fuploads%2Farticles%2Fytfmqm2q0sj26qs65q60.png" alt="pricing page without brand context — generic indigo gradient and lavender background" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Generic Tailwind aesthetic. Indigo-to-violet hero, lavender background, soft purple "MOST POPULAR" pill. Fine. Shippable. Also indistinguishable from a hundred other AI-generated pricing pages.&lt;/p&gt;

&lt;h2&gt;
  
  
  With DESIGN.md context
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F166datktwy7izavy9q39.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.amazonaws.com%2Fuploads%2Farticles%2F166datktwy7izavy9q39.png" alt="pricing page using brandmd-extracted Stripe DESIGN.md — white background, navy text, purple CTA, dark featured card with green CTA" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The branded version uses Stripe-like color roles, type, spacing, radius, and CTA hierarchy from the extracted design context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;#533AFD&lt;/code&gt; for primary CTAs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#00D66F&lt;/code&gt; for success accents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;#0D1738&lt;/code&gt; for primary text&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sohne-var&lt;/code&gt; as the primary font family&lt;/li&gt;
&lt;li&gt;a 4px spacing grid&lt;/li&gt;
&lt;li&gt;4px, 8px, and 16px radii&lt;/li&gt;
&lt;li&gt;a dark featured-card pattern&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I did not hand-pick these values from Stripe's site. They came from the generated &lt;code&gt;DESIGN.md&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx brandmd https://stripe.com &lt;span class="nt"&gt;--agent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DESIGN.md
.cursor/rules/brand.mdc
.claude/skills/brand-style/SKILL.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;DESIGN.md&lt;/code&gt; is the design system. The Cursor rule and Claude Code skill point the agent back to that file, so there is one source of truth instead of duplicated prompt text.&lt;/p&gt;

&lt;p&gt;Here is the extracted Stripe example: &lt;a href="https://github.com/yuvrajangadsingh/brandmd/blob/main/examples/stripe.md" rel="noopener noreferrer"&gt;github.com/yuvrajangadsingh/brandmd/blob/main/examples/stripe.md&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;DESIGN.md&lt;/code&gt; was introduced by Google Stitch as a markdown format for design systems that LLMs can read. brandmd extracts one from a live website so you do not have to write it by hand.&lt;/p&gt;

&lt;p&gt;It works with Claude Code, Cursor, Gemini CLI, Codex, Stitch, MCP servers, and anything that reads markdown context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx brandmd https://linear.app &lt;span class="nt"&gt;--agent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask your coding agent to build a UI component and follow the generated brand context.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/yuvrajangadsingh/brandmd" rel="noopener noreferrer"&gt;github.com/yuvrajangadsingh/brandmd&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/brandmd" rel="noopener noreferrer"&gt;npmjs.com/package/brandmd&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;v0.9 shipped today.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>css</category>
      <category>designsystems</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I ran my own tool against 100 sites. 1 in 5 returned the wrong primary font.</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Sat, 16 May 2026 19:10:55 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/i-ran-my-own-tool-against-100-sites-1-in-5-returned-the-wrong-primary-font-2eg9</link>
      <guid>https://dev.to/yuvrajangadsingh/i-ran-my-own-tool-against-100-sites-1-in-5-returned-the-wrong-primary-font-2eg9</guid>
      <description>&lt;p&gt;I built brandmd 6 months ago. It is an npm CLI that extracts any website's design system into a DESIGN.md file. The idea: drop the file in your project root, AI coding agents (Claude Code, Cursor, Gemini CLI, Stitch) read it, and they generate on-brand UI instead of generic shadcn-default.&lt;/p&gt;

&lt;p&gt;It was working. Stripe, GitHub, Vercel, Linear — primary fonts came back correct. Then a user DMd me last week.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"your tool says my primary font is Inter. it is actually Manrope. the brand is on the hero, not the paragraphs."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fair. So I ran brandmd against 100 popular design system sites to see how widespread the bug was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;9 of 45 successfully-extracted sites returned the wrong Primary font. 20%.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;brandmd said&lt;/th&gt;
&lt;th&gt;Reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;mantine.dev&lt;/td&gt;
&lt;td&gt;Menlo&lt;/td&gt;
&lt;td&gt;Outfit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;remix.run&lt;/td&gt;
&lt;td&gt;JetBrains Mono&lt;/td&gt;
&lt;td&gt;Inter Variable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;neon.tech&lt;/td&gt;
&lt;td&gt;GeistMono&lt;/td&gt;
&lt;td&gt;Inter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;valura.ai&lt;/td&gt;
&lt;td&gt;Inter&lt;/td&gt;
&lt;td&gt;Manrope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;railway.app&lt;/td&gt;
&lt;td&gt;Inter&lt;/td&gt;
&lt;td&gt;IBM Plex Serif (on the hero)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;resend.com&lt;/td&gt;
&lt;td&gt;inter&lt;/td&gt;
&lt;td&gt;aBCFavorit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;svelte.dev&lt;/td&gt;
&lt;td&gt;Georgia&lt;/td&gt;
&lt;td&gt;DM Serif Display&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;workday.design&lt;/td&gt;
&lt;td&gt;Times&lt;/td&gt;
&lt;td&gt;(no real brand font, fallback)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;htmx.org&lt;/td&gt;
&lt;td&gt;Times&lt;/td&gt;
&lt;td&gt;(intentionally minimal)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three different failure modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Code fonts winning.&lt;/strong&gt; mantine and remix had so much code on their homepage that JetBrains Mono / Menlo dominated the font frequency count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body text drowning the brand.&lt;/strong&gt; valura uses Inter on 737 elements (utility), Manrope on 28 (hero). Frequency rank picks Inter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback Times.&lt;/strong&gt; Sites with no custom font load Playwright's default fallback. Old logic surfaced Times as Primary.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Root cause
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// analyze.js, v0.7.2&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;primaryFont&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fontList&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was it. Primary was just the most-frequent font across all DOM elements. Body text always wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix (v0.8, just shipped)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// analyze.js, v0.8&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;primaryFont&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;pickNonExcluded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;displayFonts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;  &lt;span class="c1"&gt;// fontSize &amp;gt;= 40px (hero)&lt;/span&gt;
  &lt;span class="nf"&gt;pickNonExcluded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;headingFonts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;  &lt;span class="c1"&gt;// h1-h6&lt;/span&gt;
  &lt;span class="nf"&gt;pickNonExcluded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bodyFonts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;     &lt;span class="c1"&gt;// p, li, span, div&lt;/span&gt;
  &lt;span class="nx"&gt;fontList&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// excluded from Primary: mono fonts (Menlo, JetBrains Mono, GeistMono, etc.),&lt;/span&gt;
&lt;span class="c1"&gt;// default fallbacks (Times, Arial, Georgia), icon fonts (Material Icons,&lt;/span&gt;
&lt;span class="c1"&gt;// Font Awesome, Heroicons).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three principles:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Display first.&lt;/strong&gt; The hero font is the brand font. fontSize &amp;gt;= 40px is where the brand lives, not in h2/h3 subtitles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip mono.&lt;/strong&gt; Code fonts can win Secondary slot, never Primary. mantine.dev wants Outfit on top, JetBrains Mono in code blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skip fallbacks.&lt;/strong&gt; If the site has no real custom font, surfacing Times honestly is fine. But if there ARE real fonts available, use those.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Also shipped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quote-aware font-family parser&lt;/strong&gt; (handles &lt;code&gt;var(--font, 'Inter')&lt;/code&gt; and backslash escapes that v0.7.2 split incorrectly)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-role cascade&lt;/strong&gt; (heading falls back to display falls back to body)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Navigation timeout 30s → 45s&lt;/strong&gt; for slow SPAs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;7 of 9 wrong-Primary cases now report the correct brand font. The remaining 2 (railway.app, workday.design) genuinely have no distinct brand font on the hero, so the output is honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i brandmd
&lt;span class="c"&gt;# or run without install&lt;/span&gt;
npx brandmd https://your-site.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;If brandmd saved you time, &lt;a href="https://github.com/yuvrajangadsingh/brandmd" rel="noopener noreferrer"&gt;star the repo&lt;/a&gt; — it helps other devs find it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>tooling</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My numpy rewrite was 300x faster in isolation. End-to-end it was slower.</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Mon, 20 Apr 2026 10:07:39 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/my-numpy-rewrite-was-300x-faster-in-isolation-end-to-end-it-was-slower-3o2l</link>
      <guid>https://dev.to/yuvrajangadsingh/my-numpy-rewrite-was-300x-faster-in-isolation-end-to-end-it-was-slower-3o2l</guid>
      <description>&lt;p&gt;my CLI search took 4.3 seconds for 5000 files. i swapped the python cosine loop for a numpy matmul. it got slower.&lt;/p&gt;

&lt;p&gt;short-lived python CLIs punish optimizing the wrong layer.&lt;/p&gt;

&lt;p&gt;this is what happened shipping &lt;a href="https://github.com/yuvrajangadsingh/vemb" rel="noopener noreferrer"&gt;vemb&lt;/a&gt; 0.3.0: what i tried first that regressed performance, and what actually worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  the setup
&lt;/h2&gt;

&lt;p&gt;vemb is a CLI that wraps Gemini Embedding 2 for text, images, audio, video, and PDFs. &lt;code&gt;vemb search ./files "query"&lt;/code&gt; embeds every file, caches the vectors, and returns the top matches by cosine similarity.&lt;/p&gt;

&lt;p&gt;the cache in 0.2.0 was JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gemini-embedding-2-preview"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dim"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3072&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"file.png:size:mtime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.012&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;-0.034&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the search path looked like this:&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;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.vemb/cache.json&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&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;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;files&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;cache_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;values&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# python list of 3072 floats
&lt;/span&gt;    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;cosine_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_emb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&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;cosine_similarity&lt;/code&gt; was pure python. a for-loop over a list of 3072 floats. at 5000 files, that's about 15 million float multiplies and adds in python.&lt;/p&gt;

&lt;p&gt;the obvious optimization: replace the python loop with numpy.&lt;/p&gt;

&lt;h2&gt;
  
  
  the first rewrite
&lt;/h2&gt;

&lt;p&gt;i added a batched cosine that takes a query vector and a matrix of document vectors:&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;cosine_similarity_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;q_norm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;m_norms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m_norms&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;q_norm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;then i benchmarked it in isolation, numpy already imported:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;python loop&lt;/th&gt;
&lt;th&gt;numpy batch&lt;/th&gt;
&lt;th&gt;speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;19.55ms&lt;/td&gt;
&lt;td&gt;0.13ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;148x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;196ms&lt;/td&gt;
&lt;td&gt;0.70ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;278x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;td&gt;1975ms&lt;/td&gt;
&lt;td&gt;12.32ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;160x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fjbxvrxooqu7ck7kfc3zl.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.amazonaws.com%2Fuploads%2Farticles%2Fjbxvrxooqu7ck7kfc3zl.png" alt="isolated benchmark: numpy 148-300x faster" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;this was the win i expected. i pushed the branch, bumped the version, ran the test suite, and was about to tag v0.3.0.&lt;/p&gt;

&lt;p&gt;then i ran the end-to-end benchmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  the real test
&lt;/h2&gt;

&lt;p&gt;methodology: &lt;code&gt;time vemb search /tmp/test "query" --dim 3072&lt;/code&gt; in a fresh shell subprocess, not a python REPL. warm cache (all vectors already embedded), best of three runs on Apple Silicon.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;main (v0.2.0)&lt;/th&gt;
&lt;th&gt;numpy branch&lt;/th&gt;
&lt;th&gt;result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.32s&lt;/td&gt;
&lt;td&gt;1.59s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20% slower&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;2.34s&lt;/td&gt;
&lt;td&gt;2.27s&lt;/td&gt;
&lt;td&gt;tied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;4.31s&lt;/td&gt;
&lt;td&gt;6.96s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;60% slower&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fzy3c2h51sw9mrbx35mqx.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.amazonaws.com%2Fuploads%2Farticles%2Fzy3c2h51sw9mrbx35mqx.png" alt="real end-to-end: numpy regressed at every scale" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;numpy was winning in the synthetic test by 100-300x. end-to-end it lost at every real scale i cared about.&lt;/p&gt;

&lt;p&gt;and the loss got worse as N grew. at N=5000, user CPU on the numpy branch was 5.2 seconds vs 2.9 seconds on main. burning 80% more CPU to go slower.&lt;/p&gt;

&lt;h2&gt;
  
  
  where did the 4.3s go?
&lt;/h2&gt;

&lt;p&gt;rough breakdown of the main v0.2.0 command at N=5000, warm cache:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;json.loads&lt;/code&gt; of the 317 MB cache: &lt;strong&gt;~2.5s&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;gemini API round-trip for the query embedding: &lt;strong&gt;~0.6s&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;pure-python cosine loop over 5000 × 3072 floats: &lt;strong&gt;~1.0s&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;file scan, click boot, sort, print: &lt;strong&gt;~0.2s&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;the loop was ~25% of the command. the thing i was about to optimize was not the thing eating my wall time.&lt;/p&gt;

&lt;h2&gt;
  
  
  two hidden costs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;cost one: numpy import.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;python -c 'import numpy'&lt;/code&gt; on macOS takes ~180ms cold. that's a lot of native startup work before you touch the actual math.&lt;/p&gt;

&lt;p&gt;i added &lt;code&gt;import numpy as np&lt;/code&gt; at the top of &lt;code&gt;embed.py&lt;/code&gt;. &lt;code&gt;cli.py&lt;/code&gt; imports &lt;code&gt;embed.py&lt;/code&gt;. every invocation of &lt;code&gt;vemb&lt;/code&gt; — even &lt;code&gt;vemb --version&lt;/code&gt;, even &lt;code&gt;vemb text "hello"&lt;/code&gt; — now paid 180ms just to start.&lt;/p&gt;

&lt;p&gt;on &lt;code&gt;vemb --version&lt;/code&gt;, user CPU went from 0.77s on main to 2.42s on the branch. for a command that doesn't touch numpy at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;cost two: asarray conversion.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;the cache was JSON. loading it gave me a dict of Python lists. to use &lt;code&gt;m @ q&lt;/code&gt; numpy needed a contiguous float32 matrix.&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;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;at N=5000 D=3072 that's 15 million Python floats that numpy has to pull out of a list of lists and copy into a dense array. on my machine that single &lt;code&gt;asarray&lt;/code&gt; call took ~2 seconds.&lt;/p&gt;

&lt;p&gt;so to save ~1 second of python cosine, i was paying 180ms of numpy import plus 2 seconds of list-to-matrix conversion.&lt;/p&gt;

&lt;p&gt;the synthetic benchmark never measured any of this. it measured the kernel in isolation, after numpy was already loaded and the matrix was already built. that benchmark was lying to me.&lt;/p&gt;

&lt;h2&gt;
  
  
  the real fix
&lt;/h2&gt;

&lt;p&gt;the compute wasn't the bottleneck. the cache format was.&lt;/p&gt;

&lt;p&gt;parsing 317MB of JSON takes 2-3 seconds by itself. &lt;code&gt;json.loads&lt;/code&gt; is slow. it also produces python objects that numpy has to re-unpack into a contiguous array.&lt;/p&gt;

&lt;p&gt;so i replaced the cache with a binary numpy matrix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.vemb/
  vectors.npy      # float32 (N, D) matrix, pre-normalized
  manifest.json    # {key: row_index, ...} + metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the new search path:&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;keys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# np.load, milliseconds
&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asarray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_emb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;/=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;matrix&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;                          &lt;span class="c1"&gt;# single BLAS call
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;three things changed together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;vectors load from &lt;code&gt;.npy&lt;/code&gt; in tens of milliseconds instead of seconds. a &lt;code&gt;.npy&lt;/code&gt; file is a tiny header plus a raw memory dump of the array. &lt;code&gt;np.load&lt;/code&gt; can mmap it if you pass &lt;code&gt;mmap_mode='r'&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;they land as a contiguous float32 matrix already, so there's no &lt;code&gt;asarray&lt;/code&gt; conversion.&lt;/li&gt;
&lt;li&gt;they're pre-normalized on write, so cosine reduces to a plain dot product at query time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;i also moved &lt;code&gt;import numpy as np&lt;/code&gt; out of module scope and into the function that needs it. &lt;code&gt;vemb --version&lt;/code&gt; doesn't pay the import tax anymore. only &lt;code&gt;vemb search&lt;/code&gt; does.&lt;/p&gt;

&lt;p&gt;cosine stayed the same for this corpus. this was a storage and execution change, not a model change. top-k results matched the old code in my tests.&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.amazonaws.com%2Fuploads%2Farticles%2Fra7i6vr4fij7am5v454q.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.amazonaws.com%2Fuploads%2Farticles%2Fra7i6vr4fij7am5v454q.png" alt="architecture before and after, final result N=5000 2.6x faster 5x smaller" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  what shipped
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;N&lt;/th&gt;
&lt;th&gt;v0.2.0&lt;/th&gt;
&lt;th&gt;v0.3.0&lt;/th&gt;
&lt;th&gt;speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;1.62s&lt;/td&gt;
&lt;td&gt;1.48s&lt;/td&gt;
&lt;td&gt;tied&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;2.34s&lt;/td&gt;
&lt;td&gt;1.51s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.5x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;4.31s&lt;/td&gt;
&lt;td&gt;1.64s&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2.6x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;cache size at N=5000 3072-dim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;old: 317 MB &lt;code&gt;cache.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;new: 61 MB &lt;code&gt;vectors.npy&lt;/code&gt; + 229 KB &lt;code&gt;manifest.json&lt;/code&gt; (&lt;strong&gt;5x smaller&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;existing &lt;code&gt;.vemb/cache.json&lt;/code&gt; caches auto-migrate to the binary format on first load. no user action.&lt;/p&gt;

&lt;h2&gt;
  
  
  the takeaway
&lt;/h2&gt;

&lt;p&gt;for short-lived python CLIs, the bottleneck is often IO, deserialization, or imports. hot-loop speedups can miss the real cost. the synthetic benchmark said the loop was slow. the real benchmark said the loop ran for 1 second inside a 4-second command.&lt;/p&gt;

&lt;p&gt;if you only measure the kernel, you optimize the kernel. if you measure end-to-end in a fresh subprocess, you find the real cost.&lt;/p&gt;

&lt;p&gt;the methodology that exposed this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same input corpus&lt;/li&gt;
&lt;li&gt;cold process, not a REPL&lt;/li&gt;
&lt;li&gt;real disk cache&lt;/li&gt;
&lt;li&gt;best of three runs&lt;/li&gt;
&lt;li&gt;measure wall time AND user CPU (they tell different stories)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;the numpy branch is still on GitHub. i never merged it. the fix was in a layer i wasn't even looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  the principle
&lt;/h2&gt;

&lt;p&gt;measure the whole CLI in a fresh process before touching the hot loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; vemb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;repo: &lt;a href="https://github.com/yuvrajangadsingh/vemb" rel="noopener noreferrer"&gt;github.com/yuvrajangadsingh/vemb&lt;/a&gt;&lt;/p&gt;

</description>
      <category>embeddings</category>
      <category>python</category>
      <category>performance</category>
      <category>cli</category>
    </item>
    <item>
      <title>I Scanned 31 AI-Built Repos. Each Tool Leaves Behind a Different Mess.</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Wed, 08 Apr 2026 08:26:21 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/i-scanned-31-ai-built-repos-each-tool-leaves-behind-a-different-mess-4k3</link>
      <guid>https://dev.to/yuvrajangadsingh/i-scanned-31-ai-built-repos-each-tool-leaves-behind-a-different-mess-4k3</guid>
      <description>&lt;p&gt;46% of every issue I found was the same thing: deep nesting. AI models keep stuffing logic into the same function instead of breaking it apart. That pattern showed up in every tool I tested.&lt;/p&gt;

&lt;p&gt;I scanned 31 public JS/TS repos with &lt;a href="https://github.com/yuvrajangadsingh/vibecheck" rel="noopener noreferrer"&gt;vibecheck&lt;/a&gt;, a linter with 34 rules for AI-specific code smells. 10 repos from Cursor, 11 from Lovable, 10 from Bolt.new. Only public repos with real application code, no scaffolds or starters.&lt;/p&gt;

&lt;p&gt;This is not a scientific benchmark. But it's a real sample of real repos people shipped. I manually reviewed every error-level finding and noted which patterns were real issues vs template boilerplate. The &lt;a href="https://github.com/yuvrajangadsingh/vibecheck/tree/main/article" rel="noopener noreferrer"&gt;full repo list and raw scan data&lt;/a&gt; are public if you want to verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Repos&lt;/th&gt;
&lt;th&gt;Issues&lt;/th&gt;
&lt;th&gt;Files&lt;/th&gt;
&lt;th&gt;Issues/file&lt;/th&gt;
&lt;th&gt;Errors&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;9,534&lt;/td&gt;
&lt;td&gt;1,499&lt;/td&gt;
&lt;td&gt;6.36&lt;/td&gt;
&lt;td&gt;77&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lovable&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;4,832&lt;/td&gt;
&lt;td&gt;1,886&lt;/td&gt;
&lt;td&gt;2.56&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bolt&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;329&lt;/td&gt;
&lt;td&gt;212&lt;/td&gt;
&lt;td&gt;1.55&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;14,695 issues across 3,597 files. Cursor had the most issues by far, but also the biggest repos. Bolt looked cleanest by volume but had the smallest codebases.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke most often
&lt;/h2&gt;

&lt;p&gt;Three patterns dominated everything else:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deep nesting&lt;/strong&gt;: ~46% of all issues. The most AI-shaped pattern in the dataset. Models keep appending branches because it keeps context intact. Humans usually extract functions earlier.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Console.log pollution&lt;/strong&gt;: hundreds of hits across every tool. No AI coding tool cleans up debug logging.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;God functions&lt;/strong&gt;: the largest was 3,579 lines in a Cursor repo (easy-kanban's AppContent). Lovable's biggest was 1,810 lines. Bolt's was 820 lines.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's what nested AI code looks like in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newChunkCount&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&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;currentProgress&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;99.9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[PackageQueue] Progress 100%, no new chunks...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emptyRetryCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[PackageQueue] No new chunks. Retry: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emptyRetryCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;emptyRetryCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;maxEmptyRetries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[PackageQueue] PKG installation failed&lt;/span&gt;&lt;span class="dl"&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;Nested control flow plus debug logging left in production. This showed up everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Each tool had a distinct pattern
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; had the highest issue density (6.36/file) and 77 error-level findings. All SQL injection hits came from Cursor repos. It also had the most &lt;code&gt;as any&lt;/code&gt; usage (490 in one repo alone). My read: Cursor users were building bigger, more ambitious apps, and the mess scaled with them. This is inference, not proof. Bigger projects have more surface area.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lovable&lt;/strong&gt; had innerHTML or dangerouslySetInnerHTML in 100% of repos (11 out of 11). That's a confirmed pattern at scale. It also produced the only &lt;code&gt;eval&lt;/code&gt;-class calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;code&lt;/span&gt; &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;highlightedCode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;consoleProxyScript&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some of these are template-level patterns (syntax highlighting, chart CSS) that aren't exploitable in isolation. But they're the kind of code that drifts toward real XSS if someone later feeds user input into the same path. Worth reviewing, not worth panicking about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bolt&lt;/strong&gt; had the lowest issues per file (1.55) but was the only tool that shipped a hardcoded database credential and an error info leak in the same repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;5.75.154.79&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jk5h...redacted...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres&lt;/span&gt;&lt;span class="dl"&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bolt looked cleaner by volume but not necessarily safer. Small repo size helped its totals. Batch 2 had zero security findings though, so the batch 1 hardcoded key might be an outlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually mattered
&lt;/h2&gt;

&lt;p&gt;The tools did not fail in the same way. But the overlap mattered more than the differences.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;None of them naturally refactor.&lt;/li&gt;
&lt;li&gt;None of them clean up logs.&lt;/li&gt;
&lt;li&gt;None of them ask "should this really be one 3,500-line function?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scope and review mattered more than tool choice. Small apps were cleaner. Lovable had the single cleanest repo in the entire set (humanise-ai, 0.48 issues/file). Cursor had the dirtiest (easy-kanban, 14.04 issues/file). One of the cleanest Bolt repos was actually a hybrid built with Bolt + Cursor + Cline.&lt;/p&gt;

&lt;p&gt;The bad outcome was never "AI touched the repo." The bad outcome was "AI wrote it, nobody looked at it after."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;vibecheck flagged patterns, not confirmed vulnerabilities.&lt;/strong&gt; Some of these findings are real bugs. Some are smells that might never cause a problem. A linter doesn't know intent. It flags what it sees. I doubled the sample from 15 to 31 repos, and the same patterns held.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you're shipping AI-generated code, run a check before you push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @yuvrajangadsingh/vibecheck &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;34 rules. JS/TS and Python. Also runs as a &lt;a href="https://github.com/marketplace/actions/vibecheck-ai-slop" rel="noopener noreferrer"&gt;GitHub Action&lt;/a&gt;, &lt;a href="https://marketplace.visualstudio.com/items?itemName=yuvrajangadsingh.vibecheck-linter" rel="noopener noreferrer"&gt;VS Code extension&lt;/a&gt;, and &lt;a href="https://github.com/yuvrajangadsingh/vibecheck" rel="noopener noreferrer"&gt;MCP server&lt;/a&gt; for AI coding agents.&lt;/p&gt;

</description>
      <category>devtools</category>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>I Scanned a 1K-Star Cursor Project. AI Code Doesn't Look Like AI Code Anymore.</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Sun, 29 Mar 2026 14:06:35 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/i-scanned-a-1k-star-cursor-project-ai-code-doesnt-look-like-ai-code-anymore-1lck</link>
      <guid>https://dev.to/yuvrajangadsingh/i-scanned-a-1k-star-cursor-project-ai-code-doesnt-look-like-ai-code-anymore-1lck</guid>
      <description>&lt;p&gt;There's a common belief that AI-generated code is easy to spot. Obvious comments, step-by-step numbered instructions, hedging language like "might need to adjust this later."&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/yuvrajangadsingh/vibecheck" rel="noopener noreferrer"&gt;vibecheck&lt;/a&gt;, a static analysis tool that detects these patterns. I ran it against &lt;a href="https://github.com/ryokun6/ryos" rel="noopener noreferrer"&gt;ryOS&lt;/a&gt;, a 1,100-star web-based macOS clone built entirely with Cursor by Ryo Lu (Head of Design at Cursor). If any project would have AI fingerprints, this one would.&lt;/p&gt;

&lt;p&gt;The results surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero comment-level AI tells
&lt;/h2&gt;

&lt;p&gt;None. No "// Initialize the state variable" above a useState. No "// Step 1: Fetch the data." No narrator comments, no hedging, no placeholder stubs. The code reads clean line by line.&lt;/p&gt;

&lt;p&gt;AI-generated code has evolved past the obvious tells. The models learned to stop over-explaining. If you're still looking for bad comments as your AI detector, you're looking at last year's problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smell moved to architecture
&lt;/h2&gt;

&lt;p&gt;vibecheck found &lt;strong&gt;4,523 issues&lt;/strong&gt; across 378 files. Here's where the signal actually is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;God functions.&lt;/strong&gt; &lt;code&gt;MacDock&lt;/code&gt; is a single 2,003-line React component. &lt;code&gt;useIpodLogic&lt;/code&gt; is an 1,891-line hook. &lt;code&gt;useKaraokeLogic&lt;/code&gt; is 1,798 lines. A human writing incrementally would extract sub-hooks, split components, refactor. AI keeps stuffing logic into the same function because it doesn't have the "this is getting too big" instinct.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deep nesting.&lt;/strong&gt; 13 levels deep in &lt;code&gt;ChatMessages.tsx&lt;/code&gt;. Callback hell meets JSX spaghetti. When you ask AI to "add a conditional render for loading states" three times in a row, each one nests inside the previous one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Swallowed errors.&lt;/strong&gt; 35 empty catch blocks, many in sequence. &lt;code&gt;infiniteMacHandler.ts&lt;/code&gt; has 10+ empty catches in a row (lines 790, 797, 804, 827...). This happens when AI wraps every async call in try/catch but has no error handling strategy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Console.log pollution.&lt;/strong&gt; 671 console.log statements in production code. AI adds them for debugging and never removes them because no one asks it to clean up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error info leaks.&lt;/strong&gt; 11 API endpoints that send &lt;code&gt;error.message&lt;/code&gt; directly in HTTP responses. Internal details (stack traces, DB errors) exposed to clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Code review catches line-level problems. A reviewer reads 50 lines of a function and it looks fine. Clean variable names, proper TypeScript, no weird patterns.&lt;/p&gt;

&lt;p&gt;But zoom out and the function is 2,000 lines long. The reviewer never sees the full picture because the diff only shows the 30 lines that changed. The AI-generated architecture accumulates invisibly.&lt;/p&gt;

&lt;p&gt;This is the new AI code smell: &lt;strong&gt;code that passes review but fails at scale.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What to look for
&lt;/h2&gt;

&lt;p&gt;If you're reviewing AI-assisted code, stop looking for bad comments and start looking for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Function length.&lt;/strong&gt; Anything over 200 lines is suspicious. Over 500 is almost certainly AI-accumulated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nesting depth.&lt;/strong&gt; 5+ levels means the function is doing too many things.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Empty catch blocks.&lt;/strong&gt; AI loves try/catch. It hates error handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Console pollution.&lt;/strong&gt; Count the console.logs. AI never cleans up after itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeated patterns.&lt;/strong&gt; 10 empty catches in a row? That's a loop, not a developer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @yuvrajangadsingh/vibecheck ./your-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;vibecheck catches 32 patterns across JS/TS and Python. Works as a CLI, GitHub Action, VS Code extension, and pre-commit hook. All offline, no API calls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/yuvrajangadsingh/vibecheck" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://www.npmjs.com/package/@yuvrajangadsingh/vibecheck" rel="noopener noreferrer"&gt;npm&lt;/a&gt; | &lt;a href="https://marketplace.visualstudio.com/items?itemName=yuvrajangadsingh.vibecheck-linter" rel="noopener noreferrer"&gt;VS Code&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Your AI tools don't know what your brand looks like</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Thu, 26 Mar 2026 07:49:42 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/your-ai-tools-dont-know-what-your-brand-looks-like-3ajo</link>
      <guid>https://dev.to/yuvrajangadsingh/your-ai-tools-dont-know-what-your-brand-looks-like-3ajo</guid>
      <description>&lt;p&gt;Every AI coding agent generates the same UI. Gray backgrounds, blue buttons, Inter font, 8px border radius. It doesn't matter if you're building for a fintech startup or a surf shop. The output looks identical because the agent has zero context about your brand.&lt;/p&gt;

&lt;p&gt;Google noticed this too. When they redesigned Stitch in March 2026, they introduced a file format called DESIGN.md. It's a markdown file that encodes your design system (colors, typography, spacing, component styles) in a format that LLMs can read. Drop it in your project root and tools like Claude Code, Cursor, Gemini CLI, and Stitch itself will use it to generate UI that actually matches your brand.&lt;/p&gt;

&lt;p&gt;The problem is, nobody wants to write one from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a DESIGN.md looks like
&lt;/h2&gt;

&lt;p&gt;It has 5 sections:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Visual Theme &amp;amp; Atmosphere&lt;/strong&gt; - mood, shape language, depth&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Color Palette &amp;amp; Roles&lt;/strong&gt; - every color with a semantic role&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typography Rules&lt;/strong&gt; - font families, size scale, weights&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Component Stylings&lt;/strong&gt; - buttons, cards, inputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Layout Principles&lt;/strong&gt; - spacing scale, base grid unit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's a snippet from Stripe's:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;## 2. Color Palette &amp;amp; Roles
- **White** (\`#FFFFFF\`) — Page background
- **Dark Blue** (\`#533AFD\`) — Accent background
- **Cyan** (\`#00D66F\`) — Accent background
- **Dark Muted Blue** (\`#64748D\`) — Secondary text

## 3. Typography Rules
**Primary font:** sohne-var
- Headings: 26px, 32px, 48px, 56px
- Body / UI: 14px, 16px, 18px, 22px
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Writing this by hand means opening DevTools, inspecting every element, noting down colors and fonts, figuring out the spacing scale. For a complex site that's hours of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extracting it automatically
&lt;/h2&gt;

&lt;p&gt;I built a CLI called brandmd that does this in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx brandmd https://stripe.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It launches a headless browser, renders the page, scrolls through it to trigger lazy-loaded content, dismisses cookie banners, then extracts computed styles from every visible element. Colors get clustered (so you don't end up with 50 shades of the same gray), fonts and spacing values get grouped into scales, and everything gets templated into the DESIGN.md format.&lt;/p&gt;

&lt;p&gt;No LLM calls, no API keys. Runs entirely on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it catches
&lt;/h2&gt;

&lt;p&gt;I ran it on a few sites to test:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linear&lt;/strong&gt; - picked up Inter Variable as the primary font, Berkeley Mono as secondary, the indigo accent (#5E6AD2), and identified a 4px base grid from the spacing values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stripe&lt;/strong&gt; - found sohne-var (their custom font), the purple (#533AFD) and green (#00D66F) accent colors, and 4 distinct shadow styles for depth.&lt;/p&gt;

&lt;p&gt;The output isn't perfect. It can't read your Figma tokens or understand why you chose a specific color for error states. But it gives you a solid starting point that's 90% there, and you can tweak the remaining 10% in a few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Extract from your current site&lt;/span&gt;
npx brandmd https://yoursite.com &lt;span class="nt"&gt;-o&lt;/span&gt; DESIGN.md

&lt;span class="c"&gt;# Drop it in your project root&lt;/span&gt;
&lt;span class="nb"&gt;mv &lt;/span&gt;DESIGN.md ./

&lt;span class="c"&gt;# Now your AI tools generate on-brand UI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Code, Cursor, and Gemini CLI all read markdown files from your project root automatically. No configuration needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx brandmd https://linear.app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;a href="https://github.com/yuvrajangadsingh/brandmd" rel="noopener noreferrer"&gt;github.com/yuvrajangadsingh/brandmd&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Works on any public URL. If you have a site and you use AI coding tools, try it on your own URL. You'll probably be surprised at how much design information is hiding in your computed styles.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>design</category>
      <category>cli</category>
    </item>
    <item>
      <title>Embeddings shouldn't need a notebook</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Wed, 25 Mar 2026 14:14:09 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/embeddings-shouldnt-need-a-notebook-4hjb</link>
      <guid>https://dev.to/yuvrajangadsingh/embeddings-shouldnt-need-a-notebook-4hjb</guid>
      <description>&lt;p&gt;I kept running into the same annoyance whenever I needed embeddings. The retrieval part of a RAG pipeline was hard enough already. Generating vectors should've been the easy part.&lt;/p&gt;

&lt;p&gt;But every time I needed to embed something, the workflow looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open a notebook or write a throwaway script&lt;/li&gt;
&lt;li&gt;Import the SDK, set up the client&lt;/li&gt;
&lt;li&gt;Figure out the right model name (was it &lt;code&gt;text-embedding-004&lt;/code&gt; or &lt;code&gt;text-embedding-3-small&lt;/code&gt;?)&lt;/li&gt;
&lt;li&gt;Write the call, handle the response format&lt;/li&gt;
&lt;li&gt;Copy the vector out of the output&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For text that's annoying. For images or audio it's worse. Different SDKs, different input formats, different response shapes.&lt;/p&gt;

&lt;p&gt;I kept thinking: I can &lt;code&gt;curl&lt;/code&gt; an API in seconds. I can &lt;code&gt;jq&lt;/code&gt; a JSON response without writing a script. Why can't I just embed something from the terminal?&lt;/p&gt;

&lt;h2&gt;
  
  
  The tool I wanted
&lt;/h2&gt;

&lt;p&gt;Something like httpie but for embeddings. Type a command, get a vector back.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vemb text &lt;span class="s2"&gt;"hello world"&lt;/span&gt;
&lt;span class="c"&gt;# {"model": "gemini-embedding-2-preview", "dimensions": 3072, "values": [0.0123, -0.0456, ...]}&lt;/span&gt;

vemb text &lt;span class="s2"&gt;"hello world"&lt;/span&gt; &lt;span class="nt"&gt;--compact&lt;/span&gt;
&lt;span class="c"&gt;# [0.0123, -0.0456, 0.0789, ...]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embed an image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vemb image photo.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embed a PDF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vemb pdf report.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare two files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vemb similar photo1.jpg photo2.jpg
&lt;span class="c"&gt;# 0.8734&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No notebooks, no scripts, no boilerplate. Just the vector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Gemini Embedding 2
&lt;/h2&gt;

&lt;p&gt;I looked at OpenAI's embedding models first. Their embeddings endpoint is text-only. If you want to embed images, you're stitching together separate models and separate vector spaces. No clean way to compare text against images with a single embedding call.&lt;/p&gt;

&lt;p&gt;Google released &lt;a href="https://blog.google/technology/google-deepmind/gemini-embedding-model/" rel="noopener noreferrer"&gt;Gemini Embedding 2&lt;/a&gt; (public preview, March 2026). One model that handles text, images, audio, video, and PDFs natively. Same vector space for everything. You can embed a photo and a text description and compare them directly with cosine similarity.&lt;/p&gt;

&lt;p&gt;That's what made the CLI possible. One model, one API, all input types.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building it
&lt;/h2&gt;

&lt;p&gt;The whole thing is ~400 lines of Python. Two files: &lt;code&gt;embed.py&lt;/code&gt; (core logic) and &lt;code&gt;cli.py&lt;/code&gt; (Click commands).&lt;/p&gt;

&lt;p&gt;The interesting parts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-detection&lt;/strong&gt;: &lt;code&gt;vemb embed&lt;/code&gt; guesses the file type from the extension. JPEGs, PNGs, MP3s, WAVs, MP4s, PDFs all work with the same command.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Batch mode&lt;/strong&gt;: &lt;code&gt;vemb embed *.jpg --jsonl&lt;/code&gt; embeds every file and outputs one JSON object per line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Directory search&lt;/strong&gt;: &lt;code&gt;vemb search ./photos/ "dark moody sunset"&lt;/code&gt; embeds the query, embeds every file in the directory (with caching), and ranks by cosine similarity.&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="c"&gt;# search a folder of images by text description&lt;/span&gt;
vemb search ./photos/ &lt;span class="s2"&gt;"dark moody sunset"&lt;/span&gt; &lt;span class="nt"&gt;--top&lt;/span&gt; 5

0.8234    ./photos/sunset-beach.png
0.7891    ./photos/evening-skyline.png
0.7654    ./photos/golden-hour.png
0.6123    ./photos/cloudy-morning.png
0.5987    ./photos/overcast-street.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example use cases
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Retrieval experiments&lt;/strong&gt;: embed a few chunks, check similarity scores, tune the chunking. No notebook needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Image search&lt;/strong&gt;: I keep a folder of reference mockups. &lt;code&gt;vemb search ./mockups/ "login screen"&lt;/code&gt; finds the right ones instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Checking if two files are semantically close&lt;/strong&gt;: &lt;code&gt;vemb similar draft-v1.pdf draft-v2.pdf&lt;/code&gt; tells me how much the content actually changed between versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-modal search&lt;/strong&gt;: embed a text query against a folder of images and get ranked results. One model, one vector space means text and images are directly comparable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;vemb
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_key   &lt;span class="c"&gt;# free at aistudio.google.com/apikey&lt;/span&gt;
vemb text &lt;span class="s2"&gt;"hello world"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source and docs: &lt;a href="https://github.com/yuvrajangadsingh/vemb" rel="noopener noreferrer"&gt;github.com/yuvrajangadsingh/vemb&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The API key is free, the model is free tier. If you're building anything with embeddings and you're tired of opening notebooks for a one-line operation, try it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>cli</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Your AI Wrote the Code. Who's Checking It?</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Sat, 21 Mar 2026 11:01:49 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/your-ai-wrote-the-code-whos-checking-it-2d2e</link>
      <guid>https://dev.to/yuvrajangadsingh/your-ai-wrote-the-code-whos-checking-it-2d2e</guid>
      <description>&lt;p&gt;I review a lot of PRs at work. Over the last year, I started noticing patterns in AI-generated code that kept showing up. Same five or six things, every time.&lt;/p&gt;

&lt;p&gt;Empty catch blocks. &lt;code&gt;as any&lt;/code&gt; sprinkled everywhere. Comments that just restate what the code does. Hardcoded API keys. &lt;code&gt;except: pass&lt;/code&gt; in Python. The code works, it passes tests, but it's the kind of stuff you'd flag in a review and ask someone to fix.&lt;/p&gt;

&lt;p&gt;The numbers back this up. CodeRabbit found AI-generated PRs have &lt;a href="https://www.coderabbit.ai/blog/state-of-ai-vs-human-code-generation-report" rel="noopener noreferrer"&gt;1.7x more issues&lt;/a&gt; than human PRs. Veracode says &lt;a href="https://www.helpnetsecurity.com/2025/08/07/create-ai-code-security-risks/" rel="noopener noreferrer"&gt;45% of AI code samples&lt;/a&gt; contain security vulnerabilities.&lt;/p&gt;

&lt;p&gt;ESLint catches syntax issues. But nobody's catching the behavioral patterns that AI tools leave behind. So I built one.&lt;/p&gt;

&lt;h2&gt;
  
  
  vibecheck
&lt;/h2&gt;

&lt;p&gt;24 rules across JS/TS and Python. Zero config. Runs offline. Regex-based, so it's fast.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @yuvrajangadsingh/vibecheck &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  src/api/routes.ts
    12:5    error  no-hardcoded-secrets   Hardcoded secret detected
    45:3    error  no-empty-catch         Empty catch block swallows errors
    89:1    warn   no-console-pollution   console.log left in production code

  src/utils/db.ts
    34:5    error  no-sql-concat          SQL query built with string concatenation

  4 problems (3 errors, 1 warning)
  2 files with issues out of 47 scanned (0.8s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API keys, no cloud, no LLM calls. It's regex rules that match the patterns AI tools tend to produce.&lt;/p&gt;

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

&lt;p&gt;Here's the stuff I kept flagging in code reviews:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The silent failure:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&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_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your API call fails and nobody ever knows. vibecheck flags &lt;code&gt;no-bare-except&lt;/code&gt; and &lt;code&gt;no-pass-except&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "I'll type it later":&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI tools love &lt;code&gt;as any&lt;/code&gt;. It shuts up the type checker but defeats the entire point of TypeScript. vibecheck flags &lt;code&gt;no-ts-any&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The useless comment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// initialize the counter&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your linter doesn't care about this. vibecheck does. &lt;code&gt;no-obvious-comments&lt;/code&gt; catches comments that just repeat what the code already says.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hardcoded key:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sk-proj-abc123def456&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one's obvious but it keeps happening. &lt;code&gt;no-hardcoded-secrets&lt;/code&gt; matches common API key patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diff mode
&lt;/h2&gt;

&lt;p&gt;This is the part I use most. Instead of scanning your entire codebase, scan only the lines you just changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vibecheck &lt;span class="nt"&gt;--staged&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop it in a pre-commit hook and it only checks what you're about to commit:&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="c"&gt;# .git/hooks/pre-commit&lt;/span&gt;
npx @yuvrajangadsingh/vibecheck &lt;span class="nt"&gt;--staged&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CI, it runs on PR diffs so you're not drowning in warnings from old code.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Isn't this just ESLint?"
&lt;/h2&gt;

&lt;p&gt;No. ESLint catches syntax and style. vibecheck catches the patterns that come from how AI tools generate code. Your linter won't flag a catch block that only does &lt;code&gt;console.error(err)&lt;/code&gt; without rethrowing. It won't flag &lt;code&gt;# type: ignore&lt;/code&gt; without a specific error code. It won't flag a function that's 120 lines long because the AI didn't know when to stop.&lt;/p&gt;

&lt;p&gt;They're complementary. Run both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# no install needed&lt;/span&gt;
npx @yuvrajangadsingh/vibecheck &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# or install globally&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @yuvrajangadsingh/vibecheck

&lt;span class="c"&gt;# standalone binary (no Node required)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://github.com/yuvrajangadsingh/vibecheck/releases/latest/download/vibecheck-darwin-arm64 &lt;span class="nt"&gt;-o&lt;/span&gt; vibecheck
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x vibecheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/yuvrajangadsingh/vibecheck" rel="noopener noreferrer"&gt;github.com/yuvrajangadsingh/vibecheck&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open to feedback on what rules to add next. If you keep flagging the same thing in AI-generated PRs, I probably want to hear about it.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your GitHub Profile Is Lying About You</title>
      <dc:creator>Yuvraj Angad Singh</dc:creator>
      <pubDate>Tue, 10 Mar 2026 12:30:00 +0000</pubDate>
      <link>https://dev.to/yuvrajangadsingh/your-github-profile-is-lying-about-you-34a9</link>
      <guid>https://dev.to/yuvrajangadsingh/your-github-profile-is-lying-about-you-34a9</guid>
      <description>&lt;p&gt;I shipped 888 commits last year. My GitHub profile showed 0.&lt;/p&gt;

&lt;p&gt;Not because I wasn't coding. I was writing code every single day, reviewing PRs, closing issues, shipping features. But all of it went to private repos. And GitHub doesn't count private repo activity on your public profile.&lt;/p&gt;

&lt;p&gt;For almost 4 years, my contribution graph was a graveyard of gray squares.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;Recruiters spend about 6 seconds on a GitHub profile. Gray squares = "doesn't code." Green squares = "active developer." It's shallow, but it's real. I've had recruiters literally ask me why my profile looked inactive.&lt;/p&gt;

&lt;p&gt;GitHub does have a "show private contributions" toggle in settings. But all it does is show anonymous green squares. No repo names, no PRs, no context. And if your company uses a separate org account (like mine does), those contributions don't show up on your personal profile at all.&lt;/p&gt;

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

&lt;p&gt;I wrote a bash CLI called &lt;a href="https://github.com/yuvrajangadsingh/greens" rel="noopener noreferrer"&gt;greens&lt;/a&gt; that mirrors your private work activity to a public repo without exposing any code.&lt;/p&gt;

&lt;p&gt;Here's what it does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scans your work repos locally (never modifies them)&lt;/li&gt;
&lt;li&gt;Extracts commit timestamps for your email&lt;/li&gt;
&lt;li&gt;Creates empty commits with matching timestamps in a public mirror repo&lt;/li&gt;
&lt;li&gt;Pushes to GitHub&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. No code leaves your machine. The mirror repo contains empty commits with only timestamps.&lt;/p&gt;

&lt;p&gt;If you have &lt;code&gt;gh&lt;/code&gt; CLI set up, it also picks up PRs, reviews, and issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;yuvrajangadsingh/greens/greens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;greens&lt;/code&gt; and the setup wizard walks you through configuration. Takes about 2 minutes.&lt;/p&gt;

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

&lt;p&gt;The key insight is that GitHub's contribution graph only cares about commit timestamps, not content. So greens creates a bare cache of each repo (no working tree, no blobs), extracts dates where your email authored a commit, and creates empty commits with those exact timestamps in the mirror.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Work Repos → Bare Cache → Public Mirror
  (untouched)     (no blobs)    (empty commits with your dates)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your source repos can be on GitHub, GitLab, Bitbucket, or self-hosted. greens scans the local clone, not the remote.&lt;/p&gt;

&lt;h2&gt;
  
  
  My results
&lt;/h2&gt;

&lt;p&gt;After setting it up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;11 repos tracked&lt;/li&gt;
&lt;li&gt;888 commits mirrored&lt;/li&gt;
&lt;li&gt;158 active days visible on my graph&lt;/li&gt;
&lt;li&gt;Auto-syncs daily via launchd&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before: dead profile that made me look like I stopped coding in 2022.&lt;br&gt;
After: an accurate picture of my actual work.&lt;/p&gt;
&lt;h2&gt;
  
  
  "Isn't this gaming the system?"
&lt;/h2&gt;

&lt;p&gt;Fair question. I'm not faking open source contributions. The mirror repo clearly says what it is. I'm just making private work volume visible on a platform that ignores it by default.&lt;/p&gt;

&lt;p&gt;If your company has a policy against this, check before using it. But most devs I've talked to have the same frustration: their profile doesn't reflect their actual output.&lt;/p&gt;
&lt;h2&gt;
  
  
  Set up automation
&lt;/h2&gt;

&lt;p&gt;Once greens is working, automate it. On macOS, a launchd plist that runs &lt;code&gt;greens&lt;/code&gt; daily at midnight keeps everything in sync without thinking about it. On Linux, a cron job does the same thing.&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="c"&gt;# cron example&lt;/span&gt;
0 0 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /usr/local/bin/greens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;yuvrajangadsingh/greens/greens
greens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or clone manually:&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/yuvrajangadsingh/greens.git
&lt;span class="nb"&gt;cd &lt;/span&gt;greens &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; bash setup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub repo: &lt;a href="https://github.com/yuvrajangadsingh/greens" rel="noopener noreferrer"&gt;github.com/yuvrajangadsingh/greens&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Star it if you find it useful. Issues and PRs welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Yuvraj, building AI systems at &lt;a href="https://www.meetaugust.ai" rel="noopener noreferrer"&gt;August&lt;/a&gt;. I write about dev tools and open source when I'm not debugging LLM pipelines at 2 AM.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>opensource</category>
      <category>cli</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
