<?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: OptiRefine</title>
    <description>The latest articles on DEV Community by OptiRefine (@optirefine).</description>
    <link>https://dev.to/optirefine</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%2F3901349%2F02194282-90b0-47b3-b3fc-0abd06e1b2b3.jpeg</url>
      <title>DEV Community: OptiRefine</title>
      <link>https://dev.to/optirefine</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/optirefine"/>
    <language>en</language>
    <item>
      <title>Your RAG pipeline is bad at docs because your chunker splits code fences</title>
      <dc:creator>OptiRefine</dc:creator>
      <pubDate>Fri, 28 Aug 2026 02:36:09 +0000</pubDate>
      <link>https://dev.to/optirefine/your-rag-pipeline-is-bad-at-docs-because-your-chunker-splits-code-fences-24jk</link>
      <guid>https://dev.to/optirefine/your-rag-pipeline-is-bad-at-docs-because-your-chunker-splits-code-fences-24jk</guid>
      <description>&lt;p&gt;Here is the chunking code from more or less every RAG tutorial published in the last two years:&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;splitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RecursiveCharacterTextSplitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;chunk_overlap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;splitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works fine on prose. Run it over a documentation site and it quietly does four destructive&lt;br&gt;
things, none of which throw an error, and all of which show up later as "the assistant gives&lt;br&gt;
vague answers about our API."&lt;/p&gt;

&lt;p&gt;I spent a while building a docs-specific ingestion pipeline and most of the work turned out to be&lt;br&gt;
in these four places. None of it is glamorous. All of it moved retrieval quality more than&lt;br&gt;
swapping embedding models did.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. It cuts code fences in half
&lt;/h2&gt;

&lt;p&gt;A 1,000-character window lands mid-code-block constantly. You get one chunk ending in:&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="sb"&gt;``&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_secret&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TOKEN_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&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 the next chunk starting with the rest of the dict and a closing fence that now has no opener.&lt;/p&gt;

&lt;p&gt;Two things go wrong. The obvious one is that neither chunk contains a runnable example, so the&lt;br&gt;
model retrieves half a function and confabulates the other half. The subtler one is that a&lt;br&gt;
&lt;strong&gt;half-open fence poisons everything downstream&lt;/strong&gt; — every markdown renderer, and most models,&lt;br&gt;
treat the remainder of that chunk as code. Your carefully written prose about token expiry is now&lt;br&gt;
inside a Python block as far as the model is concerned.&lt;/p&gt;

&lt;p&gt;The fix is that code blocks and tables can only split at &lt;strong&gt;line and row boundaries&lt;/strong&gt;, and if a&lt;br&gt;
split does happen the fence has to be reopened with its language tag and the table header has to&lt;br&gt;
be repeated. And overlap must never bisect a fence — the overlap window is where this bug hides,&lt;br&gt;
because the chunk itself looks fine and only the neighbour is broken.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. It throws away where the chunk came from
&lt;/h2&gt;

&lt;p&gt;This is the one that costs the most retrieval quality for the least effort to fix.&lt;/p&gt;

&lt;p&gt;A sliding window gives you a chunk that reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You must include the &lt;code&gt;state&lt;/code&gt; parameter and verify it on return. Tokens expire after 3600&lt;br&gt;
seconds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Embed that and ask "how long do OAuth tokens last?" and it may or may not come back, because&lt;br&gt;
nothing in the text says OAuth, or authentication, or which product this is. The words that would&lt;br&gt;
have matched are in an &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt; four hundred characters up the page.&lt;/p&gt;

&lt;p&gt;Documentation is a tree and chunks should inherit their path. Every chunk in a docs pipeline&lt;br&gt;
should open with its heading path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Authentication &amp;gt; Authorization flows &amp;gt; OAuth 2.0

&lt;span class="gu"&gt;### OAuth 2.0&lt;/span&gt;

You must include the &lt;span class="sb"&gt;`state`&lt;/span&gt; parameter and verify it on return...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the chunk is &lt;strong&gt;independently answerable&lt;/strong&gt;. It carries its own context, it embeds near the&lt;br&gt;
question people actually ask, and when you show sources in the UI you have a breadcrumb to&lt;br&gt;
display instead of a bare URL.&lt;/p&gt;

&lt;p&gt;Keep the un-prefixed version in a separate field too (&lt;code&gt;rawText&lt;/code&gt;), because when you feed retrieved&lt;br&gt;
chunks to the model for generation you often want the clean body without the breadcrumb noise&lt;br&gt;
repeated ten times in the prompt.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. &lt;code&gt;chunk_size=1000&lt;/code&gt; isn't 1000 of anything you care about
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;RecursiveCharacterTextSplitter&lt;/code&gt; counts characters. Your embedding model has a token limit.&lt;br&gt;
The ratio between them varies enormously with content:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;th&gt;Roughly chars per token&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;English prose&lt;/td&gt;
&lt;td&gt;~4.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dense technical prose&lt;/td&gt;
&lt;td&gt;~3.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code&lt;/td&gt;
&lt;td&gt;~2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Minified JSON / config blobs&lt;/td&gt;
&lt;td&gt;~2.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So a 1,000-character chunk is ~250 tokens of prose but can be ~500 tokens of code. If you sized&lt;br&gt;
your window against a model limit using a character count, your code chunks are silently getting&lt;br&gt;
truncated at the embedding step, and truncation at the embedding step is invisible — no error, no&lt;br&gt;
warning, just a vector for the first 60% of the chunk.&lt;/p&gt;

&lt;p&gt;Count real BPE tokens with the actual tokenizer (&lt;code&gt;tiktoken&lt;/code&gt; / &lt;code&gt;gpt-tokenizer&lt;/code&gt;, &lt;code&gt;cl100k_base&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;o200k_base&lt;/code&gt; depending on your model). It is one dependency and it removes a whole class of&lt;br&gt;
"why is retrieval worse on the API reference pages" investigations.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. It has no idea your docs site has four versions of every page
&lt;/h2&gt;

&lt;p&gt;Documentation sites are duplicate factories. Versioned docs (&lt;code&gt;/v2/&lt;/code&gt;, &lt;code&gt;/v3/&lt;/code&gt;, &lt;code&gt;/latest/&lt;/code&gt;),&lt;br&gt;
locale variants, print views, and framework-generated index pages mean a naive crawl of a&lt;br&gt;
500-page docs site can yield 1,800 chunks where 600 would do.&lt;/p&gt;

&lt;p&gt;Exact-hash dedupe catches byte-identical pages and gets you maybe half of it. The rest needs&lt;br&gt;
near-duplicate detection — SimHash with banded lookup is cheap and works well here.&lt;/p&gt;

&lt;p&gt;But there's a trap. Naive near-duplicate detection will happily collapse:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Install on Linux&lt;/strong&gt; — Run &lt;code&gt;./configure &amp;amp;&amp;amp; make &amp;amp;&amp;amp; make install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install on Windows&lt;/strong&gt; — Run &lt;code&gt;./configure &amp;amp;&amp;amp; make &amp;amp;&amp;amp; make install&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Identical bodies, completely different answers to "how do I install this on Windows?" So&lt;br&gt;
similarity matching has to be &lt;strong&gt;scoped to the heading&lt;/strong&gt;: two chunks are only candidates for&lt;br&gt;
deduplication if their heading path matches too.&lt;/p&gt;


&lt;h2&gt;
  
  
  Doing all four without writing a crawler
&lt;/h2&gt;

&lt;p&gt;I ended up packaging this as an Apify actor —&lt;br&gt;
&lt;strong&gt;&lt;a href="https://apify.com/optirefine/docs-to-rag-pipeline" rel="noopener noreferrer"&gt;Docs-to-RAG Pipeline Builder&lt;/a&gt;&lt;/strong&gt; — because I&lt;br&gt;
was rebuilding the same thing for every project and the crawl half is more annoying than it looks.&lt;/p&gt;

