<?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: Devanshu Biswas</title>
    <description>The latest articles on DEV Community by Devanshu Biswas (@dev48v).</description>
    <link>https://dev.to/dev48v</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%2F3929385%2F75a3696c-143d-4252-ba59-6ed4083ca827.jpg</url>
      <title>DEV Community: Devanshu Biswas</title>
      <link>https://dev.to/dev48v</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dev48v"/>
    <language>en</language>
    <item>
      <title>A Search Scope Is a Read Scope: 751 Yes/No Questions Recover Every "Private" Field in My Personal API</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:31:24 +0000</pubDate>
      <link>https://dev.to/dev48v/a-search-scope-is-a-read-scope-751-yesno-questions-recover-every-private-field-in-my-personal-4dbe</link>
      <guid>https://dev.to/dev48v/a-search-scope-is-a-read-scope-751-yesno-questions-recover-every-private-field-in-my-personal-4dbe</guid>
      <description>&lt;p&gt;You build one local API over your notes, contacts and calendar. Five endpoints, each behind its own scope: &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;search&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;aggregate&lt;/code&gt;. You hand an agent a token with &lt;code&gt;notes:search&lt;/code&gt; and nothing else. The body is withheld on every endpoint that returns a document.&lt;/p&gt;

&lt;p&gt;That token recovers &lt;strong&gt;all 14 private bodies, character for character&lt;/strong&gt;, in a mean of &lt;strong&gt;751 yes/no questions&lt;/strong&gt; each.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;PUBLIC, MIT, &lt;code&gt;dependencies = []&lt;/code&gt;:&lt;/strong&gt; &lt;a href="https://github.com/dev48v/personal-api" rel="noopener noreferrer"&gt;https://github.com/dev48v/personal-api&lt;/a&gt;&lt;br&gt;
👉 &lt;strong&gt;Live measurement in your browser:&lt;/strong&gt; &lt;a href="https://dev48v.infy.uk/agentlab/vol4-04-personal-api.html" rel="noopener noreferrer"&gt;https://dev48v.infy.uk/agentlab/vol4-04-personal-api.html&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; personal_api            &lt;span class="c"&gt;# what each scope is actually worth&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; personal_api &lt;span class="nt"&gt;--oracles&lt;/span&gt;  &lt;span class="c"&gt;# the five oracles, side by side&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; personal_api &lt;span class="nt"&gt;--demo&lt;/span&gt;     &lt;span class="c"&gt;# watch one note come back out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What each scope promises, and what it delivers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;token scope&lt;/th&gt;
&lt;th&gt;can it read a body?&lt;/th&gt;
&lt;th&gt;bodies recovered anyway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;read everything&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;14/14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;titles only&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;titles + search&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14/14 exact&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;titles + count&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;3/14 exact, &lt;strong&gt;14 whole bodies&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;titles + aggregate (k floor)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/14&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;search only&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;14/14 exact&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second column is what the scope model promises. The third is the measurement.&lt;/p&gt;

&lt;p&gt;The only preset that holds is &lt;code&gt;titles only&lt;/code&gt; — and it holds because it has no query endpoint at all. A personal API you cannot ask questions of is not the thing anybody was trying to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack is not clever
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
&lt;span class="k"&gt;while&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;known&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX&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;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ALPHABET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="c1"&gt;# 37 symbols
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask whether the body starts with &lt;code&gt;a&lt;/code&gt;, then &lt;code&gt;b&lt;/code&gt;, then &lt;code&gt;c&lt;/code&gt;. When one says yes, keep it and ask for the next character. At most 37 questions per character, no backtracking. It is what an agent with a token and a &lt;code&gt;while&lt;/code&gt; loop does on its own, and it is linear in the length.&lt;/p&gt;