&lt;p&gt;Minimal run:&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;"startUrls"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.example.com"&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;"maxCrawlPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That returns chunks that already respect all four rules above. Each one looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9f2c1a77b0e34d15"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.example.com/api/authentication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"anchorUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.example.com/api/authentication#oauth-2-0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pageTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Authentication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"breadcrumb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Authentication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Authorization flows"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"heading"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OAuth 2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headingLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chunkIndex"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chunkCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Authentication &amp;gt; Authorization flows &amp;gt; OAuth 2.0&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;### OAuth 2.0&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&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;"rawText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"### OAuth 2.0&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&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;"tokenCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;612&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contentHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1b0f..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"generator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mkdocs-material"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"extractor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"profile:mkdocs-material"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"crawledAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-24T10:14:22.104Z"&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;Note &lt;code&gt;anchorUrl&lt;/code&gt;. Deep-linking a citation to the exact heading rather than the page top is a&lt;br&gt;
two-line change that makes a support bot feel dramatically more trustworthy, and almost nobody&lt;br&gt;
does it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Two design decisions worth stealing even if you build your own
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Crawl in two passes, not one.&lt;/strong&gt; Rendering every page in Chromium is slow and expensive;&lt;br&gt;
rendering none of them breaks on every client-side-rendered docs site. So: fetch over plain HTTP&lt;br&gt;
first, score the extracted content, and escalate only the pages that fail the gate — JS shells,&lt;br&gt;
suspiciously thin pages — to a browser. &lt;code&gt;renderingMode: "auto"&lt;/code&gt; with &lt;code&gt;escalateBelowWords: 60&lt;/code&gt;&lt;br&gt;
does this. On a typical docs site the large majority of pages never touch a browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let extraction strategies compete instead of picking one.&lt;/strong&gt; Docs sites are built by generators,&lt;br&gt;
and generators have known DOM shapes. So run several extractors — an explicit&lt;br&gt;
&lt;code&gt;mainContentSelector&lt;/code&gt; if the user gave one, a profile matched to the detected generator (MkDocs,&lt;br&gt;
Docusaurus, Sphinx, Starlight, VitePress and friends), a general readability-style extractor, and&lt;br&gt;
a link-density heuristic falling back to &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; — score all of their outputs, and take the&lt;br&gt;
winner. A single hardcoded selector is a silent failure waiting for the day the docs site&lt;br&gt;
upgrades its theme; it doesn't error, it just starts returning navigation sidebars as content.&lt;/p&gt;

&lt;p&gt;The run report tells you which extractor won on which pages, which is how you find out your&lt;br&gt;
selector went stale.&lt;/p&gt;
&lt;h3&gt;
  
  
  Embeddings without an API bill
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;embeddingProvider&lt;/code&gt; has three settings, and two of them cost nothing:&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;"startUrls"&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs.example.com"&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;"embeddingProvider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cloudflare-worker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workerUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://docs-to-rag-worker.you.workers.dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"workerApiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;API_TOKEN&amp;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;You deploy a small Worker to your own Cloudflare account and it runs &lt;code&gt;@cf/baai/bge-m3&lt;/code&gt; (1024&lt;br&gt;
dims) on Workers AI. The free tier covers roughly 9 million tokens a day — a 500-page site&lt;br&gt;
chunked at 800 tokens uses about &lt;strong&gt;0.5% of one day's allowance&lt;/strong&gt;. Set &lt;code&gt;embeddingProvider: "local"&lt;/code&gt;&lt;br&gt;
instead and it runs &lt;code&gt;Xenova/all-MiniLM-L6-v2&lt;/code&gt; (384 dims) on-device with no network calls after&lt;br&gt;
the model downloads.&lt;/p&gt;

&lt;p&gt;Both are meaningfully worse than a frontier embedding model on hard retrieval. Both are entirely&lt;br&gt;
adequate for "answer questions about our docs", and being free changes how willing you are to&lt;br&gt;
re-embed after changing your chunking — which you will, several times.&lt;/p&gt;

&lt;h3&gt;
  
  
  The artifacts nobody asks for and everybody ends up using
&lt;/h3&gt;

&lt;p&gt;Alongside the dataset it writes to the key-value store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;chunks.jsonl&lt;/code&gt; — the embedding-ready records&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;corpus.md&lt;/code&gt; — the whole site as one readable markdown file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;llms.txt&lt;/code&gt; and &lt;code&gt;llms-full.txt&lt;/code&gt; — the &lt;a href="https://llmstxt.org" rel="noopener noreferrer"&gt;llms.txt&lt;/a&gt; site index format&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run-report.json&lt;/code&gt; — extractor wins, escalations, failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;corpus.md&lt;/code&gt; is the one I use most and expected least. When retrieval gives a wrong answer, being&lt;br&gt;
able to &lt;code&gt;grep&lt;/code&gt; the entire corpus in one file tells you in five seconds whether the content was&lt;br&gt;
missing from the crawl or present-but-not-retrieved. Those two failures look identical from the&lt;br&gt;
chat UI and have completely different fixes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure it on your own corpus, because I can't measure it on yours
&lt;/h2&gt;

&lt;p&gt;Retrieval quality claims that aren't measured on your data aren't worth much, and mine aren't&lt;br&gt;
either. The good news is that this particular A/B is cheap to run, and free if you use local&lt;br&gt;
embeddings.&lt;/p&gt;

&lt;p&gt;Build the same corpus twice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Baseline&lt;/strong&gt; — &lt;code&gt;RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)&lt;/code&gt; over
raw HTML converted to text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure-aware&lt;/strong&gt; — heading-recursive chunking, breadcrumb prefixes on, dedupe on&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then write &lt;strong&gt;20–30 questions you already know the answer to&lt;/strong&gt;. This is the part people skip and&lt;br&gt;
it's the only part that matters. Take them from your actual support inbox or your team's Slack —&lt;br&gt;
questions real people asked, not questions you invented while looking at the docs, which are&lt;br&gt;
biased toward the phrasing the docs already use.&lt;/p&gt;

&lt;p&gt;Three numbers to compare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chunk count.&lt;/strong&gt; The dedupe delta is usually the first thing that jumps out on a versioned
docs site. Fewer chunks at equal coverage is a straight win on embedding cost and retrieval noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hit rate @5&lt;/strong&gt; — for each question, is the chunk containing the answer in the top five? This
is the number that predicts whether your assistant feels good to use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Whole-example rate.&lt;/strong&gt; Of the retrieved chunks containing code, how many contain a complete,
runnable block rather than a fragment? This is the one the sliding window loses badly, and it's
worth counting separately because hit-rate alone hides it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If structure-aware chunking doesn't beat the baseline on your corpus, that is a real and useful&lt;br&gt;
finding — it probably means your docs are flatter than you thought, and your effort belongs in&lt;br&gt;
the retriever or in reranking instead. Measure before you commit to any of this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd push back on myself
&lt;/h2&gt;

&lt;p&gt;Structure-aware chunking is not free. It's a lot more code than eight lines of LangChain, and on&lt;br&gt;
a corpus that isn't structured — support-ticket exports, transcripts, PDFs of scanned contracts —&lt;br&gt;
none of it helps, because there are no headings to be aware of. The sliding window is genuinely&lt;br&gt;
the right default there.&lt;/p&gt;

&lt;p&gt;The claim is narrower than "sliding windows are bad": &lt;strong&gt;documentation is one of the most&lt;br&gt;
structured corpora you will ever index, and throwing that structure away at the chunking step is&lt;br&gt;
the single most common own-goal in RAG pipelines over docs.&lt;/strong&gt; If you're indexing something else,&lt;br&gt;
ignore most of this and go tune your retriever instead.&lt;/p&gt;




&lt;p&gt;The actor is &lt;a href="https://apify.com/optirefine/docs-to-rag-pipeline" rel="noopener noreferrer"&gt;Docs-to-RAG Pipeline Builder&lt;/a&gt;&lt;br&gt;
(MIT-licensed, pay-per-event, runs with a single &lt;code&gt;startUrls&lt;/code&gt; entry). If you'd rather build your&lt;br&gt;
own, the four rules at the top of this post are the ones I'd implement first — in that order.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>llm</category>
      <category>webdev</category>
    </item>
    <item>
      <title>A short-form video script pipeline that runs on a schedule and needs no LLM API key</title>
      <dc:creator>OptiRefine</dc:creator>
      <pubDate>Fri, 28 Aug 2026 02:33:36 +0000</pubDate>
      <link>https://dev.to/optirefine/a-short-form-video-script-pipeline-that-runs-on-a-schedule-and-needs-no-llm-api-key-1ip4</link>
      <guid>https://dev.to/optirefine/a-short-form-video-script-pipeline-that-runs-on-a-schedule-and-needs-no-llm-api-key-1ip4</guid>
      <description>&lt;p&gt;Every "automate your faceless YouTube channel" tutorial follows the same shape. Pull an RSS feed,&lt;br&gt;
&lt;code&gt;for&lt;/code&gt; loop over the items, &lt;code&gt;POST&lt;/code&gt; each one to an LLM with a prompt that says "write a 40-second&lt;br&gt;
TikTok script," write the response to a file. Forty lines. Works on the demo.&lt;/p&gt;

&lt;p&gt;Then you schedule it and the parts nobody wrote about start showing up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The model returns prose with no segment structure roughly one time in six, and your downstream
parser explodes.&lt;/li&gt;
&lt;li&gt;Half the RSS items are three-sentence stubs that link to a paywall. There is nothing to write a
script &lt;em&gt;about&lt;/em&gt;, but the model cheerfully writes one anyway, from nothing.&lt;/li&gt;
&lt;li&gt;Your scripts run 55 seconds when you asked for 40, because nobody counted words against a
speaking rate.&lt;/li&gt;
&lt;li&gt;You pick three categories, and the first one eats the entire item budget before the other two
get a look in.&lt;/li&gt;
&lt;li&gt;You're paying per token for outputs you throw away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I ended up building this as an Apify actor&lt;br&gt;
(&lt;strong&gt;&lt;a href="https://apify.com/optirefine/short-form-script-generator" rel="noopener noreferrer"&gt;TikTok &amp;amp; YouTube Shorts Script Generator&lt;/a&gt;&lt;/strong&gt;),&lt;br&gt;
and the interesting part turned out not to be the generation. It was the five problems above. Here&lt;br&gt;
is how each one gets solved, whether you use the actor or write your own.&lt;/p&gt;
&lt;h2&gt;
  
  
  The output has to be structured, or none of it composes