&lt;p&gt;The corpus is &lt;strong&gt;invented&lt;/strong&gt; and written out in one short file before any number is computed, so you can check the counts rather than take them. &lt;code&gt;test_alphabet_covers_corpus&lt;/code&gt; holds the alphabet to the corpus — a body containing a symbol outside it would stall the attack and understate every figure here. (That bug was real: the first version omitted digits 1–9 and reported 1/14 instead of 14/14.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoints ranked by what they are worth to an attacker
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;oracle&lt;/th&gt;
&lt;th&gt;exact&lt;/th&gt;
&lt;th&gt;any whole body&lt;/th&gt;
&lt;th&gt;fragment recovered&lt;/th&gt;
&lt;th&gt;mean calls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;search, prefix&lt;/strong&gt; (autocomplete)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14/14&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;14/14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100.0%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;751&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;search, contains (a search box)&lt;/td&gt;
&lt;td&gt;1/14&lt;/td&gt;
&lt;td&gt;1/14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;53.5%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;count, prefix&lt;/td&gt;
&lt;td&gt;3/14&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;14/14&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;23.1%&lt;/td&gt;
&lt;td&gt;706&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aggregate, k=2&lt;/td&gt;
&lt;td&gt;0/14&lt;/td&gt;
&lt;td&gt;0/14&lt;/td&gt;
&lt;td&gt;2.8%&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aggregate, k=3&lt;/td&gt;
&lt;td&gt;0/14&lt;/td&gt;
&lt;td&gt;0/14&lt;/td&gt;
&lt;td&gt;0.7%&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;An autocomplete is worth far more than a search box.&lt;/strong&gt; A prefix oracle has an anchor — the start of the string — so extension never backtracks and recovery is exact. A contains oracle has to find an anchor first and can only extend rightward, so it returns a &lt;em&gt;fragment&lt;/em&gt;: 1 exact body in 14, but &lt;strong&gt;53.5% of each body on average&lt;/strong&gt;, contiguous. Half of a note about a bank account is not half a secret.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Counting is a read too, just aimed less well.&lt;/strong&gt; &lt;code&gt;count &amp;gt; 0&lt;/code&gt; is the same boolean, so the same loop runs — but the count is over the whole collection, so greedy extension follows whichever record branches first. It recovers the &lt;em&gt;targeted&lt;/em&gt; body 3 times in 14 and a complete body of &lt;em&gt;some&lt;/em&gt; record &lt;strong&gt;all 14 times&lt;/strong&gt;. The scope did not stop the leak. It randomised the victim.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one defence that works, and why that is a caveat
&lt;/h2&gt;

&lt;p&gt;With a k-anonymity floor the channel closes: &lt;strong&gt;0 exact, 0 whole bodies&lt;/strong&gt;, and 2.8% of a body at k=2 — about one character. That is a real defence and it is the recommendation this build actually supports.&lt;/p&gt;

&lt;p&gt;But it works &lt;strong&gt;because every body in this corpus is unique&lt;/strong&gt;, so any prefix long enough to be informative matches fewer than k records and gets suppressed. A floor does not protect a value that k people share. The suite says so out loud in &lt;code&gt;test_the_k_floor_works_because_the_secrets_are_unique&lt;/code&gt;, so the caveat cannot quietly come unstuck from the claim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limiting is arithmetic
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;limit&lt;/th&gt;
&lt;th&gt;one body&lt;/th&gt;
&lt;th&gt;all 14&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;60 req/min&lt;/td&gt;
&lt;td&gt;12.5 min&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2 h 55 m&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;600 req/min&lt;/td&gt;
&lt;td&gt;1.3 min&lt;/td&gt;
&lt;td&gt;17.5 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6000 req/min&lt;/td&gt;
&lt;td&gt;7.5 s&lt;/td&gt;
&lt;td&gt;1.8 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At the tightest limit anyone actually ships, the whole corpus comes out &lt;strong&gt;inside one night&lt;/strong&gt;. Rate limiting converts "instant" into "overnight", which is not a defence against something that runs overnight.&lt;/p&gt;

&lt;p&gt;The control that &lt;em&gt;would&lt;/em&gt; help is the one I did not model: &lt;strong&gt;968 near-identical prefix queries against one field is a shape&lt;/strong&gt;, and an audit log can alarm on a shape. That is the honest recommendation out of this build, and it is not a scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this is not
&lt;/h2&gt;

&lt;p&gt;Small invented corpus; the query counts scale with its strings. In-process API — no HTTP layer, no token expiry, no audit log. Nothing models partial field exposure, redaction, differential privacy with real noise, or an LLM in the loop deciding what to answer. Both attacks are the naive ones &lt;strong&gt;on purpose&lt;/strong&gt;: they establish a &lt;em&gt;floor&lt;/em&gt; on what is recoverable, not a ceiling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;25 pytest, most of them controls&lt;/strong&gt; — that &lt;code&gt;read&lt;/code&gt; is genuinely denied, that &lt;code&gt;list&lt;/code&gt; genuinely withholds the body, that &lt;code&gt;titles only&lt;/code&gt; genuinely recovers nothing, and that the attack never once calls a denied endpoint (the denied-call counter is asserted to stay at zero, so it is a reconstruction and not a read wearing a hat).&lt;/p&gt;

&lt;p&gt;The claim is narrow. &lt;strong&gt;An endpoint that answers questions about a field is an endpoint that returns the field&lt;/strong&gt;, and a scope model that separates them is describing an intent rather than a boundary.&lt;/p&gt;

</description>
      <category>security</category>
      <category>python</category>
      <category>api</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Your Vector Store Rejects the Embedder Change That Barely Matters and Cannot See the One That Destroys Retrieval</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:30:43 +0000</pubDate>
      <link>https://dev.to/dev48v/your-vector-store-rejects-the-embedder-change-that-barely-matters-and-cannot-see-the-one-that-3a6d</link>
      <guid>https://dev.to/dev48v/your-vector-store-rejects-the-embedder-change-that-barely-matters-and-cannot-see-the-one-that-3a6d</guid>
      <description>&lt;p&gt;The provider ships v2, or somebody changes the tokenizer. The index is still there, still returning five results, still with plausible scores.&lt;/p&gt;

&lt;p&gt;Change the &lt;strong&gt;width&lt;/strong&gt; and the vector is the wrong shape, so a typed index refuses it outright — and even with that check removed, precision@5 only falls from 0.9417 to 0.7750.&lt;/p&gt;

&lt;p&gt;Change the &lt;strong&gt;hash seed or the tokenizer&lt;/strong&gt; and the width is identical, nothing anywhere objects, and precision@5 falls to &lt;strong&gt;0.0333, 0.0917 and 0.1000&lt;/strong&gt; — against a chance baseline of &lt;strong&gt;0.0833&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Live, the whole grid computed in your browser:&lt;/strong&gt; &lt;a href="https://dev48v.infy.uk/ai/days/day79-embedding-drift.html" rel="noopener noreferrer"&gt;https://dev48v.infy.uk/ai/days/day79-embedding-drift.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing here calls an embedding API
&lt;/h2&gt;

&lt;p&gt;The embedder is a &lt;strong&gt;hashed bag-of-tokens&lt;/strong&gt; implemented in full — FNV-1a with a stated seed, the signed hashing trick, optional idf, L2 normalisation, cosine. A "version" is a concrete configuration of that embedder, so a version change here is one people actually make.&lt;/p&gt;

&lt;p&gt;The corpus is synthetic and declared: 12 topics × 8 characteristic terms, 10 documents each. Because every document's topic is known by construction, retrieval is scored for &lt;strong&gt;correctness&lt;/strong&gt;, not just for agreement with an earlier run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detectable failures are the mild ones
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;version change&lt;/th&gt;
&lt;th&gt;does a typed store catch it?&lt;/th&gt;
&lt;th&gt;precision@5 after a full re-index&lt;/th&gt;
&lt;th&gt;precision@5 if only the query moves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;same version (control)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;0.9417&lt;/td&gt;
&lt;td&gt;0.9417&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;width 128 → 256&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes — wrong width&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.9583&lt;/td&gt;
&lt;td&gt;0.7750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;width 128 → 64&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes — wrong width&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.8583&lt;/td&gt;
&lt;td&gt;0.6333&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hash seed 1 → 2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no — same width&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.9250&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0917&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-grams → 3-grams&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no — same width&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.9083&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0333&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-grams → words&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no — same width&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.8583&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.1000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;idf weighting on&lt;/td&gt;
&lt;td&gt;no — same width&lt;/td&gt;
&lt;td&gt;0.9333&lt;/td&gt;
&lt;td&gt;0.9250&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sort by damage and you get almost exactly the reverse of sorting by detectability. The 3-gram row lands at &lt;strong&gt;0.0333&lt;/strong&gt; — &lt;em&gt;below&lt;/em&gt; the 0.0833 you would get by returning five documents at random.&lt;/p&gt;

&lt;p&gt;The vectors carry no version tag. Cosine is happy to compare any two of the same length. There is no point in that path where a mismatch can be detected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The half-migrated index is the shape this actually takes
&lt;/h2&gt;

&lt;p&gt;The migration script runs on new ingests and nobody backfills. Half the index is in one space, half in another:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;half-migrated&lt;/th&gt;
&lt;th&gt;single-version&lt;/th&gt;
&lt;th&gt;difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;new documents' share of top-5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;21.67%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;52.50%&lt;/td&gt;
&lt;td&gt;−30.83 points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;precision@5&lt;/td&gt;
&lt;td&gt;0.4667&lt;/td&gt;
&lt;td&gt;0.9417&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−50.4%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mean top-1 cosine&lt;/td&gt;
&lt;td&gt;0.3992&lt;/td&gt;
&lt;td&gt;0.5689&lt;/td&gt;
&lt;td&gt;−29.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;queries returning five documents&lt;/td&gt;
&lt;td&gt;24 of 24&lt;/td&gt;
&lt;td&gt;24 of 24&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Those documents are not ranked badly. They are &lt;strong&gt;unreachable&lt;/strong&gt; — and the system returns five results every time, drawn from the half that still lines up. No error, never an empty result set.&lt;/p&gt;

&lt;h2&gt;
  
  
  The monitor you would reach for is the wrong shape too
&lt;/h2&gt;

&lt;p&gt;Without labels, the one thing you can watch is the similarity score. On a &lt;em&gt;total&lt;/em&gt; mismatch it does move: mean top-1 cosine falls from 0.5689 to about 0.23, a ~60% drop. A threshold catches that.&lt;/p&gt;

&lt;p&gt;On the half-migrated index it falls &lt;strong&gt;29.8%&lt;/strong&gt; while precision falls &lt;strong&gt;50.4%&lt;/strong&gt;. Every query still has a well-matched document somewhere in the half that lines up, so the top score stays respectable while half the corpus has quietly left the building.&lt;/p&gt;

&lt;p&gt;A threshold tuned on the loud failure will not fire on the quiet one, and the quiet one is the one that happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Even the correct migration moves your answers
&lt;/h2&gt;

&lt;p&gt;Re-embedding everything is the right fix and it works — precision comes back to 0.858–0.958, in one case slightly &lt;em&gt;above&lt;/em&gt; the original. But top-5 overlap with the old index runs from &lt;strong&gt;0.933 down to 0.725&lt;/strong&gt;: between &lt;strong&gt;6.7% and 27.5%&lt;/strong&gt; of retrieved documents are different afterwards.&lt;/p&gt;

&lt;p&gt;Nothing is broken. It is just that any prompt, cache, eval set or human sign-off pinned to the old results is now pinned to results that no longer come back — on the day you did everything right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and the honest scope
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Write the embedder version into the index and refuse to serve a query whose version does not match.&lt;/strong&gt; Every consequence above follows from the vectors not carrying one.&lt;/p&gt;

&lt;p&gt;Scope, because it changes how you read the table: the embedder is a hashed bag-of-tokens, &lt;strong&gt;not a neural model&lt;/strong&gt;. A hash-seed change produces two &lt;em&gt;unrelated&lt;/em&gt; spaces — that is the extreme case and an &lt;strong&gt;upper bound&lt;/strong&gt;, not a prediction for what v1→v2 of a trained model does. The tokenizer rows are the better analogue, and they are nearly as bad.&lt;/p&gt;

&lt;p&gt;Nothing here models &lt;strong&gt;semantic&lt;/strong&gt; drift: there is no meaning in this corpus beyond term overlap, so this says nothing about a v2 that is simply better at understanding a question. 120 documents, 24 queries, top-5, plain cosine, no chunking, no reranker, no hybrid keyword leg.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;28 in-page checks, 95 verifier assertions, 0 failures.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>embeddings</category>
      <category>programming</category>
    </item>
    <item>
      <title>MC Dropout With 10 Samples Is Further From the Truth Than Just Turning Dropout Off. Here Is the Break-Even.</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:30:01 +0000</pubDate>
      <link>https://dev.to/dev48v/mc-dropout-with-10-samples-is-further-from-the-truth-than-just-turning-dropout-off-here-is-the-2ghp</link>
      <guid>https://dev.to/dev48v/mc-dropout-with-10-samples-is-further-from-the-truth-than-just-turning-dropout-off-here-is-the-2ghp</guid>
      <description>&lt;p&gt;Dropout is a training trick. At test time you turn it off and the whole network answers. MC dropout says leave it on, run k forward passes, average them, and read the spread as uncertainty.&lt;/p&gt;

&lt;p&gt;Both of those are approximations to the same object — &lt;strong&gt;the mean of the dropout ensemble&lt;/strong&gt;. So I computed that object exactly instead of approximating it: 12 dropout units means 2¹² = &lt;strong&gt;4096 masks, every one enumerated&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Live, the full enumeration in your browser:&lt;/strong&gt; &lt;a href="https://dev48v.infy.uk/dl/day79-dropout-at-inference.html" rel="noopener noreferrer"&gt;https://dev48v.infy.uk/dl/day79-dropout-at-inference.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning dropout off does not give you the ensemble mean
&lt;/h2&gt;

&lt;p&gt;With inverted dropout, a kept unit is divided by the keep probability, so the &lt;em&gt;expected&lt;/em&gt; post-dropout activation equals the plain activation. That is exactly why "turn dropout off" is supposed to give you the ensemble mean.&lt;/p&gt;

&lt;p&gt;It does not, and the control says why:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;network&lt;/th&gt;
&lt;th&gt;largest gap&lt;/th&gt;
&lt;th&gt;typical output&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;CONTROL: gap with a linear head&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;seed 7&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.5397&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.5537&lt;/td&gt;
&lt;td&gt;2.75×10⁻¹⁴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 101&lt;/td&gt;
&lt;td&gt;0.4106&lt;/td&gt;
&lt;td&gt;0.8880&lt;/td&gt;
&lt;td&gt;2.75×10⁻¹⁴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 2027&lt;/td&gt;
&lt;td&gt;0.2956&lt;/td&gt;
&lt;td&gt;0.4613&lt;/td&gt;
&lt;td&gt;2.71×10⁻¹⁴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 31337&lt;/td&gt;
&lt;td&gt;0.8336&lt;/td&gt;
&lt;td&gt;1.2534&lt;/td&gt;
&lt;td&gt;1.19×10⁻¹³&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 90210&lt;/td&gt;
&lt;td&gt;0.2457&lt;/td&gt;
&lt;td&gt;0.3972&lt;/td&gt;
&lt;td&gt;2.51×10⁻¹⁴&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Make the layer &lt;em&gt;after&lt;/em&gt; dropout linear and the expectation passes straight through: &lt;code&gt;E[head(drop)] = head(E[drop])&lt;/code&gt;, exactly, and the gap collapses to floating-point zero in all five networks. Put the ReLU back and the gap is a third of the signal.&lt;/p&gt;

&lt;p&gt;A bug in the enumeration would not switch itself off when the ReLU does. That is what makes this a property of the architecture rather than a mistake in my code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The break-even is 13.9 passes
&lt;/h2&gt;

&lt;p&gt;The k-sample mean is unbiased, so its RMS error is &lt;code&gt;σ/√k&lt;/code&gt;. The deterministic pass has no variance but a fixed bias. Set them equal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;k* = σ² / bias²   =   13.9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the measured curve crosses exactly where it should:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;stochastic passes k&lt;/th&gt;
&lt;th&gt;measured RMS error&lt;/th&gt;
&lt;th&gt;vs dropout-off (0.2109)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.7859&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;273% further&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0.5671&lt;/td&gt;
&lt;td&gt;169% further&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0.3557&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69% further&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;0.2492&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;18% further&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;0.1391&lt;/td&gt;
&lt;td&gt;34% closer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;0.0792&lt;/td&gt;
&lt;td&gt;62% closer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;0.0240&lt;/td&gt;
&lt;td&gt;89% closer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Across five networks the break-even lands between &lt;strong&gt;5.8 and 29.8&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The point is not that 30 passes is expensive. It is that &lt;strong&gt;5 and 10 — the numbers that actually appear in code — sit on the wrong side of the line.&lt;/strong&gt; Averaging a handful of stochastic passes is a more elaborate way of being further from the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncertainty moves. The prediction does not.
&lt;/h2&gt;

&lt;p&gt;The keep probability is a training hyperparameter. Sweeping it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;keep&lt;/th&gt;
&lt;th&gt;RMS reported spread&lt;/th&gt;
&lt;th&gt;deterministic output&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.7865&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.60&lt;/td&gt;
&lt;td&gt;0.6568&lt;/td&gt;
&lt;td&gt;identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.70&lt;/td&gt;
&lt;td&gt;0.5391&lt;/td&gt;
&lt;td&gt;identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.80&lt;/td&gt;
&lt;td&gt;0.4222&lt;/td&gt;
&lt;td&gt;identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.90&lt;/td&gt;
&lt;td&gt;0.2893&lt;/td&gt;
&lt;td&gt;identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.95&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.2021&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;identical&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A &lt;strong&gt;3.89×&lt;/strong&gt; change in the reported uncertainty, and the deterministic output is &lt;strong&gt;bit-identical at all 61 inputs&lt;/strong&gt; — because that pass never sees a mask at all.&lt;/p&gt;

&lt;p&gt;Two teams who picked 0.5 and 0.9 will report different uncertainties for the same prediction from the same network, and neither is more right. The answer and the confidence attached to it come from different places: one from the weights, the other from a number chosen during training.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(This one is exact too: at keep ≠ 0.5 the masks are not equally likely, so they are weighted by binomial mass rather than sampled. Still all 4096.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I expected to find, and did not
&lt;/h2&gt;

&lt;p&gt;Going in, my hypothesis was that this spread is just a restatement of how large the activations are — that it would track output magnitude and carry nothing else.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;network&lt;/th&gt;
&lt;th&gt;r( spread , |output| )&lt;/th&gt;
&lt;th&gt;r( spread , |gap| )&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;seed 7&lt;/td&gt;
&lt;td&gt;0.7304&lt;/td&gt;
&lt;td&gt;−0.0750&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 101&lt;/td&gt;
&lt;td&gt;0.9550&lt;/td&gt;
&lt;td&gt;0.5912&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 2027&lt;/td&gt;
&lt;td&gt;0.9573&lt;/td&gt;
&lt;td&gt;−0.3257&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 31337&lt;/td&gt;
&lt;td&gt;0.7092&lt;/td&gt;
&lt;td&gt;0.8325&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;seed 90210&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−0.1885&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.5506&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In four of five it holds, at 0.71 to 0.96. In the fifth it is &lt;strong&gt;−0.19&lt;/strong&gt;. One counterexample in five is not a rounding error, so &lt;strong&gt;that claim is not established and I am not making it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The second column is the cleaner negative. The spread does not tell you where the deterministic approximation is worst: &lt;strong&gt;−0.33 to +0.83&lt;/strong&gt; across five networks — not merely weak, but with an unstable &lt;em&gt;sign&lt;/em&gt;. Whatever that number is measuring, it is not the size of the error it is standing next to.&lt;/p&gt;

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

&lt;p&gt;These networks are &lt;strong&gt;not trained&lt;/strong&gt; — the weights come deterministically from a stated seed, so nothing here says what dropout does to a fitted model. One dropout layer, scalar in and out. No label noise, so the spread is purely the ensemble's own disagreement with no aleatoric term to separate it from. And none of this evaluates MC dropout as a &lt;em&gt;calibration&lt;/em&gt; method: no labels, no reliability diagram.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;25 in-page checks, 66 verifier assertions, 0 failures.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>deeplearning</category>
      <category>machinelearning</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Your 90%-Precision Threshold Delivers 63.5% After a Prevalence Shift — With TPR and FPR Bit-Identical</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:29:20 +0000</pubDate>
      <link>https://dev.to/dev48v/your-90-precision-threshold-delivers-635-after-a-prevalence-shift-with-tpr-and-fpr-56h9</link>
      <guid>https://dev.to/dev48v/your-90-precision-threshold-delivers-635-after-a-prevalence-shift-with-tpr-and-fpr-56h9</guid>
      <description>&lt;p&gt;You tuned a decision threshold on validation to hit "precision at least 90%". You shipped it. The population's prevalence drifted from 10% to 2%.&lt;/p&gt;

&lt;p&gt;The model did not change. The scores did not change. &lt;strong&gt;TPR and FPR at that threshold are bit-identical&lt;/strong&gt; — the same reduced fractions, &lt;code&gt;22663/29409&lt;/code&gt; and &lt;code&gt;21043/499953&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Precision is now &lt;strong&gt;63.46%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Live, all 201 thresholds enumerated exactly in your browser:&lt;/strong&gt; &lt;a href="https://dev48v.infy.uk/ml/day79-threshold-transfer.html" rel="noopener noreferrer"&gt;https://dev48v.infy.uk/ml/day79-threshold-transfer.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing is sampled and no rate is a float
&lt;/h2&gt;

&lt;p&gt;The score axis is &lt;strong&gt;201 bins of declared integer weights&lt;/strong&gt;, all 201 thresholds are enumerated, and every rate is an exact rational compared by &lt;strong&gt;BigInt cross-multiplication&lt;/strong&gt; — so an argmax is never decided by rounding, and there are no confidence intervals anywhere because there is nothing to be uncertain about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The identity the whole thing rests on
&lt;/h2&gt;

&lt;p&gt;Over a virtual population of &lt;code&gt;q·WP·WN&lt;/code&gt;, every confusion cell is an exact integer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TP = p * Sp * WN         FP = (q - p) * Sn * WP
FN = p * (WP - Sp) * WN  TN = (q - p) * (WN - Sn) * WP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TPR = TP / (TP + FN)
    = (p·Sp·WN) / (p·Sp·WN + p·(WP−Sp)·WN)
    = Sp / WP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;p&lt;/code&gt; and &lt;code&gt;q&lt;/code&gt; cancel.&lt;/strong&gt; TPR does not know what the prevalence is. Neither does FPR. Both are conditioned on the class.&lt;/p&gt;

&lt;p&gt;Precision is not. Over six prevalences from 0.1% to 90% at one fixed threshold:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;prevalence&lt;/th&gt;
&lt;th&gt;TPR (reduced)&lt;/th&gt;
&lt;th&gt;FPR (reduced)&lt;/th&gt;
&lt;th&gt;precision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.1%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;22663/29409&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;21043/499953&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.0180&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;22663/29409&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;21043/499953&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.2720&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;22663/29409&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;21043/499953&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.6704&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;30%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;22663/29409&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;21043/499953&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.8870&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;22663/29409&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;21043/499953&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.9482&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;22663/29409&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;21043/499953&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.9940&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two identical columns and a fifty-fold swing. That is an algebraic identity on one side and the entire problem on the other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two of five tuning rules pick a portable threshold
&lt;/h2&gt;

&lt;p&gt;Re-running each rule on the shifted population and reading off where it lands:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;tuning rule&lt;/th&gt;
&lt;th&gt;source&lt;/th&gt;
&lt;th&gt;after 10%→2%&lt;/th&gt;
&lt;th&gt;portable?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;score ≥ 0.50&lt;/td&gt;
&lt;td&gt;bin 100&lt;/td&gt;
&lt;td&gt;bin 100&lt;/td&gt;
&lt;td&gt;n/a — it cannot move&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maximise F1&lt;/td&gt;
&lt;td&gt;bin 127&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;bin 146&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;maximise TPR − FPR (Youden)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;bin 100&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;bin 100&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;precision ≥ 0.90&lt;/td&gt;
&lt;td&gt;bin 148&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;bin 169&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;recall ≥ 0.90&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;bin 98&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;bin 98&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two that survive are &lt;strong&gt;exactly the two whose definitions mention only class-conditional rates.&lt;/strong&gt; That is not an empirical coincidence to be spot-checked; it follows from the identity above. Youden picks the same bin at five different prevalences; max-F1 picks a different bin at nearly every one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scope:&lt;/strong&gt; recall≥0.90 is also unmoved by the score shift I used here, but only because that shift moves the &lt;em&gt;negatives&lt;/em&gt;. Recall does not look at negatives. Move the positives and it moves like everything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-tuning meets the target and hollows out the model
&lt;/h2&gt;

&lt;p&gt;Re-running precision≥0.90 on each world does restore the number:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;world&lt;/th&gt;
&lt;th&gt;re-tuned threshold&lt;/th&gt;
&lt;th&gt;precision&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;recall&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;source&lt;/td&gt;
&lt;td&gt;bin 148&lt;/td&gt;
&lt;td&gt;0.9044&lt;/td&gt;
&lt;td&gt;0.4955&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;prevalence shift&lt;/td&gt;
&lt;td&gt;bin 169&lt;/td&gt;
&lt;td&gt;0.9088&lt;/td&gt;
&lt;td&gt;0.2412&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;score shift&lt;/td&gt;
&lt;td&gt;bin 166&lt;/td&gt;
&lt;td&gt;0.9041&lt;/td&gt;
&lt;td&gt;0.2773&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;both&lt;/td&gt;
&lt;td&gt;bin 193&lt;/td&gt;
&lt;td&gt;0.9022&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0190&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In the last world the threshold that satisfies the contract fires on &lt;strong&gt;1.9% of the positives&lt;/strong&gt;. A precision target is a promise about the answers you give, and it can always be kept by giving fewer. A monitor watching only precision reports a healthy system right up to the point where the system is not answering.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part with no good answer
&lt;/h2&gt;

&lt;p&gt;In production you usually have no labels. The one thing you can watch is how often the model says yes.&lt;/p&gt;

&lt;p&gt;I constructed two worlds with the &lt;strong&gt;exactly equal&lt;/strong&gt; positive rate — not close, the identical rational &lt;code&gt;780643/49995300&lt;/code&gt; ≈ 1.5614%. The prevalence for the second was &lt;em&gt;solved&lt;/em&gt; from &lt;code&gt;rate = π·TPR + (1−π)·FPR&lt;/code&gt;, which has an exact rational answer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;world&lt;/th&gt;
&lt;th&gt;positive rate&lt;/th&gt;
&lt;th&gt;precision&lt;/th&gt;
&lt;th&gt;recall&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;prevalence shift, 10% → 2%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;780643/49995300&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.6346&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.4955&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;score shift, prevalence 0.0702%&lt;/td&gt;
&lt;td&gt;&lt;code&gt;780643/49995300&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0223&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.4955&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same firing rate. Same recall. A &lt;strong&gt;28.5×&lt;/strong&gt; difference in precision. Every dashboard that watches the positive rate shows a flat line across both.&lt;/p&gt;

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

&lt;p&gt;One score axis in 201 bins, one shift direction (negatives up 12 bins, positives never move), one pair of class-conditional shapes, five tuning rules. &lt;strong&gt;There is no cost matrix&lt;/strong&gt;, so "which threshold is right" is never answered here — only which one is &lt;em&gt;stable&lt;/em&gt;. No calibration model and no recalibration method: the point is what breaks, not how to fix it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;21 in-page checks, 72 verifier assertions, 0 failures.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Implemented npm, Cargo, Composer and pip Semver. Only 2 of 13 Ranges Mean the Same Thing in All Four.</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:28:39 +0000</pubDate>
      <link>https://dev.to/dev48v/i-implemented-npm-cargo-composer-and-pip-semver-only-2-of-13-ranges-mean-the-same-thing-in-all-3ah1</link>
      <guid>https://dev.to/dev48v/i-implemented-npm-cargo-composer-and-pip-semver-only-2-of-13-ranges-mean-the-same-thing-in-all-3ah1</guid>
      <description>&lt;p&gt;&lt;code&gt;^1.2.3&lt;/code&gt;, &lt;code&gt;~1.2&lt;/code&gt;, &lt;code&gt;1.2.x&lt;/code&gt;, &lt;code&gt;&amp;gt;=1.2 &amp;lt;2&lt;/code&gt;. The same strings go into four package managers. They do not agree about what those strings select.&lt;/p&gt;

&lt;p&gt;I implemented all four &lt;strong&gt;from their own documented rules over one shared version parser and comparator&lt;/strong&gt;, so a disagreement in the grid is a disagreement in the &lt;em&gt;rules&lt;/em&gt; rather than four parsers diverging by accident.&lt;/p&gt;

&lt;p&gt;13 ranges × 19 versions = an enumerated &lt;strong&gt;247-cell grid&lt;/strong&gt;. Every figure below is a count.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Live, the whole grid computed in your browser:&lt;/strong&gt; &lt;a href="https://dev48v.infy.uk/solve/day79-semver-resolver.html" rel="noopener noreferrer"&gt;https://dev48v.infy.uk/solve/day79-semver-resolver.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Only the longhand ranges are portable
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;range&lt;/th&gt;
&lt;th&gt;npm&lt;/th&gt;
&lt;th&gt;Cargo&lt;/th&gt;
&lt;th&gt;Composer&lt;/th&gt;
&lt;th&gt;pip&lt;/th&gt;
&lt;th&gt;all four agree?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;^1.2.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;^0.2.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;^0.0.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;^1.0.0-alpha&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~1.2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~1.2.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1.2.x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1.x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;&amp;gt;=1.2 &amp;lt;2&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;YES&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;&amp;gt;=1.0.0&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;YES&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1.2.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~=1.2.3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two that survive are the two with no shorthand in them. Every caret, every tilde, every &lt;code&gt;.x&lt;/code&gt;, and a bare version number is read differently by at least one of the four.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you want a range to mean the same thing to everybody, you have to write the inequality out.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The agreement number is a trap
&lt;/h2&gt;

&lt;p&gt;Across the whole grid the four give the same answer on &lt;strong&gt;195 of 247 cells — 78.95%&lt;/strong&gt;. That sounds like broad agreement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;178 of those 195 are cells where all four say &lt;em&gt;no&lt;/em&gt;.&lt;/strong&gt; Only &lt;strong&gt;17 cells&lt;/strong&gt; in the entire grid are ones where every ecosystem agrees to &lt;em&gt;select&lt;/em&gt; the version.&lt;/p&gt;

&lt;p&gt;Agreeing to reject 2.1.0 for &lt;code&gt;^0.0.3&lt;/code&gt; is not agreement worth having.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each pair comes apart
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;cells that differ&lt;/th&gt;
&lt;th&gt;what causes it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;td&gt;Composer&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 / 247&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;nothing, on these thirteen forms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;td&gt;Cargo&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;a bare version is a caret range in Cargo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cargo&lt;/td&gt;
&lt;td&gt;Composer&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm&lt;/td&gt;
&lt;td&gt;pip&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;pip has no caret and no &lt;code&gt;.x&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Composer&lt;/td&gt;
&lt;td&gt;pip&lt;/td&gt;
&lt;td&gt;49&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cargo&lt;/td&gt;
&lt;td&gt;pip&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;td&gt;same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;npm and Composer agree on every cell &lt;strong&gt;of this grid&lt;/strong&gt; — which is the honest scope, not a claim they are the same in general.&lt;/p&gt;

&lt;p&gt;Cargo's five split across two rules, both of which are documented and both of which surprise people:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A bare &lt;code&gt;1.2.3&lt;/code&gt; in &lt;code&gt;Cargo.toml&lt;/code&gt; is a caret range&lt;/strong&gt;, meaning &lt;code&gt;&amp;gt;=1.2.3 &amp;lt;2.0.0&lt;/code&gt;. npm and Composer read the same string as an exact pin. Three cells.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cargo's &lt;code&gt;~1.2&lt;/code&gt; pins the minor&lt;/strong&gt; (&lt;code&gt;&amp;gt;=1.2.0 &amp;lt;1.3.0&lt;/code&gt;) where Composer's &lt;code&gt;~1.2&lt;/code&gt; pins only the major (&lt;code&gt;&amp;gt;=1.2.0 &amp;lt;2.0.0&lt;/code&gt;). Two cells.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The prerelease rule catches people in both directions
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^1.2.3          does NOT select 2.0.0-rc.1   in any of the four
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though &lt;code&gt;2.0.0-rc.1&lt;/code&gt; sorts &lt;strong&gt;strictly below&lt;/strong&gt; &lt;code&gt;2.0.0&lt;/code&gt;, which is the range's upper bound. The rule is not about ordering: a prerelease is excluded unless the range itself names one.&lt;/p&gt;

&lt;p&gt;And when the range does name one, the door opens wider than expected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^1.0.0-alpha    selects 1.0.0-alpha.1, 1.0.0-beta, 1.0.0, 1.2.0, …   (11 of 19)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;1.0.0-beta&lt;/code&gt; is a prerelease the range never mentions, and &lt;code&gt;1.2.0&lt;/code&gt; is an ordinary release well past it. The opt-in is for the &lt;strong&gt;version tuple&lt;/strong&gt; you named, not for prereleases in general.&lt;/p&gt;

&lt;h2&gt;
  
  
  One bug the verifier caught
&lt;/h2&gt;

&lt;p&gt;My first &lt;code&gt;&amp;gt;=1.2 &amp;lt;2&lt;/code&gt; implementation silently lost its ceiling. &lt;code&gt;parse("2")&lt;/code&gt; returns null — a bound may be written with one or two components, and only three-component strings are legal &lt;em&gt;versions&lt;/em&gt;. So the upper bound became &lt;code&gt;null&lt;/code&gt;, &lt;code&gt;!hi&lt;/code&gt; was true, and the range happily selected &lt;code&gt;2.0.0&lt;/code&gt; and &lt;code&gt;2.1.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;gt;=1.2 &amp;lt;2&lt;/code&gt; was reporting 8 selections instead of 6, and it looked entirely reasonable in the output. Pad partial bounds before parsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this covers and what it does not
&lt;/h2&gt;

&lt;p&gt;The four resolvers implement the forms that produce the disagreements above: caret with its zero-major special cases, tilde with its component-count dependence, &lt;code&gt;.x&lt;/code&gt; wildcards, explicit inequalities, bare versions, and pip's &lt;code&gt;~=&lt;/code&gt;. They are &lt;strong&gt;not complete reimplementations&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Out of scope, each of which is another way for two tools to differ: compound ranges with &lt;code&gt;||&lt;/code&gt;, npm's &lt;code&gt;--include=prerelease&lt;/code&gt;, Cargo's &lt;code&gt;--precise&lt;/code&gt;, Composer's stability flags, pip's environment markers, and every resolver's behaviour when nothing satisfies the range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;21 in-page checks, 86 verifier assertions, 0 failures.&lt;/strong&gt; The comparator is checked against the semver 2.0.0 spec's own example ordering before anything else is measured.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>npm</category>
      <category>rust</category>
      <category>programming</category>
    </item>
    <item>
      <title>I Built a Rig to Show That an Agent's Own Notes Poison Its Context. The Effect Was Not There.</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:27:58 +0000</pubDate>
      <link>https://dev.to/dev48v/i-built-a-rig-to-show-that-an-agents-own-notes-poison-its-context-the-effect-was-not-there-1b93</link>
      <guid>https://dev.to/dev48v/i-built-a-rig-to-show-that-an-agents-own-notes-poison-its-context-the-effect-was-not-there-1b93</guid>
      <description>&lt;p&gt;The worry is stated everywhere: an agent reads a document, writes a note about it into its own context, reads that note again next turn — and once a wrong note is in there, everything downstream is contaminated.&lt;/p&gt;

&lt;p&gt;By turn 60, &lt;strong&gt;91.3%&lt;/strong&gt; of the claims the agent reads are its own earlier notes. On &lt;strong&gt;5–8%&lt;/strong&gt; of decisions it goes with a note &lt;em&gt;against every source it can see that turn&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;And keeping every note versus keeping none moves accuracy by between &lt;strong&gt;−0.39 and +1.34 percentage points&lt;/strong&gt;. The sign is usually positive.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Live, 400 enumerated runs per policy in your browser:&lt;/strong&gt; &lt;a href="https://dev48v.infy.uk/prompt/day79-context-poisoning.html" rel="noopener noreferrer"&gt;https://dev48v.infy.uk/prompt/day79-context-poisoning.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing here simulates language
&lt;/h2&gt;

&lt;p&gt;No model, nothing sampled from a distribution I cannot inspect. The engine simulates the &lt;strong&gt;ledger&lt;/strong&gt;: 24 facts with declared truth values, twelve sources of stated reliability, a retrieval step, and five context policies over &lt;strong&gt;400 enumerated runs&lt;/strong&gt; each. Every rate is a count.&lt;/p&gt;

&lt;p&gt;That is a real limitation and I will come back to it. But it is what makes the null result checkable rather than a shrug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two things that are true
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The context really does become self-referential.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;turns&lt;/th&gt;
&lt;th&gt;share of read claims that are the agent's own notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;47.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;76.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;td&gt;86.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;91.3%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Monotone, and it goes essentially all the way. With one retrieved source per turn and a growing note pile, the model is overwhelmingly reading itself by the end. The self-override rate — decisions where the note wins against every visible source — sits at 5–8%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And it costs nothing measurable.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;bad sources (of 12)&lt;/th&gt;
&lt;th&gt;accuracy gap, keep-everything vs keep-nothing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;+0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;+1.34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;+0.71&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;−0.39&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;+0.92&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;+0.16&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Not "small". Inside the noise of the thing itself, in both directions, across every setting I tried.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the null is real and not a broken harness
&lt;/h2&gt;

&lt;p&gt;A null result is the easiest finding to produce by accident, so the controls matter more than the finding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;With zero unreliable sources, every policy is exactly 100% correct.&lt;/strong&gt; The pipeline works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;With nine unreliable sources of twelve, every policy drops below 35%.&lt;/strong&gt; The metric moves — by more than 60 points — when something real changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All five policies answer the same number of questions&lt;/strong&gt; (4.415 of 6 per run), so the comparison is like-for-like rather than one policy quietly declining to answer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The measurement can detect a difference. There is not one to detect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reason, which is the interesting part
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A note inherits the accuracy of the process that wrote it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent writes notes using the same judgement it would use to answer directly. So re-reading a note is neither better nor worse than re-deriving the same conclusion from the same sources. The note is not an independent second opinion, and it is not a corruption — it is a &lt;strong&gt;cache of the agent's own reasoning&lt;/strong&gt;, and a cache does not change the function it caches.&lt;/p&gt;

&lt;p&gt;Compounding needs an amplifier. Something has to make a wrong note &lt;em&gt;more&lt;/em&gt; likely to be believed than the wrong reasoning that produced it. In this world nothing does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would make it compound, and is not modelled here
&lt;/h2&gt;

&lt;p&gt;This is where the finding stops. The failure I did not model is the one that happens &lt;strong&gt;outside the loop&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a note that gets &lt;strong&gt;shared&lt;/strong&gt; with another agent, or another session, where it arrives without the uncertainty that produced it&lt;/li&gt;
&lt;li&gt;a note that is &lt;strong&gt;written in more confident language&lt;/strong&gt; than the evidence supported, so it reads as settled next turn&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compaction or summarisation&lt;/strong&gt;, which drops the hedge and keeps the claim&lt;/li&gt;
&lt;li&gt;a note that becomes a &lt;strong&gt;retrieval target&lt;/strong&gt;, so it competes with sources rather than sitting beside them&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is an amplifier. None of them is in this engine. If context poisoning is real — and I would still bet it is — the mechanism is likely one of those, not the plain re-reading loop that gets described.&lt;/p&gt;

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

&lt;p&gt;Boolean facts with declared truth values, not natural language. No model, so no sycophancy, no instruction-following drift, no attention effects. Sources are reliable or unreliable with nothing in between. And 4.415 of 6 questions get answered at all, which is a property of the retrieval budget rather than of the policies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;16 in-page checks, 62 verifier assertions, 0 failures.&lt;/strong&gt; I published the null.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Optimistic-UI Rollback Everyone Writes Is Exactly Right for a Toggle and the Worst Option for a Set</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:27:17 +0000</pubDate>
      <link>https://dev.to/dev48v/the-optimistic-ui-rollback-everyone-writes-is-exactly-right-for-a-toggle-and-the-worst-option-for-a-4l97</link>
      <guid>https://dev.to/dev48v/the-optimistic-ui-rollback-everyone-writes-is-exactly-right-for-a-toggle-and-the-worst-option-for-a-4l97</guid>
      <description>&lt;p&gt;You tap the heart. It fills instantly. The request goes out behind it. If the server disagrees, you roll back.&lt;/p&gt;

&lt;p&gt;That rollback — &lt;em&gt;undo the toggle when the request fails&lt;/em&gt; — agrees with the server on &lt;strong&gt;600 of 600&lt;/strong&gt; sessions when the control is a like button.&lt;/p&gt;

&lt;p&gt;On a set-a-value control, the identical policy agrees on &lt;strong&gt;219 of 600&lt;/strong&gt;. Which is worse than not rolling back at all (&lt;strong&gt;411&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Live, 600 enumerated sessions computed in your browser:&lt;/strong&gt; &lt;a href="https://dev48v.infy.uk/design/day79-optimistic-ui.html" rel="noopener noreferrer"&gt;https://dev48v.infy.uk/design/day79-optimistic-ui.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The engine simulates the queue, not a browser
&lt;/h2&gt;

&lt;p&gt;There is no DOM here and nothing is sampled. The model is a sequence of taps, a network that reorders and fails, a server with its own truth, and five reconciliation policies run over &lt;strong&gt;600 enumerated sessions&lt;/strong&gt; each. Every figure below is a count, not an estimate.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;policy&lt;/th&gt;
&lt;th&gt;toggle&lt;/th&gt;
&lt;th&gt;set&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;no rollback at all&lt;/td&gt;
&lt;td&gt;411 / 600&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;411 / 600&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;undo the toggle on failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;600 / 600&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;219 / 600&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;restore the pre-tap value&lt;/td&gt;
&lt;td&gt;396 / 600&lt;/td&gt;
&lt;td&gt;357 / 600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;last-write-wins from the server&lt;/td&gt;
&lt;td&gt;600 / 600&lt;/td&gt;
&lt;td&gt;600 / 600&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;refetch on failure&lt;/td&gt;
&lt;td&gt;600 / 600&lt;/td&gt;
&lt;td&gt;600 / 600&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The policy did not change. The operation did.
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;toggle&lt;/code&gt; and &lt;code&gt;set&lt;/code&gt; are the same shape at the call site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;applyTo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&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="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;op&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;set&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line, two operations, and the rollback that inverts a toggle is &lt;em&gt;its own inverse&lt;/em&gt; — apply it twice and you are back where you started. That is why it is perfect on a like button. It is a group operation with an inverse, and the inverse is the operation itself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;set&lt;/code&gt; has no such property. Undoing a &lt;code&gt;set&lt;/code&gt; requires knowing what the value was &lt;strong&gt;before&lt;/strong&gt;, and by the time a failure comes back, later taps have already moved the item. So the "undo" writes a value that was never correct at any point in the timeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Nothing in the UI code knows which one it is holding.&lt;/strong&gt; The rollback function has the same signature either way, passes review either way, and is catastrophically wrong in one of the two cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  The careful-looking alternative is worse at both
&lt;/h2&gt;

&lt;p&gt;The obvious fix is to capture the previous value at tap time and restore &lt;em&gt;that&lt;/em&gt; on failure. It looks more correct. It is more wrong:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;396&lt;/strong&gt; on a toggle and &lt;strong&gt;357&lt;/strong&gt; on a set — mediocre at both, and it &lt;em&gt;loses to the naive toggle-undo on the toggle case&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Same reason. It restores a value that was accurate when the tap was made and is stale when the failure lands. Being careful about the wrong instant does not help.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that should worry you
&lt;/h2&gt;

&lt;p&gt;I wrote every property test I could think of that does not require a live server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the optimistic state converges once the queue drains&lt;/li&gt;
&lt;li&gt;no tap is lost&lt;/li&gt;
&lt;li&gt;the state is always one of the values the user actually chose&lt;/li&gt;
&lt;li&gt;rolling back an untouched item is a no-op&lt;/li&gt;
&lt;li&gt;applying the same failure twice is idempotent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Every optimistic policy passes every one of them.&lt;/strong&gt; Including the one that is right 36% of the time.&lt;/p&gt;

&lt;p&gt;That is not a gap in my imagination. It is structural: all of those properties are about the &lt;em&gt;queue&lt;/em&gt;, and the difference between the policies is about the &lt;em&gt;server's truth&lt;/em&gt;, which is exactly the thing a test without a server does not have. A suite like that will stay green through this bug forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  One honest result that reversed on me
&lt;/h2&gt;

&lt;p&gt;I expected "restore the previous value" to at least beat "no rollback" on the set case, since it is strictly more information. It does — 357 against 411 is the &lt;em&gt;wrong&lt;/em&gt; direction, so it does not.&lt;/p&gt;

&lt;p&gt;Reordering alone breaks it, without any failures at all. Two &lt;code&gt;set&lt;/code&gt; operations do not commute, so a queue that delivers them out of order lands on the wrong value even when both succeed. I had asserted "same on a set with zero failures" while building this and it is false. That turned into a stronger finding than the one I was looking for.&lt;/p&gt;

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

&lt;p&gt;One item at a time, no batching, no conflict resolution beyond these five policies, no CRDTs, no offline queue that survives a reload. The network model reorders and drops but never duplicates. And "agrees with the server" is the only metric — nothing here measures how the UI &lt;em&gt;feels&lt;/em&gt;, which is the reason optimistic UI exists in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;18 in-page checks, 69 verifier assertions, 0 failures.&lt;/strong&gt; Pure vanilla JS, one file, no build step.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ux</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Whatever Length-3 Coin Pattern You Pick, I Have a Reply That Beats It — and My Worst Case Is 2/3</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:26:37 +0000</pubDate>
      <link>https://dev.to/dev48v/whatever-length-3-coin-pattern-you-pick-i-have-a-reply-that-beats-it-and-my-worst-case-is-23-md3</link>
      <guid>https://dev.to/dev48v/whatever-length-3-coin-pattern-you-pick-i-have-a-reply-that-beats-it-and-my-worst-case-is-23-md3</guid>
      <description>&lt;p&gt;You pick a pattern of three coin flips. Then I pick one, &lt;strong&gt;after seeing yours&lt;/strong&gt;. We flip a fair coin until one of the two patterns appears, and whoever's pattern lands first wins.&lt;/p&gt;

&lt;p&gt;There is no pattern you can choose that leaves me under a two-to-one favourite.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;Live, every probability solved exactly in your browser:&lt;/strong&gt; &lt;a href="https://dev48v.infy.uk/game/day79-penney-ante.html" rel="noopener noreferrer"&gt;https://dev48v.infy.uk/game/day79-penney-ante.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing here is simulated and nothing is a float
&lt;/h2&gt;

&lt;p&gt;This is the part that matters more than the game. Each pair of patterns is solved as an &lt;strong&gt;absorbing Markov chain over rationals&lt;/strong&gt;, with BigInt numerator and denominator. So "I win two thirds of the time" is &lt;code&gt;2n/3n&lt;/code&gt;, not &lt;code&gt;0.6667&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And every answer is cross-checked against &lt;strong&gt;Conway's leading-numbers algorithm&lt;/strong&gt;, which shares no code with the chain — a closed form that computes the same odds by a completely different route. Two implementations that have to agree is worth considerably more than one that is asserted to be correct.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;your pick&lt;/th&gt;
&lt;th&gt;my reply&lt;/th&gt;
&lt;th&gt;my odds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HHH&lt;/td&gt;
&lt;td&gt;THH&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7/8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HHT&lt;/td&gt;
&lt;td&gt;THH&lt;/td&gt;
&lt;td&gt;3/4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTH&lt;/td&gt;
&lt;td&gt;HHT&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTT&lt;/td&gt;
&lt;td&gt;HHT&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;THH&lt;/td&gt;
&lt;td&gt;TTH&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;THT&lt;/td&gt;
&lt;td&gt;TTH&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTH&lt;/td&gt;
&lt;td&gt;HTT&lt;/td&gt;
&lt;td&gt;3/4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TTT&lt;/td&gt;
&lt;td&gt;HTT&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7/8&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eight rows, eight wins for the second player. The relation is &lt;strong&gt;non-transitive&lt;/strong&gt;, which is why "pick the best pattern" is not a thing you can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The intuition that decides it for most people is simply wrong
&lt;/h2&gt;

&lt;p&gt;Ask someone why THH beats HHT and they will reach for waiting time: &lt;em&gt;THH must show up sooner on average.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HHT, HTT, THH and TTH all have a mean waiting time of exactly 8 flips.&lt;/strong&gt; Identical. And yet THH beats HHT three times in four.&lt;/p&gt;

&lt;p&gt;Which pattern appears soonest &lt;strong&gt;on its own&lt;/strong&gt; says nothing about which appears first when two are &lt;strong&gt;racing&lt;/strong&gt;. Those are different questions, and the second one is the one the game asks. The race is decided by overlap structure: when HHT is one flip from completing, the flips already on the table can be finishing THH instead.&lt;/p&gt;

&lt;p&gt;That is the whole trick, and it survives being explained. You can tell someone the strategy and still beat them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the game changes with the length, and not how I expected
&lt;/h2&gt;

&lt;p&gt;I assumed the second player's edge would grow smoothly with pattern length. It does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At length 2 there is no advantage at all.&lt;/strong&gt; The second player's best reply wins exactly &lt;strong&gt;1/2&lt;/strong&gt; — a dead-even game. Penney's ante needs length 3 to exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;At length 4 the relation becomes genuinely cyclic.&lt;/strong&gt; There are &lt;strong&gt;42 triples&lt;/strong&gt; where A beats B, B beats C, and C beats A. At length 3 there are &lt;strong&gt;none&lt;/strong&gt; — the length-3 relation, non-transitive as it is, contains no 3-cycles at all.&lt;/p&gt;

&lt;p&gt;So the game is not "the same idea, more of it" as patterns get longer. Length 2 is degenerate, length 3 is non-transitive but acyclic, and length 4 is properly cyclic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why exact rationals rather than a simulation
&lt;/h2&gt;

&lt;p&gt;A simulation of this game would have been three lines and it would have been fine — you would get 0.667 and 0.875 and move on.&lt;/p&gt;

&lt;p&gt;But then "the second player always wins" is an observation about a sample, and "42 cyclic triples at length 4" is a claim you cannot make at all, because near-tied pairs would flip sign with the seed. Cycles are exactly where a Monte Carlo answer stops being trustworthy: a 0.501 and a 0.499 are indistinguishable to a sampler and are the difference between a cycle existing and not.&lt;/p&gt;

&lt;p&gt;With rationals, &lt;code&gt;1/2&lt;/code&gt; is &lt;code&gt;1/2&lt;/code&gt;. The length-2 result is not "about half".&lt;/p&gt;

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

&lt;p&gt;Fair coins only — a biased coin changes every number here. Two players, one pair at a time; the multi-player version is a different problem. Patterns of one fixed length per game, so no HHH-versus-TT. And the length-4 enumeration is complete but the code is exponential in the pattern length, so length 6 was not run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;17 in-page checks and 224 verifier assertions&lt;/strong&gt;, including the Conway cross-check on every pair. 0 failures.&lt;/p&gt;

&lt;p&gt;Pure vanilla JS, one file, no build step, no dependencies.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>javascript</category>
      <category>probability</category>
      <category>math</category>
    </item>
    <item>
      <title>The Change Detector Everyone Writes First Alerts on 48 of 48 Fetches</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Thu, 03 Sep 2026 05:47:31 +0000</pubDate>
      <link>https://dev.to/dev48v/the-change-detector-everyone-writes-first-alerts-on-48-of-48-fetches-2d6d</link>
      <guid>https://dev.to/dev48v/the-change-detector-everyone-writes-first-alerts-on-48-of-48-fetches-2d6d</guid>
      <description>&lt;p&gt;Lookout watches the pages you care about and speaks only when something changed. Runs on your machine: no API key, no network, no model, &lt;code&gt;dependencies = []&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project 3 of 5 · Agent Lab Vol 4&lt;/strong&gt;&lt;br&gt;
Page: &lt;a href="https://dev48.infy.uk/agentlab/vol4-03-lookout.html" rel="noopener noreferrer"&gt;https://dev48.infy.uk/agentlab/vol4-03-lookout.html&lt;/a&gt;&lt;br&gt;
Repo: &lt;a href="https://github.com/dev48v/lookout" rel="noopener noreferrer"&gt;https://github.com/dev48v/lookout&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The word doing the work is &lt;em&gt;changed&lt;/em&gt;, and six reasonable definitions of it disagree on most of a corpus.&lt;/p&gt;

&lt;h2&gt;
  
  
  The corpus is invented, and the labels came first
&lt;/h2&gt;

&lt;p&gt;There is no public archive of one person's watched pages with the meaningful changes labelled — and building one out of somebody else's browsing would be unavailable &lt;em&gt;and&lt;/em&gt; a privacy problem. So &lt;strong&gt;60 snapshots across 12 sources&lt;/strong&gt; were written by hand from a fixed seed.&lt;/p&gt;

&lt;p&gt;Each carries a &lt;strong&gt;declared label&lt;/strong&gt;: would a person have wanted to be told? Made once, in the open, in &lt;code&gt;pages.py&lt;/code&gt;, &lt;strong&gt;before any detector ran&lt;/strong&gt;. That is what makes precision and recall counts rather than a judgement formed after seeing which detector won.&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious detector alerts on every single fetch
&lt;/h2&gt;

&lt;p&gt;48 consecutive fetch pairs. Four are worth telling somebody about.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;detector&lt;/th&gt;
&lt;th&gt;alerts&lt;/th&gt;
&lt;th&gt;false&lt;/th&gt;
&lt;th&gt;missed&lt;/th&gt;
&lt;th&gt;precision&lt;/th&gt;
&lt;th&gt;recall&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;hash the whole response&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.0833&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;strip the tags, hash the text&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;44&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0833&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hash the main region only&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.3333&lt;/td&gt;
&lt;td&gt;1.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;main region, numbers masked&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.4286&lt;/td&gt;
&lt;td&gt;0.7500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;the set of words&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.0000&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.7500&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;words + numbers in content&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.5000&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.0000&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hashing the response fires &lt;strong&gt;48 of 48&lt;/strong&gt; — a rotating CSRF token, a build stamp and an ad slot are all "changed". For every real change it interrupts you eleven times. A watcher you mute is a watcher you do not have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stripping the tags first changes nothing at all.&lt;/strong&gt; Same 48, same precision. The noise was never in the markup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix and the flaw are the same edit
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  boilerplate  counter  reordered  one number  the real change
raw-hash                   24        4          4           1                3
main-normalised             0        0          4           0                3
word-set                    0        0          0           0                3
word-set+numbers            0        4          0           1                3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quietest detector is &lt;strong&gt;perfectly precise&lt;/strong&gt; — three alerts, three real, zero false. It ignores a rotating token, a live view-counter and a re-ordered team list.&lt;/p&gt;

&lt;p&gt;It also misses a price going from &lt;strong&gt;$49 to $59&lt;/strong&gt;, because masking numbers is exactly what made it quiet. There is nothing in the HTML that distinguishes &lt;code&gt;Viewed 4,182 times&lt;/code&gt; from &lt;code&gt;$59&lt;/code&gt; — both are a number in the main region that used to be a different number.&lt;/p&gt;

&lt;p&gt;Put the numbers back: recall returns to 1.0000, the counter returns with it, precision falls to 0.5000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No detector on this corpus gets both.&lt;/strong&gt; Every one of the six is either noisy or blind. A change detector is a definition of "meaningful" applied by a program with no idea what the page is for — so the honest version of &lt;em&gt;speaks only when something changed&lt;/em&gt; is &lt;em&gt;speaks when this particular definition of changed says so&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What checks what
&lt;/h2&gt;

&lt;p&gt;33 pytest, several of them controls. A differential verifier at &lt;strong&gt;524 asserts&lt;/strong&gt; lifts the page's engine out of the HTML and diffs it against the real Python package — &lt;strong&gt;all 60 generated documents by SHA-256&lt;/strong&gt;, plus every score. Page self-check 18. Zero failures.&lt;/p&gt;

&lt;p&gt;Two embedding hazards it caught, both worth knowing: the engine generates a fake page containing a literal &lt;code&gt;&amp;lt;/script&amp;gt;&lt;/code&gt;, which &lt;strong&gt;terminated the tag and blanked the page&lt;/strong&gt; until the string was split — and the same corpus contains &lt;code&gt;window.__nonce=&lt;/code&gt; as &lt;em&gt;data&lt;/em&gt;, which made the verifier's "does not touch window" scan fail. That scan now blanks string literals as well as comments before looking. Check the code, not the text.&lt;/p&gt;

&lt;p&gt;MIT. Python standard library only.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>programming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Retries Take Your Agent's Success Rate to 96.95% While Exactly-Once Goes Down</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Thu, 03 Sep 2026 05:46:50 +0000</pubDate>
      <link>https://dev.to/dev48v/retries-take-your-agents-success-rate-to-9695-while-exactly-once-goes-down-4ghd</link>
      <guid>https://dev.to/dev48v/retries-take-your-agents-success-rate-to-9695-while-exactly-once-goes-down-4ghd</guid>
      <description>&lt;p&gt;The tool times out, the agent retries. Every framework does this and it is the right default for a read.&lt;/p&gt;

&lt;p&gt;Run it: &lt;a href="https://dev48.infy.uk/ai/days/day78-tool-call-retries.html" rel="noopener noreferrer"&gt;https://dev48.infy.uk/ai/days/day78-tool-call-retries.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Nothing here simulates language. The engine simulates the &lt;strong&gt;call&lt;/strong&gt; — a request that may not arrive, a server that may have applied it anyway, a reply that may not come back — over &lt;strong&gt;4,000 enumerated timelines&lt;/strong&gt; per policy. The agent sees only the reply. The world knows what happened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three failures, and only one makes a retry dangerous
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;never arrived     -&amp;gt; nothing applied,  agent sees nothing
applied, reply lost -&amp;gt; APPLIED,        agent sees nothing   &amp;lt;- this one
refused           -&amp;gt; nothing applied,  agent sees a refusal
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the agent's side the first two are indistinguishable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metric and the world move in opposite directions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;reported success&lt;/th&gt;
&lt;th&gt;applied exactly once&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;no retry&lt;/td&gt;
&lt;td&gt;81.92%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89.95%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retry up to 3&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.95%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;89.42%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The agent's own success rate improves by &lt;strong&gt;fifteen points&lt;/strong&gt; and the share of calls that applied exactly once goes &lt;strong&gt;down&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What paid for it: &lt;strong&gt;334 calls of 4,000 applied more than once&lt;/strong&gt; — and the success metric cannot see a single one. From inside the agent, a duplicated write and a clean write are the same event. The success rate is a measurement of the &lt;strong&gt;replies&lt;/strong&gt;, not of the world.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest half
&lt;/h2&gt;

&lt;p&gt;It would be wrong to stop at "retries are bad". Without them the agent reports a &lt;strong&gt;failure for a call that actually succeeded 321 times&lt;/strong&gt; in 4,000 — the write landed, the reply was lost, and whatever the agent does next is built on a false picture.&lt;/p&gt;

&lt;p&gt;Retrying cuts that to &lt;strong&gt;33&lt;/strong&gt;. Both problems are real; retries trade one for the other, and the trade is good for reads and bad for un-keyed writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix changes nothing the agent can see
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;retry 3&lt;/th&gt;
&lt;th&gt;retry 3 &lt;strong&gt;with an idempotency key&lt;/strong&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;reported success rate&lt;/td&gt;
&lt;td&gt;96.95%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.95%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tries per call&lt;/td&gt;
&lt;td&gt;1.18&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.18&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;applied exactly once&lt;/td&gt;
&lt;td&gt;89.42%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;97.78%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;applied twice or more&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;334&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An idempotency key does not prevent the retry and does not change the reply. It stops the &lt;strong&gt;second application&lt;/strong&gt;. So the reported rate is identical — the same &lt;em&gt;count&lt;/em&gt;, from the same 4,000 timelines — while duplicates go to zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Any evaluation that scores an agent on its own success rate rates those two configurations exactly equally.&lt;/strong&gt; No metric computed from replies can distinguish them, which is worth knowing before you trust one.&lt;/p&gt;

&lt;p&gt;159 verifier asserts, 15 in-page checks, 0 failures.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>distributedsystems</category>
      <category>programming</category>
    </item>
    <item>
      <title>Raising the Batch Size Makes Batch Norm's Inference Statistics 13x Worse</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Thu, 03 Sep 2026 05:46:09 +0000</pubDate>
      <link>https://dev.to/dev48v/raising-the-batch-size-makes-batch-norms-inference-statistics-13x-worse-bi</link>
      <guid>https://dev.to/dev48v/raising-the-batch-size-makes-batch-norms-inference-statistics-13x-worse-bi</guid>
      <description>&lt;p&gt;In training, batch norm normalises each row with the statistics of the batch it happens to be in. At inference there is no batch, so it uses a running average collected during training.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Those are two different functions, and the layer swaps one for the other silently.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;See it: &lt;a href="https://dev48.infy.uk/dl/day78-batch-norm-at-inference.html" rel="noopener noreferrer"&gt;https://dev48.infy.uk/dl/day78-batch-norm-at-inference.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every score is a real forward pass over an &lt;strong&gt;enumerated 512-row population&lt;/strong&gt;, so the errors are exact means rather than estimates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same input, same weights, two answers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;train mode vs eval mode&lt;/td&gt;
&lt;td&gt;RMSE &lt;strong&gt;0.3219&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;worst running-variance error&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;5.36&lt;/strong&gt; on a single feature&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Bigger batches make inference worse
&lt;/h2&gt;

&lt;p&gt;The intuition is that a larger batch gives a better estimate, so the running average should improve. The first half is true. The second does not follow — the running average is an exponential moving average over &lt;strong&gt;updates&lt;/strong&gt;, and a bigger batch means fewer of them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;batch size&lt;/th&gt;
&lt;th&gt;updates&lt;/th&gt;
&lt;th&gt;running stats vs truth&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.1036&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;0.1754&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0.4506&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.3771&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;13× worse at 256 than at 8.&lt;/strong&gt; With momentum 0.9 and two updates, the initialisation still carries weight 0.81 — the running variance never leaves its starting value of 1, on features whose real variance is nowhere near 1.&lt;/p&gt;

&lt;p&gt;Two things move in opposite directions as the batch grows, and the second wins. The page checks the first separately, so the mechanism is not assumed: &lt;strong&gt;a bigger batch really does estimate the mean better.&lt;/strong&gt; The loss is entirely in the number of updates.&lt;/p&gt;

&lt;p&gt;The knob that makes training more stable makes the thing training hands to production less accurate.&lt;/p&gt;

&lt;h2&gt;
  
  
  And the tests cannot see any of it
&lt;/h2&gt;

&lt;p&gt;Here is everything a batch-norm implementation can assert about itself without a second implementation to compare against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the training output has mean ≈ β per feature&lt;/li&gt;
&lt;li&gt;and variance ≈ γ² per feature&lt;/li&gt;
&lt;li&gt;eval mode gives the same answer twice&lt;/li&gt;
&lt;li&gt;the output has the input's shape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Checked on shuffled data and on data &lt;strong&gt;sorted by its first feature&lt;/strong&gt; — which is what you get whenever rows arrive grouped by class, by user, or by time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All four pass in both.&lt;/strong&gt; Meanwhile the train/eval divergence goes from &lt;strong&gt;0.3219 to 0.6648&lt;/strong&gt; — it doubles — because sorted batches are not exchangeable, so each batch's statistics describe its own slice and not the data. Nothing in the four properties mentions the order of the rows.&lt;/p&gt;

&lt;p&gt;60 verifier asserts, 14 in-page checks, 0 failures.&lt;/p&gt;

</description>
      <category>deeplearning</category>
      <category>machinelearning</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>Split Before You Scale Guards the Mistake That Costs 0.000168. The One That Costs 0.64 Gets a Footnote.</title>
      <dc:creator>Devanshu Biswas</dc:creator>
      <pubDate>Thu, 03 Sep 2026 05:45:29 +0000</pubDate>
      <link>https://dev.to/dev48v/split-before-you-scale-guards-the-mistake-that-costs-0000168-the-one-that-costs-064-gets-a-2ch0</link>
      <guid>https://dev.to/dev48v/split-before-you-scale-guards-the-mistake-that-costs-0000168-the-one-that-costs-064-gets-a-2ch0</guid>
      <description>&lt;p&gt;&lt;em&gt;Fit the scaler after you split&lt;/em&gt; is the first rule anybody learns about leakage. This measures what breaking it is actually worth.&lt;/p&gt;

&lt;p&gt;Run it: &lt;a href="https://dev48.infy.uk/ml/day78-preprocessing-leakage.html" rel="noopener noreferrer"&gt;https://dev48.infy.uk/ml/day78-preprocessing-leakage.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Nothing is estimated
&lt;/h2&gt;

&lt;p&gt;Closed-form ridge regression solved by Gaussian elimination, and every score is the &lt;strong&gt;exact risk over a 4,000-row held-out population&lt;/strong&gt; — not a test-set estimate with a confidence interval around it. 80 rows to work with, 1,200 columns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The famous mistake is the cheap one
&lt;/h2&gt;

&lt;p&gt;Fitting the scaler on all the data leaks the mean and standard deviation of the held-out rows into training. It is the example in every tutorial.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;true-risk difference, clean vs scaler-leaked&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;worst across six seeds&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.000168&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Under two ten-thousandths. Two means and two standard deviations, computed over a few extra rows, move the fitted model almost not at all. The rule is correct and worth keeping — the &lt;em&gt;reason&lt;/em&gt; usually given for it is not what the number says.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake that pays is choosing what to look at
&lt;/h2&gt;

&lt;p&gt;Selecting the top-k features &lt;strong&gt;using all the data&lt;/strong&gt; means the validation rows helped choose the features they are about to score. Across the same six seeds the optimism is never below &lt;strong&gt;0.6381&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is &lt;strong&gt;over 1,000×&lt;/strong&gt; the scaler's cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it buys is worse than a wrong number
&lt;/h2&gt;

&lt;p&gt;The headline population has &lt;strong&gt;no signal in it at all&lt;/strong&gt; — 1,200 columns of pure noise, nothing to learn.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;reports&lt;/th&gt;
&lt;th&gt;true risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;honest pipeline&lt;/td&gt;
&lt;td&gt;1.0152&lt;/td&gt;
&lt;td&gt;1.2327&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;leaked pipeline&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.5370&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.3184&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It reports a number that looks like a model which halved the error. There is nothing there to halve.&lt;/p&gt;

&lt;p&gt;And the leaked model's &lt;em&gt;true&lt;/em&gt; risk is &lt;strong&gt;worse&lt;/strong&gt; — 1.3184 against 1.2327. You pay twice: a flattering number, and a model that earned less than the one you would have had, because selecting on the validation rows picks columns that fit noise those rows happened to carry.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limit, stated because I expected the opposite
&lt;/h2&gt;

&lt;p&gt;I expected turning the signal up to shrink the gap. It does not — the coefficients grow with the signal count, so the target's variance grows and every error grows with it. Worse for the tidy version: &lt;strong&gt;above about four signal columns the comparison stops being stable, and at signal 6 the clean pipeline is the more optimistic of the two.&lt;/strong&gt; That is the opposite of the finding.&lt;/p&gt;

&lt;p&gt;With 80 rows and a 20-row validation split, the optimism of a single fit is noisy, and once the target carries real variance that noise swamps the leak. So the claim is about the &lt;strong&gt;low-signal regime&lt;/strong&gt; and is scoped that way on the page — &lt;em&gt;p&lt;/em&gt; large, effect small, which is exactly where feature selection gets reached for. Both ends are checked, so neither can quietly stop being true.&lt;/p&gt;

&lt;p&gt;73 verifier asserts, 16 in-page checks, 0 failures.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>python</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