&lt;/h2&gt;

&lt;p&gt;A script is not a paragraph. It's a timed sequence with distinct jobs per segment, and if your&lt;br&gt;
pipeline treats it as a blob you can't do anything downstream — no b-roll matching, no caption&lt;br&gt;
timing, no per-segment retakes.&lt;/p&gt;

&lt;p&gt;So the output shape is a list of segments, each labelled:&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;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tech"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A hidden phone setting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"40 seconds"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TikTok/YouTube Shorts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"wordCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"segments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HOOK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"durationSec"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"speech"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Your phone has been throttling itself since the day you bought it."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"visual"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Close-up of hand pulling down settings panel, harsh overhead light"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BODY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"speech"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"visual"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CTA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"speech"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"visual"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"generatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-27T14:02:11.884Z"&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;Two things about that shape are load-bearing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;speech&lt;/code&gt; and &lt;code&gt;visual&lt;/code&gt; are separate fields.&lt;/strong&gt; Not one field with stage directions inline. The&lt;br&gt;
moment you want to feed narration to TTS, or match stock footage to a scene description, or&lt;br&gt;
generate captions, you need the spoken words alone with no parenthetical noise in them. Models&lt;br&gt;
will merrily write &lt;code&gt;(cut to close-up)&lt;/code&gt; in the middle of the narration if you let them, and then&lt;br&gt;
your voiceover says "cut to close-up" out loud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;start&lt;/code&gt; and &lt;code&gt;end&lt;/code&gt; exist before there's any audio.&lt;/strong&gt; The plan is a plan. When you do generate&lt;br&gt;
audio, you get &lt;code&gt;actualStart&lt;/code&gt; and &lt;code&gt;actualEnd&lt;/code&gt; measured from the file — and the gap between planned&lt;br&gt;
and actual is the most useful debugging number in the whole pipeline. A hook that planned for 3&lt;br&gt;
seconds and measured 5.2 is a hook that will not survive the scroll.&lt;/p&gt;
&lt;h2&gt;
  
  
  The hook is a per-niche problem, not a prompt problem
&lt;/h2&gt;

&lt;p&gt;This is where most generic pipelines produce unusable output, and it's not fixable by asking&lt;br&gt;
harder in the prompt.&lt;/p&gt;

&lt;p&gt;A tech short and a beauty short have structurally different openings. Tech opens on a&lt;br&gt;
counterintuitive claim — &lt;em&gt;your phone has been throttling itself&lt;/em&gt;. Beauty opens on a visual&lt;br&gt;
result, before/after, no claim at all. News opens on the stakes. Gaming opens mid-action, usually&lt;br&gt;
mid-sentence. Kids' content opens on a question the viewer can answer out loud.&lt;/p&gt;

&lt;p&gt;One prompt cannot do all of those well, which is why single-prompt pipelines produce the&lt;br&gt;
recognisable slop voice — a generic "Did you know that..." opener bolted onto every topic&lt;br&gt;
regardless of niche.&lt;/p&gt;

&lt;p&gt;So category is a first-class input, not a topic string. Each of&lt;br&gt;
&lt;code&gt;tech&lt;/code&gt;, &lt;code&gt;trends&lt;/code&gt;, &lt;code&gt;beauty&lt;/code&gt;, &lt;code&gt;fashion&lt;/code&gt;, &lt;code&gt;sports&lt;/code&gt;, &lt;code&gt;gaming&lt;/code&gt;, &lt;code&gt;news&lt;/code&gt;, &lt;code&gt;learning&lt;/code&gt;, &lt;code&gt;kid friendly&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;music&lt;/code&gt;, &lt;code&gt;general&lt;/code&gt; carries its own persona, hook strategy and segment structure.&lt;/p&gt;

&lt;p&gt;RSS mode, which is the version you'd schedule:&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"categories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tech"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gaming"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"40 seconds"&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;Direct mode, for when you have your own source of topics — a spreadsheet, a scraper, your own&lt;br&gt;
backlog:&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"direct"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tech"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"topic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A hidden phone setting"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Most phones ship with a battery saver that is off by default. Enabling it..."&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;Direct mode is the one that matters if you're building something bigger. It makes this a &lt;em&gt;stage&lt;/em&gt;&lt;br&gt;
in a pipeline rather than the whole pipeline — anything that can produce &lt;code&gt;{category, topic, data}&lt;/code&gt;&lt;br&gt;
can feed it.&lt;/p&gt;
&lt;h3&gt;
  
  
  The multi-category budget trap
&lt;/h3&gt;

&lt;p&gt;Ask for &lt;code&gt;["tech", "gaming", "news"]&lt;/code&gt; with &lt;code&gt;maxItems: 10&lt;/code&gt; and the naive implementation walks the&lt;br&gt;
tech feed first, produces ten tech scripts, and stops. You asked for three categories and got one.&lt;/p&gt;

&lt;p&gt;The fix is to divide the budget across categories and &lt;strong&gt;interleave collection&lt;/strong&gt; rather than&lt;br&gt;
draining feeds in order. It's an obvious bug once you've seen it and an easy one to ship, because&lt;br&gt;
it looks completely fine when you test with a single category.&lt;/p&gt;
&lt;h2&gt;
  
  
  Thin sources produce hallucinated scripts
&lt;/h2&gt;

&lt;p&gt;An RSS item with a 40-character summary and a paywalled link contains no information. Feed it to a&lt;br&gt;
model with "write a script about this" and the model will write one — confidently, and entirely&lt;br&gt;
from its own priors. That's a fabrication with your channel's name on it.&lt;/p&gt;

&lt;p&gt;The gate is boring and it works: &lt;strong&gt;require a summary of at least ~500 characters, or 200+&lt;br&gt;
characters of extracted body text, and skip the item otherwise.&lt;/strong&gt; Thin and paywalled pages get&lt;br&gt;
dropped rather than embellished.&lt;/p&gt;

&lt;p&gt;Whatever you build, put a content-length floor somewhere in it. It is the difference between a&lt;br&gt;
pipeline that summarises and one that invents.&lt;/p&gt;
&lt;h2&gt;
  
  
  Failed generations are a first-class output, not an exception
&lt;/h2&gt;

&lt;p&gt;Small models return unparseable output some of the time. Not often, but often enough that at&lt;br&gt;
scheduled scale you will get some every week. The wrong responses are to retry forever, to crash&lt;br&gt;
the run, or to silently drop the item.&lt;/p&gt;

&lt;p&gt;The right one is to route it: unparseable generations land in a separate &lt;code&gt;failed-generations&lt;/code&gt;&lt;br&gt;
dataset with &lt;code&gt;"status": "unparseable"&lt;/code&gt;, a &lt;code&gt;reason&lt;/code&gt;, and the raw text that came back — &lt;strong&gt;and they&lt;br&gt;
aren't billed&lt;/strong&gt;, because you didn't get a script.&lt;/p&gt;

&lt;p&gt;That last part is worth designing for even in your own build. If you're paying per token, you're&lt;br&gt;
paying for garbage output at the same rate as good output, and you have no incentive signal&lt;br&gt;
telling you the prompt is degrading. Separating billable success from non-billable failure makes&lt;br&gt;
prompt regressions visible instead of just expensive.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;failed-generations&lt;/code&gt; dataset is also the best prompt-debugging corpus you'll get. Read twenty&lt;br&gt;
of them and you'll usually find one specific phrasing in one category that reliably derails the&lt;br&gt;
model.&lt;/p&gt;
&lt;h2&gt;
  
  
  Generation without an API key
&lt;/h2&gt;

&lt;p&gt;The actor runs an embedded &lt;strong&gt;Llama 3.1 8B&lt;/strong&gt; by default. No OpenAI key, no Anthropic key, no&lt;br&gt;
separate billing relationship — you run it and it generates.&lt;/p&gt;

&lt;p&gt;That is not because an 8B model is better. It isn't. It's that short-form scripts are close to the&lt;br&gt;
ideal task for a small model: ~100 words of output, rigid structure, strong per-category&lt;br&gt;
scaffolding around the prompt. Almost all of the quality here comes from the structure, and very&lt;br&gt;
little from raw model capability. When the scaffolding does most of the work, model size stops&lt;br&gt;
being the bottleneck.&lt;/p&gt;

&lt;p&gt;If you disagree, the escape hatch is documented: deploy the worker in &lt;code&gt;worker/&lt;/code&gt; to your own&lt;br&gt;
Cloudflare account and pass &lt;code&gt;workerUrl&lt;/code&gt; and &lt;code&gt;workerSecret&lt;/code&gt; &lt;strong&gt;as a pair&lt;/strong&gt; — a URL without a secret&lt;br&gt;
is rejected, and the built-in generator's own secret is never forwarded to a custom worker.&lt;/p&gt;
&lt;h3&gt;
  
  
  Voiceover, if you want it
&lt;/h3&gt;

&lt;p&gt;Add an ElevenLabs key and each segment gets an &lt;code&gt;audioUrl&lt;/code&gt; plus measured timings:&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;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rss"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"categories"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tech"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"elevenLabsApiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your key&amp;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;"voiceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"21m00Tcm4TlvDq8ikWAM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ttsModelId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eleven_turbo_v2_5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ttsConcurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&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;Two things to know. &lt;strong&gt;ElevenLabs bills your account directly&lt;/strong&gt;, not through Apify — the run log&lt;br&gt;
estimates the character count up front so you can see the damage before it happens. And&lt;br&gt;
&lt;code&gt;ttsConcurrency&lt;/code&gt; defaults to 2 because the free tier will rate-limit you above that; raise it only&lt;br&gt;
if you're on a paid plan.&lt;/p&gt;
&lt;h2&gt;
  
  
  What to expect from the output, honestly
&lt;/h2&gt;

&lt;p&gt;An 8B model producing 100 words against a rigid template gets you a solid first draft and not a&lt;br&gt;
finished script. In practice the parts that hold up and the parts you rewrite are consistent:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Usually fine as-is:&lt;/strong&gt; the segment structure and timing, the visual cues (they're generic but&lt;br&gt;
they're a real shot list, which is more than a blank page), and the CTA.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Usually needs a pass:&lt;/strong&gt; the hook. It's the highest-leverage seven words in the whole video and&lt;br&gt;
it's the thing a small model is weakest at, because a good hook depends on knowing what your&lt;br&gt;
specific audience already believes. Expect to rewrite most of them. That's fine — rewriting one&lt;br&gt;
line beats writing five.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch for:&lt;/strong&gt; claims stated more confidently than the source supports. The content-length gate&lt;br&gt;
stops the worst of it, but a summary that says "researchers suggest" can come out the other end&lt;br&gt;
as "researchers proved." Read the &lt;code&gt;speech&lt;/code&gt; fields against the &lt;code&gt;sourceUrl&lt;/code&gt; before you record&lt;br&gt;
anything factual.&lt;/p&gt;

&lt;p&gt;The right mental model is that this replaces the blank page and the timing math, not the editorial&lt;br&gt;
judgement. If you generate ten and keep three, it's working correctly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Putting it on a schedule
&lt;/h2&gt;

&lt;p&gt;The whole point is that you don't run this by hand. In Apify, a schedule is a cron expression on&lt;br&gt;
the actor — &lt;code&gt;0 7 * * *&lt;/code&gt; for a daily 7am batch — and the output accumulates in a dataset you can&lt;br&gt;
pull from anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://api.apify.com/v2/datasets/&lt;/span&gt;&lt;span class="nv"&gt;$DATASET_ID&lt;/span&gt;&lt;span class="s2"&gt;/items?clean=true&amp;amp;format=json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[] | select(.status == "ok")
           | "\(.topic)\n" + (.segments[] | "  [\(.label)] \(.speech)")'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A sensible daily loop looks like: generate 10 in the morning → you skim and keep 3 → those 3 go to&lt;br&gt;
a shot list → record or assemble → publish. The pipeline's job is to make the &lt;em&gt;skim&lt;/em&gt; cheap. It is&lt;br&gt;
not to publish unattended, and I'd argue strongly against wiring the output directly to an upload&lt;br&gt;
API. The value is in cutting the blank-page cost of the first draft, not in removing the human&lt;br&gt;
from the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits worth knowing before you build on it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;maxItems&lt;/code&gt; caps at 50 per run.&lt;/strong&gt; Shared inference allowance; run more often rather than bigger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;generationConcurrency&lt;/code&gt; defaults to 5&lt;/strong&gt; (max 20), &lt;code&gt;ttsConcurrency&lt;/code&gt; to 2 (max 10).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's a run deadline&lt;/strong&gt; (&lt;code&gt;runDeadlineSeconds&lt;/code&gt;, default 210) and a per-item timeout. Long
batches with TTS on will hit them — split the work rather than raising both to the ceiling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RSS quality is the ceiling on output quality.&lt;/strong&gt; Use &lt;code&gt;feedOverrides&lt;/code&gt; to point at feeds that
publish real summaries. Garbage in still applies, and no amount of prompt work fixes a feed that
publishes headlines only.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The actor is&lt;br&gt;
&lt;strong&gt;&lt;a href="https://apify.com/optirefine/short-form-script-generator" rel="noopener noreferrer"&gt;TikTok &amp;amp; YouTube Shorts Script Generator&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
— pay-per-event, and unparseable generations aren't charged.&lt;/p&gt;

&lt;p&gt;If you're building your own instead: the four things worth copying, in order, are the&lt;br&gt;
segment-level output shape, the per-category hook strategy, the content-length gate on the source,&lt;br&gt;
and the separate failure dataset. The model call is the least interesting part of the whole thing.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>After CHAINDROP, I stopped trusting npm audit. Here’s the dependency check I run instead.</title>
      <dc:creator>OptiRefine</dc:creator>
      <pubDate>Fri, 28 Aug 2026 02:31:22 +0000</pubDate>
      <link>https://dev.to/optirefine/after-chaindrop-i-stopped-trusting-npm-audit-heres-the-dependency-check-i-run-instead-3cii</link>
      <guid>https://dev.to/optirefine/after-chaindrop-i-stopped-trusting-npm-audit-heres-the-dependency-check-i-run-instead-3cii</guid>
      <description>&lt;p&gt;On 4 August 2026, the CHAINDROP wave of the Shai-Hulud worm compromised over 400 npm packages&lt;br&gt;
with a combined 1.3 billion monthly downloads. &lt;code&gt;keyv&lt;/code&gt; alone accounts for around 600 million of&lt;br&gt;
those. &lt;code&gt;flat-cache&lt;/code&gt;, &lt;code&gt;cacheable-request&lt;/code&gt;, &lt;code&gt;cache-manager&lt;/code&gt; — none of them packages you chose.&lt;br&gt;
All of them packages you have.&lt;/p&gt;

&lt;p&gt;The worm stole credentials for AI providers, AWS, GCP, Azure, and GitHub, obfuscated itself with&lt;br&gt;
control-flow flattening and Base91 string encoding, and pulled its command-and-control address&lt;br&gt;
out of an Ethereum smart contract so takedowns wouldn't stick.&lt;/p&gt;

&lt;p&gt;By the time you read this, that specific campaign is contained. The pattern is not. This is the&lt;br&gt;
third round of the same worm family, and each round has worked the same way: compromise a&lt;br&gt;
maintainer account, publish a patch version, wait for everyone's &lt;code&gt;^&lt;/code&gt; range to pick it up.&lt;/p&gt;

&lt;p&gt;So here's the uncomfortable thing about the tool most of us reach for first.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;npm audit&lt;/code&gt; answers the wrong question
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; tells you which of your dependencies have &lt;strong&gt;published advisories&lt;/strong&gt;. That is a useful&lt;br&gt;
question. It is not the question these attacks punish you for.&lt;/p&gt;

&lt;p&gt;A malicious version published forty minutes ago has no advisory. It has no CVE. It will pass&lt;br&gt;
&lt;code&gt;npm audit&lt;/code&gt; cleanly until a researcher files it — which, in the CHAINDROP case, took hours&lt;br&gt;
during which the package was being installed continuously.&lt;/p&gt;

&lt;p&gt;What &lt;em&gt;would&lt;/em&gt; have flagged those packages ahead of time is duller and more boring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maintainer count of one.&lt;/strong&gt; A single compromised account is a single point of failure, and
most of the worm's entry points were solo-maintained packages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long silence, then a sudden patch release.&lt;/strong&gt; A package that hadn't shipped in fourteen months
suddenly cutting a patch version at 3am is the signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transitive depth.&lt;/strong&gt; The packages that did the damage were four and five levels down, where
nobody looks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deprecation.&lt;/strong&gt; Deprecated packages still installed in production are unowned attack surface.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is a vulnerability. All of it is risk. And there is no single command that gives it&lt;br&gt;
to you across a whole &lt;code&gt;package-lock.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is the gap I kept hitting, so I built something to fill it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The check I actually run
&lt;/h2&gt;

&lt;p&gt;The data already exists, publicly, in four places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm registry / PyPI&lt;/strong&gt; — versions, licenses, maintainers, deprecation flags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://osv.dev" rel="noopener noreferrer"&gt;OSV.dev&lt;/a&gt;&lt;/strong&gt; — vulnerability advisories, properly version-scoped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://deps.dev" rel="noopener noreferrer"&gt;deps.dev&lt;/a&gt;&lt;/strong&gt; — the resolved dependency graph, including transitives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pypistats / npm downloads&lt;/strong&gt; — adoption signal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The annoying part isn't getting any one of them. It's getting all four, for 300 packages, joined&lt;br&gt;
on the same key, without writing four rate-limit handlers and a cache.&lt;/p&gt;

&lt;p&gt;I wrapped that into an Apify actor —&lt;br&gt;
&lt;strong&gt;&lt;a href="https://apify.com/optirefine/package-intelligence" rel="noopener noreferrer"&gt;npm, PyPI &amp;amp; crates.io Package Intelligence&lt;/a&gt;&lt;/strong&gt; —&lt;br&gt;
that takes a list of packages and returns one row each: version, license, maintainer count,&lt;br&gt;
download volume, OSV advisories, deps.dev dependency counts, and a 0–100 health score.&lt;/p&gt;

&lt;p&gt;The bulk endpoint takes up to 50 packages per call, which is what makes this practical against a&lt;br&gt;
real lock file rather than a curiosity you run on one package.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 1 — get your actual dependency list
&lt;/h3&gt;

&lt;p&gt;Not your &lt;code&gt;package.json&lt;/code&gt;. Your lock file, because that's where the transitives live:&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;# npm — every package in the resolved tree, deduplicated&lt;/span&gt;
npm &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'[.. | objects | select(has("version")) | .name?] | unique | .[]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'^null$'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; packages.txt

&lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; packages.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip list &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[].name'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; packages.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a mid-sized Node service this is usually somewhere between 400 and 1,200 lines. That number&lt;br&gt;
alone is worth sitting with for a second.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2 — score them in bulk
&lt;/h3&gt;

&lt;p&gt;Input to the actor is deliberately boring:&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;"packages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"keyv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flat-cache"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cacheable-request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lodash"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ecosystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;Package names can carry an ecosystem prefix, so a polyglot monorepo goes in as one run rather&lt;br&gt;
than three:&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;"packages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"npm:keyv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pypi:requests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crates:serde"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"includeDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&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;Run it from the API and pipe the dataset straight into &lt;code&gt;jq&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;jq &lt;span class="nt"&gt;-Rn&lt;/span&gt; &lt;span class="s1"&gt;'{packages: [inputs], includeDependencies: true}'&lt;/span&gt; &amp;lt; packages.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; input.json

curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.apify.com/v2/acts/optirefine~package-intelligence/run-sync-get-dataset-items?token=&lt;/span&gt;&lt;span class="nv"&gt;$APIFY_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @input.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; health.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The scoring endpoints are pay-per-event and priced per package; the raw lookup endpoints&lt;br&gt;
(metadata, vulns, deps, downloads) are free at 60/min and 2,000/day, so you can prototype the&lt;br&gt;
whole pipeline before spending anything. Packages that don't exist return 404 without charging,&lt;br&gt;
which matters more than it sounds when you feed it a lock file containing internal scoped&lt;br&gt;
packages.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3 — sort by the thing that actually predicts trouble
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'
  map(select(.maintainerCount == 1 and .downloads &amp;gt; 1000000))
  | sort_by(.healthScore)
  | .[]
  | [.name, .healthScore, .maintainerCount, .downloads, .lastPublished]
  | @tsv
'&lt;/span&gt; health.json | column &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;That query — &lt;strong&gt;high download volume, single maintainer, low health score&lt;/strong&gt; — is the CHAINDROP&lt;br&gt;
shape. It is the list I'd want on a screen before approving any dependency bump.&lt;/p&gt;
&lt;h2&gt;
  
  
  The four numbers worth writing down
&lt;/h2&gt;

&lt;p&gt;When you run this against your own tree, four counts tell you almost everything. I'd write them&lt;br&gt;
in a comment on the PR that introduces the check, so you have a baseline to compare against in&lt;br&gt;
six months:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Total packages in the resolved tree.&lt;/strong&gt; Not direct dependencies — everything. This is the
number that makes people quiet in a room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many are single-maintainer with over a million weekly downloads.&lt;/strong&gt; Your CHAINDROP-shaped
surface. This is the list a human should actually read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many are deprecated but still installed.&lt;/strong&gt; Unowned code running in production. Every one
of these is either a migration ticket or a decision to accept the risk on purpose — and it
should be one of those two, not neither.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many haven't published in over a year.&lt;/strong&gt; Not inherently bad — some packages are simply
finished. But combined with (2), it's the set where a sudden patch release should make you
look rather than merge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Track those four over time rather than chasing an aggregate score. The absolute numbers vary&lt;br&gt;
enormously by ecosystem and project age, so your own trend is the only meaningful comparison.&lt;br&gt;
What you want to see is (2) and (3) going down while (1) goes up, which is what a team actively&lt;br&gt;
managing its surface looks like.&lt;/p&gt;
&lt;h2&gt;
  
  
  How the health score is built (and why I kept it dumb)
&lt;/h2&gt;

&lt;p&gt;The score is a documented v1 heuristic, not a model. It reads on these signals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Release recency&lt;/strong&gt; — how long since the last publish&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintainer count&lt;/strong&gt; — one is a risk, not a virtue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deprecation status&lt;/strong&gt; — flagged hard&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vulnerability load&lt;/strong&gt; — OSV advisories resolved against the &lt;em&gt;current&lt;/em&gt; version, not every
version ever published&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency health&lt;/strong&gt; — the shape of what it pulls in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point about OSV is the one I'd defend hardest. A lot of tooling reports every advisory&lt;br&gt;
ever filed against a package name, which makes well-maintained packages that patched promptly&lt;br&gt;
look worse than abandoned ones that never had a researcher look at them. Scoring against the&lt;br&gt;
resolved current version fixes that inversion.&lt;/p&gt;

&lt;p&gt;The score is deliberately auditable. Every input is a public field you can go and check yourself.&lt;br&gt;
If you disagree with the weighting, the raw fields are all in the same row and you can build your&lt;br&gt;
own — which I'd encourage, because the right weighting depends on your threat model, not mine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does not do:&lt;/strong&gt; it doesn't detect malicious code. Nothing in this pipeline reads a&lt;br&gt;
package's source. It measures the conditions under which a compromise is more likely to go&lt;br&gt;
unnoticed. Those are different claims, and I'd rather be clear about which one I'm making.&lt;/p&gt;
&lt;h2&gt;
  
  
  Wire it into CI, or it won't happen
&lt;/h2&gt;

&lt;p&gt;The version that actually changes behaviour runs on pull requests that touch the lock file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/dependency-health.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dependency health&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;package-lock.json'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;requirements.txt'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;poetry.lock'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;health&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;2&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Diff newly added packages&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;added&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git diff HEAD^ HEAD -- package-lock.json \&lt;/span&gt;
            &lt;span class="s"&gt;| grep -oP '^\+\s+"node_modules/\K[^"]+' \&lt;/span&gt;
            &lt;span class="s"&gt;| sort -u &amp;gt; added.txt&lt;/span&gt;
          &lt;span class="s"&gt;echo "count=$(wc -l &amp;lt; added.txt)" &amp;gt;&amp;gt; "$GITHUB_OUTPUT"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Score them&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.added.outputs.count != '0'&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APIFY_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./scripts/score-packages.sh added.txt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One deliberate design choice worth copying: &lt;strong&gt;only score newly added dependencies, not version&lt;br&gt;
bumps of existing ones.&lt;/strong&gt; Scoring the whole tree on every PR produces a wall of noise that people&lt;br&gt;
learn to click through within a week, and a check everyone ignores is worse than no check.&lt;/p&gt;

&lt;p&gt;Pair it with the boring controls that would have actually stopped CHAINDROP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;soak period&lt;/strong&gt; before adopting updates — pin exact versions and let new releases age 48–72h&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;npm 12+&lt;/code&gt;, which &lt;strong&gt;blocks &lt;code&gt;preinstall&lt;/code&gt; hooks by default&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;2FA enforced on every npm account you own&lt;/li&gt;
&lt;li&gt;Post-incident: rotate GitHub tokens, npm credentials, and cloud keys from any machine that
installed during the window, and grep your repos for unauthorized commits (CHAINDROP's were
labelled &lt;code&gt;chore: update config&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part I'd argue about
&lt;/h2&gt;

&lt;p&gt;Health scoring is a lagging measure dressed as a leading one. A single-maintainer package with a&lt;br&gt;
low score has been that way for years without being compromised, and most of them never will be.&lt;br&gt;
Treat the score as a &lt;strong&gt;prioritisation aid for where to spend review attention&lt;/strong&gt;, not as a gate&lt;br&gt;
that blocks merges. I've watched teams turn a score threshold into a hard CI failure and then&lt;br&gt;
spend the next month adding exceptions until the check meant nothing.&lt;/p&gt;

&lt;p&gt;The useful version of this is smaller than it sounds: a weekly list of the fifteen riskiest&lt;br&gt;
things in your tree, looked at by a human for ten minutes.&lt;/p&gt;




&lt;p&gt;The actor is here if it's useful:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://apify.com/optirefine/package-intelligence" rel="noopener noreferrer"&gt;npm, PyPI &amp;amp; crates.io Package Intelligence&lt;/a&gt;&lt;/strong&gt;.&lt;br&gt;
Free endpoints for the raw registry, vulnerability and dependency data; pay-per-event for the&lt;br&gt;
health scoring, so a one-off audit of a few hundred packages costs about the price of a coffee.&lt;/p&gt;

&lt;p&gt;If you run it against your own tree, I'd like to hear what your single-maintainer count came out&lt;br&gt;
at — and whether the number surprised you. That's the one I've never seen anyone guess correctly&lt;br&gt;
in advance.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.elastic.co/security-labs/shai-hulud-chaindrop-npm-supply-chain" rel="noopener noreferrer"&gt;Shai-Hulud strikes again: CHAINDROP worm hits 400+ npm packages — Elastic Security Labs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://unit42.paloaltonetworks.com/npm-supply-chain-attack/" rel="noopener noreferrer"&gt;"Shai-Hulud" Worm Compromises npm Ecosystem — Unit 42, Palo Alto Networks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://research.jfrog.com/post/shai-hulud-is-back-august/" rel="noopener noreferrer"&gt;Major Shai-Hulud campaign strikes npm again, affecting keyv and 400+ packages — JFrog Security Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem" rel="noopener noreferrer"&gt;Widespread Supply Chain Compromise Impacting npm Ecosystem — CISA&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>npm</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>How AuraWatch Finds Security Bugs Your Linter Will Never Catch</title>
      <dc:creator>OptiRefine</dc:creator>
      <pubDate>Tue, 05 May 2026 20:14:10 +0000</pubDate>
      <link>https://dev.to/optirefine/how-aurawatch-finds-security-bugs-your-linter-will-never-catch-1418</link>
      <guid>https://dev.to/optirefine/how-aurawatch-finds-security-bugs-your-linter-will-never-catch-1418</guid>
      <description>&lt;h1&gt;
  
  
  How AuraWatch Finds Security Bugs Your Linter Will Never Catch
&lt;/h1&gt;

&lt;p&gt;Most security tools for developers fall into one of two camps: static linters that pattern-match on surface-level code, or AI-powered scanners that guess based on training data. Both have the same fundamental problem — they can't &lt;em&gt;reason&lt;/em&gt; about your code. They can tell you that &lt;code&gt;eval()&lt;/code&gt; exists, but they can't tell you whether user input actually reaches it.&lt;/p&gt;

&lt;p&gt;AuraWatch is built differently. It uses a six-engine deterministic analysis pipeline that constructs a full semantic model of your code and reasons about it mathematically. No guessing. No false positives from pattern matching. Provable results.&lt;/p&gt;

&lt;p&gt;AuraWatch launches on the VS Code Marketplace on &lt;strong&gt;May 15th, 2026&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's how it works under the hood.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With Existing Tools
&lt;/h2&gt;

&lt;p&gt;Consider this code:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;results&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SELECT * FROM products WHERE name = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;term&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="nx"&gt;res&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="nx"&gt;results&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;A regex-based linter might flag the template literal inside &lt;code&gt;query()&lt;/code&gt; if it's lucky. But what about this:&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/search&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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;term&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;term&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;cleaned&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;term&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&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;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SELECT * FROM products WHERE name = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cleaned&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="nx"&gt;res&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="nx"&gt;results&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 data moved through an intermediate variable. Most linters miss this entirely because they're not tracking &lt;em&gt;data flow&lt;/em&gt; — they're just looking for patterns. AuraWatch catches both because it builds a complete picture of how data moves through your code.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Six-Engine Pipeline
&lt;/h2&gt;

&lt;p&gt;Every scan runs six independent engines in parallel. Each engine looks at the code from a different angle. Their results are merged and deduplicated before being returned.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engine 1 — LST Analyzer (Lossless Semantic Tree)
&lt;/h3&gt;

&lt;p&gt;The foundation of AuraWatch is a Lossless Semantic Tree, built using &lt;strong&gt;ts-morph&lt;/strong&gt; for JavaScript/TypeScript and &lt;strong&gt;LibCST&lt;/strong&gt; for Python. Unlike a standard AST, an LST preserves every token in the original source including whitespace, comments, and formatting. This matters because transformers need to reconstruct the original code faithfully when applying fixes.&lt;/p&gt;

&lt;p&gt;The LST engine runs nine rule-based analyzers over the tree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;eval()&lt;/code&gt; calls&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; without DOMPurify&lt;/li&gt;
&lt;li&gt;Hardcoded credentials in variable declarations&lt;/li&gt;
&lt;li&gt;Hardcoded credentials in object literal properties (catches &lt;code&gt;{ password: 'secret' }&lt;/code&gt; inside database config objects — a pattern most tools miss)&lt;/li&gt;
&lt;li&gt;SQL injection via template literals and string concatenation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;child_process.exec()&lt;/code&gt; with dynamic arguments&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Math.random()&lt;/code&gt; used to generate security-sensitive values&lt;/li&gt;
&lt;li&gt;Unguarded database connection calls (null dereference)&lt;/li&gt;
&lt;li&gt;Deep property chains without optional chaining&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each analyzer returns a structured vulnerability object with a unique ID, CWE classification, line number, severity, and a flag indicating whether a deterministic auto-fix is available.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engine 2 — Type-Aware Taint Analysis
&lt;/h3&gt;

&lt;p&gt;This is where AuraWatch goes beyond linting. The taint engine performs inter-procedural data flow analysis to track user-controlled data from its &lt;em&gt;source&lt;/em&gt; to a dangerous &lt;em&gt;sink&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sources&lt;/strong&gt; are any access to HTTP request data:&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;
&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;
&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-custom&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;&lt;strong&gt;Sinks&lt;/strong&gt; are functions where tainted data causes harm:&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="nf"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;// CWE-95: Code Injection&lt;/span&gt;
&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;          &lt;span class="c1"&gt;// CWE-78: OS Command Injection&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;      &lt;span class="c1"&gt;// CWE-89: SQL Injection&lt;/span&gt;
&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// CWE-22: Path Traversal&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;// CWE-601: Open Redirect&lt;/span&gt;
&lt;span class="nx"&gt;innerHTML&lt;/span&gt;       &lt;span class="c1"&gt;// CWE-79: XSS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine works in three passes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Source collection&lt;/strong&gt; — walks all variable declarations and assignments to identify variables initialized from &lt;code&gt;req.*&lt;/code&gt; values, including destructured patterns like &lt;code&gt;const { username, password } = req.body&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Taint propagation&lt;/strong&gt; — marks any variable that flows from a tainted source as tainted itself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sink matching&lt;/strong&gt; — checks whether any argument to a known sink contains a tainted variable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When a tainted variable reaches a sink, AuraWatch reports which specific variables carried the taint and exactly where the data entered the program.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engine 3 — JS-SMT Symbolic Constraint Solver
&lt;/h3&gt;

&lt;p&gt;SMT (Satisfiability Modulo Theories) solving is traditionally the domain of formal verification — not developer tooling. AuraWatch brings a lightweight SMT constraint solver directly into the VS Code extension pipeline.&lt;/p&gt;

&lt;p&gt;The engine maintains a &lt;strong&gt;range map&lt;/strong&gt; for every integer variable it encounters, tracking the possible values each variable can take given the conditional guards in scope:&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;// After seeing: if (count &amp;gt; 0)&lt;/span&gt;
&lt;span class="c1"&gt;// The engine knows: count.min = 1&lt;/span&gt;

&lt;span class="c1"&gt;// After seeing: if (count &amp;lt; 10)&lt;/span&gt;
&lt;span class="c1"&gt;// The engine knows: count.max = 9&lt;/span&gt;

&lt;span class="c1"&gt;// Combined: count ∈ [1, 9]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this range information, the engine can prove:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Division by zero:&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;function&lt;/span&gt; &lt;span class="nf"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pageSize&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="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// SMT proves pageSize can be 0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Contradictory conditions (dead code):&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;// SMT proves these are mutually exclusive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Always-true auth bypasses:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isAdmin&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// SMT proves this always executes&lt;/span&gt;
    &lt;span class="nf"&gt;deleteAllUsers&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;For Python, AuraWatch uses the &lt;strong&gt;Z3 SMT solver&lt;/strong&gt; directly via its Python bindings, enabling full logical formula construction and satisfiability checking. The JavaScript engine uses a custom lightweight interval arithmetic solver that runs in-process without any native dependencies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engine 4 — Async Flow and Prototype Pollution Tracker
&lt;/h3&gt;

&lt;p&gt;This engine handles two distinct but related categories of JavaScript-specific vulnerabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prototype pollution&lt;/strong&gt; occurs when user-controlled data is used as an object key in an assignment:&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;// User sends: { "__proto__": { "isAdmin": true } }&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&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;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// key = "__proto__"&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;If &lt;code&gt;key&lt;/code&gt; equals &lt;code&gt;__proto__&lt;/code&gt;, &lt;code&gt;constructor&lt;/code&gt;, or &lt;code&gt;prototype&lt;/code&gt;, this mutates the base &lt;code&gt;Object&lt;/code&gt; prototype — affecting every object in the process. AuraWatch detects computed property assignments where the key is user-controlled, direct &lt;code&gt;__proto__&lt;/code&gt; mutations, and &lt;code&gt;Object.assign()&lt;/code&gt; calls with untrusted sources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unhandled promise rejection&lt;/strong&gt; crashes Node.js processes in production (since Node 15). The engine identifies promise chains that use &lt;code&gt;.then()&lt;/code&gt; without &lt;code&gt;.catch()&lt;/code&gt; at the expression statement level — a common oversight when converting callback-based code to async.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engine 5 — Hallucination Defense
&lt;/h3&gt;

&lt;p&gt;This engine is specifically designed for the AI-generated code era. Large language models frequently &lt;em&gt;hallucinate&lt;/em&gt; npm package names — inventing packages that don't exist on the npm registry. When a developer installs an AI-suggested package and the real package doesn't exist, a malicious actor can publish a package with that exact name. This is a supply chain attack vector that barely existed five years ago.&lt;/p&gt;

&lt;p&gt;AuraWatch maintains a curated registry of ~200 well-known legitimate packages. For every &lt;code&gt;require()&lt;/code&gt; call and &lt;code&gt;import&lt;/code&gt; statement it finds, it checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is the package in the known-good registry?&lt;/li&gt;
&lt;li&gt;Does the package name match a known typosquatting pattern?&lt;/li&gt;
&lt;li&gt;If a &lt;code&gt;package.json&lt;/code&gt; is provided in the scan request, is the package listed as a dependency?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Typosquatting patterns are matched against a curated list of confusable names:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lodas  → lodash
expres → express
axioss → axios
helmt  → helmet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A package flagged as potentially hallucinated gets a CRITICAL severity finding — because installing a non-existent package that gets claimed by an attacker is code execution on install via the &lt;code&gt;postinstall&lt;/code&gt; lifecycle hook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Engine 6 — Framework-Specific Sink Maps
&lt;/h3&gt;

&lt;p&gt;Generic rules miss framework-specific vulnerabilities. This engine knows the dangerous patterns specific to Express, Fastify, Next.js, and React.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Express:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;res.render(userInput)&lt;/code&gt; — Server-Side Template Injection when the template name is user-controlled&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;res.redirect(userInput)&lt;/code&gt; — Open Redirect when the destination URL comes from user input&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;res.send(\&lt;/code&gt;${userInput}&lt;code&gt;)&lt;/code&gt; — Reflected XSS when user data appears in an HTML response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Next.js:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;context.query&lt;/code&gt; values spread directly into page props in &lt;code&gt;getServerSideProps&lt;/code&gt; without validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fastify:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;reply.send(request.body)&lt;/code&gt; — echoing raw request body in responses that may be rendered as HTML&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The framework is auto-detected from import statements — no configuration required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deterministic Auto-Fixing
&lt;/h2&gt;

&lt;p&gt;When AuraWatch finds a vulnerability with a known safe transformation, it can fix it automatically using the same LST infrastructure it uses for analysis.&lt;/p&gt;

&lt;p&gt;Fixers work as &lt;strong&gt;CST Transformers&lt;/strong&gt; — they traverse the syntax tree and replace unsafe nodes with safe equivalents while preserving all surrounding formatting and comments:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vulnerability&lt;/th&gt;
&lt;th&gt;Auto-Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hardcoded secret&lt;/td&gt;
&lt;td&gt;Replaced with `process.env.VARIABLE_NAME \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;{% raw %}&lt;code&gt;Math.random()&lt;/code&gt; in security context&lt;/td&gt;
&lt;td&gt;Replaced with &lt;code&gt;crypto.randomBytes(4).readUInt32BE(0)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Wrapped with &lt;code&gt;DOMPurify.sanitize()&lt;/code&gt;, import added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;eval(x)&lt;/code&gt; (Python)&lt;/td&gt;
&lt;td&gt;Replaced with &lt;code&gt;ast.literal_eval(x)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;pickle.loads(x)&lt;/code&gt; (Python)&lt;/td&gt;
&lt;td&gt;Replaced with &lt;code&gt;json.loads(x)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;yaml.load(x)&lt;/code&gt; (Python)&lt;/td&gt;
&lt;td&gt;Replaced with &lt;code&gt;yaml.safe_load(x)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;subprocess.run(shell=True)&lt;/code&gt; (Python)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;shell=True&lt;/code&gt; argument removed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unguarded async result&lt;/td&gt;
&lt;td&gt;Property accesses converted to optional chaining (&lt;code&gt;?.&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Fixes are presented as a unified diff in VS Code's native diff viewer before anything is written to disk. The user reviews the patch and explicitly accepts or rejects it — AuraWatch never modifies your files without confirmation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Two Engines, One Interface
&lt;/h2&gt;

&lt;p&gt;AuraWatch runs as two separate Cloud Run services behind a single VS Code extension:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python Engine&lt;/strong&gt; (LibCST + Z3)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handles &lt;code&gt;.py&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;Uses LibCST for lossless CST analysis&lt;/li&gt;
&lt;li&gt;Uses Z3 for formal SMT solving&lt;/li&gt;
&lt;li&gt;10 LST analyzers, 6 auto-fixers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;JS/TS Engine&lt;/strong&gt; (ts-morph + custom SMT)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Handles &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.jsx&lt;/code&gt;, &lt;code&gt;.tsx&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;Uses ts-morph for full TypeScript-aware AST analysis&lt;/li&gt;
&lt;li&gt;Custom interval arithmetic SMT solver&lt;/li&gt;
&lt;li&gt;6 base analyzers + 4 advanced engines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The VS Code extension automatically routes each scan to the correct engine based on the active file's language. The API key, billing, and auth system are unified — one account works across both engines.&lt;/p&gt;

&lt;p&gt;Authentication uses magic links — no passwords. When a user signs in, a time-limited token is written to Firestore, emailed to the user, and validated server-side when clicked. The resulting API key is stored in VS Code's native &lt;code&gt;SecretStorage&lt;/code&gt; API (the OS keychain) — never in plaintext settings files.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The roadmap includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions integration&lt;/strong&gt; — run the full analysis pipeline as a CI check on every pull request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inline diagnostics&lt;/strong&gt; — surface findings as VS Code squiggles directly in the editor without opening the chat panel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java and Go support&lt;/strong&gt; — extending the LST engine to additional languages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-function taint tracking&lt;/strong&gt; — following data flow across function boundaries and module imports&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Launch
&lt;/h2&gt;

&lt;p&gt;AuraWatch launches on the VS Code Marketplace on &lt;strong&gt;May 15th, 2026&lt;/strong&gt;. Follow along on Twitter/X and LinkedIn to get notified on launch day. If you work in security or DevSecOps and want to give early feedback, reach out — every piece of feedback shapes what gets built next.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>security</category>
      <category>webdev</category>
      <category>python</category>
    </item>
    <item>
      <title>The Code Review Comment I Left 47 Times</title>
      <dc:creator>OptiRefine</dc:creator>
      <pubDate>Tue, 28 Apr 2026 00:33:22 +0000</pubDate>
      <link>https://dev.to/optirefine/the-code-review-comment-i-left-47-times-1e7f</link>
      <guid>https://dev.to/optirefine/the-code-review-comment-i-left-47-times-1e7f</guid>
      <description>&lt;p&gt;I counted once. Not proud of it.&lt;/p&gt;

&lt;p&gt;Forty-seven times across different PRs, different engineers, different companies — I left some version of the same comment. It usually looked like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Hey — this nested loop is going to hurt you at scale. You're doing O(n²) work here. Can you use a set for the inner lookup instead?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes the author got it immediately and fixed it in ten minutes. Sometimes I'd spend forty-five minutes in a thread explaining what O(n²) actually means in practice, with benchmarks, with examples, with a rewrite pasted directly into the comment. Sometimes the fix went in. Sometimes a slightly different version of the same pattern showed up in the next PR from the same person.&lt;/p&gt;

&lt;p&gt;And every single time, I thought the same thing: &lt;em&gt;this should not require a human.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Thing Nobody Talks About in Code Review
&lt;/h2&gt;

&lt;p&gt;Code review has this mythology around it. We talk about it like it's this high-value, high-skill practice where senior engineers pass down wisdom to junior ones. And sometimes it is. The architectural discussions, the "have you considered what happens when this service goes down" conversations, the stuff that requires genuine experience and judgment — that's worth a human's time.&lt;/p&gt;

&lt;p&gt;But a massive chunk of code review isn't that. It's pattern matching. It's the same ten categories of problem appearing in slightly different costumes, over and over, in every codebase, at every company. Nested loops. Hardcoded credentials. Unused variables. Functions so tangled with conditionals that you need a flowchart to test them. Files left open without context managers. List comprehensions written as manual loops for no reason.&lt;/p&gt;

&lt;p&gt;These aren't judgment calls. They have objectively correct answers. And yet we route them through humans anyway, which means they get caught inconsistently, they get caught late, and they generate the kind of review comments that quietly frustrate junior engineers who feel like they're failing when really they just weren't told the rules.&lt;/p&gt;

&lt;p&gt;I got tired of being part of that system. So I built something to replace the parts of it that shouldn't need me.&lt;/p&gt;




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

&lt;p&gt;OptiScan is a static analysis engine. You paste Python code in. It parses the code into a concrete syntax tree — not a string, not a token stream, an actual typed tree where every node is a specific kind of thing with specific kinds of children. Then it walks that tree and runs a set of analysis passes, each of which is looking for something specific.&lt;/p&gt;

&lt;p&gt;The output is not a suggestion. It's a rewrite.&lt;/p&gt;

&lt;p&gt;When the engine finds a nested loop doing pair comparisons — the O(n²) pattern I've left forty-seven comments about — it doesn't tell you it's bad. It replaces it. You get back working code that does the same thing in O(n) time using a HashSet for constant-time membership checks. You can copy it directly. You can run the benchmark in the browser to see the difference in milliseconds.&lt;/p&gt;

&lt;p&gt;That's the thing I wanted to get right. A linter that tells you your code is slow and then leaves you to figure out the fix is only half useful. The fix is the point.&lt;/p&gt;




&lt;h2&gt;
  
  
  Let Me Show You What I Mean
&lt;/h2&gt;

&lt;p&gt;Here's the function that ships as the default example:&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;find_pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&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;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;result&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;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

&lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&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="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;find_pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean enough. Readable. Does what it says. And for a dataset of 500 elements it probably runs fine on your laptop, which is exactly why it makes it to production.&lt;/p&gt;

&lt;p&gt;But feed it 50,000 elements and suddenly you're doing 1.25 billion comparison operations. Feed it a million elements and you've brought a service to its knees over a function that looked completely harmless in review.&lt;/p&gt;

&lt;p&gt;The AST engine parses this, detects the outer loop, detects the inner &lt;code&gt;For&lt;/code&gt; node as the only direct child of the outer loop body, confirms there's an &lt;code&gt;append&lt;/code&gt; call buried inside, identifies the collection name from the &lt;code&gt;range(len(arr))&lt;/code&gt; pattern, and rewrites:&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;arr_set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arr&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;num&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;complement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;num&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;complement&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;arr_set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&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;num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;complement&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One pass. O(1) set lookup. The benchmark in the browser — running both versions via Pyodide, a Python interpreter compiled to WebAssembly — shows this version running between 20 and 40 times faster on the default dataset. Not because I said so. Because you can run it yourself and watch the numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Security Part That Kept Surprising People
&lt;/h2&gt;

&lt;p&gt;I added a security auditor because hardcoded secrets in source code is the other comment I've left too many times. But when I started building it I ran into something that made me realise most linters are only doing half the job.&lt;/p&gt;

&lt;p&gt;The obvious case is 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;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-abc123def456&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everyone knows that's bad. Most linters catch it. But what about 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;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1223&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a real thing people do. Numeric API keys, numeric tokens, numeric "passwords" that are just IDs they didn't want to hardcode as strings for some reason. Most static analysis tools only check for string literals in sensitive variable assignments because that's the obvious case. They're checking the wrong layer — they're looking at the &lt;em&gt;value type&lt;/em&gt; they expect rather than the &lt;em&gt;variable name pattern&lt;/em&gt; combined with &lt;em&gt;any literal at all&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The OptiScan security auditor checks both. It matches the variable name against a list of sensitive keywords — &lt;code&gt;api_key&lt;/code&gt;, &lt;code&gt;token&lt;/code&gt;, &lt;code&gt;secret&lt;/code&gt;, &lt;code&gt;password&lt;/code&gt;, &lt;code&gt;credentials&lt;/code&gt;, &lt;code&gt;stripe_key&lt;/code&gt;, &lt;code&gt;twilio_token&lt;/code&gt;, twenty-odd others — and then checks whether the assigned value is &lt;em&gt;any&lt;/em&gt; literal type: string, integer, float, all of it. If your variable name looks like it should be a secret, it shouldn't be assigned a literal. Ever. In any form.&lt;/p&gt;

&lt;p&gt;It also catches &lt;code&gt;eval()&lt;/code&gt; and &lt;code&gt;exec()&lt;/code&gt; calls, &lt;code&gt;subprocess&lt;/code&gt; with &lt;code&gt;shell=True&lt;/code&gt;, AWS Access Key IDs embedded in string literals via regex, and a handful of dangerous module imports. The report comes back with line numbers and specific remediation instructions, not just a flag.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cyclomatic Complexity Is One of Those Metrics That Sounds Boring Until You See It On Your Code
&lt;/h2&gt;

&lt;p&gt;I'll be honest — I almost didn't add this. Cyclomatic complexity feels like the kind of thing you put in an enterprise tool so project managers have something to put in a quarterly report.&lt;/p&gt;

&lt;p&gt;Then I ran it on some of my own old code.&lt;/p&gt;

&lt;p&gt;The score is simple: start at 1, add 1 for every branch point. Every &lt;code&gt;if&lt;/code&gt;. Every &lt;code&gt;for&lt;/code&gt;. Every &lt;code&gt;while&lt;/code&gt;. Every &lt;code&gt;except&lt;/code&gt;. Every &lt;code&gt;and&lt;/code&gt; and &lt;code&gt;or&lt;/code&gt; in a boolean expression. Functions under 5 are LOW — easy to reason about, easy to write tests for. 6–10 is MEDIUM — manageable but worth a look. 11 and above is HIGH — that function is doing too many things and the test surface is enormous.&lt;/p&gt;

&lt;p&gt;The thing about cyclomatic complexity is that it's not really about complexity. It's about testability. A function with a score of 14 has 14 independent paths through it. That's 14 test cases minimum for full branch coverage. In practice you're probably testing 3 of them and hoping the others behave. The score makes that problem visible in a way that reading the code doesn't always do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dead Code and Why It Accumulates
&lt;/h2&gt;

&lt;p&gt;Every codebase I've worked in for more than a year has dead code in it. Functions that were replaced but not removed. Variables assigned in an early version of a feature that got refactored away. Imports for libraries that no longer get called.&lt;/p&gt;

&lt;p&gt;It accumulates because removing it feels risky and finding it is annoying. You're never quite sure if something is &lt;em&gt;actually&lt;/em&gt; unused or if you just can't see where it's used. So you leave it. Then the next engineer leaves it. Then it's been there for three years and nobody knows what it does or whether removing it will break something.&lt;/p&gt;

&lt;p&gt;The dead code detector uses libcst's scope provider — actual scope analysis, not just text search — to track which names are defined and which names are referenced in the same module. Defined but never referenced means dead. It surfaces unused functions with their names, unused variable assignments, and unreachable code after return statements. Not heuristics. Not grep. Scope analysis.&lt;/p&gt;




&lt;h2&gt;
  
  
  On Building This Alone
&lt;/h2&gt;

&lt;p&gt;I want to say something about the solo build experience because I think it's relevant to the tool itself.&lt;/p&gt;

&lt;p&gt;When you're building something without a team, you have to be extremely honest about where the value actually is. You can't ship six half-finished features and call it a product. You have to pick the thing that is genuinely useful and make that thing work properly.&lt;/p&gt;

&lt;p&gt;For me that meant the analysis had to be deterministic. Not "pretty good most of the time." Deterministic. If the security auditor says there's a hardcoded secret, there's a hardcoded secret. If the complexity engine says this is O(n²), it's O(n²). If the rewriter produces code, that code has to actually run and be correct.&lt;/p&gt;

&lt;p&gt;That constraint forced some decisions that made the engine better. Using libcst instead of a language model for analysis. Testing the transformer output before marking anything as successfully converted — the bug where my transformer was silently deleting variable declarations because it marked a conversion as successful before checking if the conversion actually worked taught me that lesson very directly.&lt;/p&gt;

&lt;p&gt;The engine is real. It has rough edges I'm filing down. But what it does, it does correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where It Goes From Here
&lt;/h2&gt;

&lt;p&gt;GitHub PR integration is the one I'm working toward most urgently. Paste a pull request URL, get back an analysis of every Python file changed in the diff. That's the version that fits into a real engineering workflow without anyone having to change how they work.&lt;/p&gt;

&lt;p&gt;After that: more language support, team workspaces so organisations can share scan history and audit reports, and a webhook mode so you can trigger scans automatically on push and route results to wherever your team already lives.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://optirefine.qzz.io" rel="noopener noreferrer"&gt;optirefine.qzz.io&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Free tier available. No credit card. Paste something you've been staring at for too long and see what comes back.&lt;/p&gt;

&lt;p&gt;If the engine misses something it should catch, or produces a rewrite that's wrong, I want to know. The best way to improve static analysis tooling is to throw real-world code at it — the messy, inconsistent, underdocumented code that actually exists in production, not the clean examples from tutorials.&lt;/p&gt;

&lt;p&gt;That's the code that matters. That's what it's built for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Solo-built. Actively maintained. Feedback welcome.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>python</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
