<?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: AS</title>
    <description>The latest articles on DEV Community by AS (@tonytonycoder11).</description>
    <link>https://dev.to/tonytonycoder11</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%2F4007152%2Fd6c1091e-c33c-48cd-b757-8d99daebc4b3.jpeg</url>
      <title>DEV Community: AS</title>
      <link>https://dev.to/tonytonycoder11</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tonytonycoder11"/>
    <language>en</language>
    <item>
      <title>Your quantized model got worse, and nothing told you</title>
      <dc:creator>AS</dc:creator>
      <pubDate>Sun, 02 Aug 2026 15:14:41 +0000</pubDate>
      <link>https://dev.to/tonytonycoder11/your-quantized-model-got-worse-and-nothing-told-you-22m9</link>
      <guid>https://dev.to/tonytonycoder11/your-quantized-model-got-worse-and-nothing-told-you-22m9</guid>
      <description>&lt;p&gt;Getting a model onto a phone means exporting it, and almost always quantizing it. Both of those change the numbers. Everyone knows that part.&lt;/p&gt;

&lt;p&gt;What got me is that nothing tells you when they change &lt;em&gt;too much&lt;/em&gt;. The export succeeds. The app builds. The model runs on device and hands back something that looks completely reasonable. And the thing you shipped is worse than the thing you evaluated, by some amount nobody measured, and you find out from a support ticket a month later. Or you don't find out.&lt;/p&gt;

&lt;p&gt;I went looking for the standard answer, assuming I'd just missed it. There is one, sort of. Compare your offline predictions against your online ones, and against the post-serialization ones. Every deployment guide says some version of that. Not one of them turns it into something a build can fail on. It's written up as a thing you should do, which in practice means you do it once, the week before launch, while you're doing forty other things, and then never again.&lt;/p&gt;

&lt;p&gt;There's a second gap and it fails the opposite way. Preprocessing gets written twice. Once in Python, for training. Once in Dart, for serving. Same normalization constants, same resize, same channel order, sitting in two files that nobody diffs. They agree the day you write them. Then someone changes the mean and std on the Python side six weeks later and the app just quietly starts feeding the model something it's never seen.&lt;/p&gt;

&lt;p&gt;So I built the thing I wanted. It's called &lt;a href="https://github.com/NaCode-Studios/Fluttorch" rel="noopener noreferrer"&gt;Fluttorch&lt;/a&gt;, it hit &lt;code&gt;1.0.0&lt;/code&gt; this week, and honestly the release is the least interesting part of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  One document, three readers
&lt;/h2&gt;

&lt;p&gt;The idea is small: the exporter writes a manifest next to the artifact, and nothing on the Dart side is allowed to restate what's in it.&lt;/p&gt;

&lt;p&gt;Shapes, dtypes, preprocessing constants, labels, the weight hash. Written once. Three things read them, the code generator, the parity gate and the runtime, and none of them declares its own copy. If the Dart side can't restate it, the Dart side can't disagree with training.&lt;/p&gt;

&lt;p&gt;The runtime also refuses to load an artifact whose content hash doesn't match what the manifest recorded. That sounded a bit paranoid when I wrote it. Then I re-exported and updated only half the pair, and got an artifact that satisfied every shape check and returned every number wrong, and I stopped thinking it was paranoid.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generated side
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fluttorch_gen&lt;/code&gt; is a &lt;code&gt;build_runner&lt;/code&gt; builder. It turns the manifest into Dart sitting next to it, and you commit what it generates so CI can regenerate and diff. That diff is what catches a manifest that moved without anyone rebuilding.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;classifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Classifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;artifact:&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;classifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;image:&lt;/span&gt; &lt;span class="n"&gt;ClassifierImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pixels&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;logits&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// Float32List, over the same memory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things that used to be runtime surprises are compile errors now. Passing the wrong tensor, since each one is its own extension type. Passing them in the wrong order, since &lt;code&gt;run&lt;/code&gt; takes named arguments. Reading an output that doesn't exist.&lt;/p&gt;

&lt;p&gt;None of that costs anything at run time. An extension type is just the underlying tensor once it's compiled. Wrong length still gets caught, at construction, and it tells you which tensor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TensorShapeException on "image": expected 3072 values, got 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The bit I actually care about
&lt;/h2&gt;

&lt;p&gt;At export time you hand the exporter some representative inputs and it records what the model answered. Those answers replay in your test suite, against the quantized artifact, on the real backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'the quantized model still agrees with the one we evaluated'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;goldens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;DirectoryGoldenBundle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;'build/classifier/classifier.fluttorch.json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;artifact:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'build/classifier/classifier.pte'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAsBytes&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nl"&gt;manifest:&lt;/span&gt; &lt;span class="n"&gt;goldens&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;expectParity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;goldens:&lt;/span&gt; &lt;span class="n"&gt;goldens&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tolerance comes from the recipe and the precision the manifest recorded, so an &lt;code&gt;int8-dynamic&lt;/code&gt; model isn't judged against the bound a full-precision one answers to. You can pass your own, and you probably should if your activation ranges look nothing like the ones the defaults came from.&lt;/p&gt;

&lt;p&gt;When it fails it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAIL  parity/case-3
      backend: xnnpack  quantization: int8-static
      output "load_mw"  max |Δ| 1.72  &amp;gt;  Tolerance(atol 0.1, rtol 0.1, cos ≥ 0.998)
        worst at [0]: 14.0210 vs 12.3000
        2 of 4 elements (50.0%) exceed the elementwise bound
      no layer attribution: backend "xnnpack" offers no activation taps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which tensor drifted, which bound broke, which element broke it, which backend produced the number. I put all four in because I kept hitting failures where I had three of them and was still guessing. That last line is there because per-layer attribution needs the backend to expose intermediate activations and xnnpack won't, so instead of silently giving you less, it says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I got wrong
&lt;/h2&gt;

&lt;p&gt;I measured the default tolerances instead of picking them, mostly out of stubbornness, and the measurement told me something I'd have got backwards.&lt;/p&gt;

&lt;p&gt;The two-layer model drifts up to eight times further than the convolutional one. The &lt;em&gt;simpler&lt;/em&gt; network. I stared at that for a while assuming I'd broken the harness.&lt;/p&gt;

&lt;p&gt;It isn't complexity. The two-layer model's outputs land around &lt;code&gt;9.4&lt;/code&gt;, the other ends in a softmax, and relative error is measured against the output while the rounding happened on intermediates. Big outputs absorb the same underlying error inside a much smaller relative bound. Obvious once you see it, and I did not see it for an embarrassingly long time.&lt;/p&gt;

&lt;p&gt;One bound in that table is still unmeasured and says so, because nothing in the suite exports int4 yet and I'd rather have a hole than a plausible-looking number.&lt;/p&gt;

&lt;p&gt;There's also a model that ExecuTorch happily lowers and then can't execute, identically under its own Python runtime. I left it in the suite as a failing expectation rather than skipping it, so whenever upstream fixes it, my tests are what tell me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;It doesn't train anything, convert between formats, or serve inference. It takes something you exported with &lt;code&gt;torch.export&lt;/code&gt; and makes the border between Python and Dart typed and checked, and that's the whole scope.&lt;/p&gt;

&lt;p&gt;ExecuTorch was just the first backend, not the shape of the thing. ONNX Runtime and LiteRT sit behind the same seam, and nothing above it knows which one is running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it is
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fluttorch&lt;/code&gt; and &lt;code&gt;fluttorch_gen&lt;/code&gt; are on pub.dev, Apache-2.0. The native bindings aren't published because pub.dev can't carry the native half, so you pull those from the repo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/NaCode-Studios/Fluttorch" rel="noopener noreferrer"&gt;https://github.com/NaCode-Studios/Fluttorch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a href="https://nacode-studios.github.io/Fluttorch/" rel="noopener noreferrer"&gt;https://nacode-studios.github.io/Fluttorch/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ship models to phones I'd genuinely like to know how you handle this, especially if the answer is "I don't". That's the part I'm least confident I've got right, and I'd rather hear it now than after someone builds on it.&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>machinelearning</category>
      <category>pytorch</category>
    </item>
    <item>
      <title>Kmemo 2.0 is out, and the two gaps I admitted to in the first post are closed</title>
      <dc:creator>AS</dc:creator>
      <pubDate>Sat, 01 Aug 2026 11:27:36 +0000</pubDate>
      <link>https://dev.to/tonytonycoder11/kmemo-20-is-out-and-the-two-gaps-i-admitted-to-in-the-first-post-are-closed-4hbg</link>
      <guid>https://dev.to/tonytonycoder11/kmemo-20-is-out-and-the-two-gaps-i-admitted-to-in-the-first-post-are-closed-4hbg</guid>
      <description>&lt;p&gt;This is a follow-up to &lt;a href="https://dev.to/tonytonycoder11/kmemo-a-semantic-cache-for-llm-calls-that-refuses-to-serve-you-the-wrong-answer-54h7"&gt;Kmemo: a semantic cache for LLM calls that refuses to serve you the wrong answer&lt;/a&gt;. If you have not read it, the one-line version is that &lt;code&gt;Convert 100 USD to EUR&lt;/code&gt; and &lt;code&gt;Convert 250 USD to EUR&lt;/code&gt; sit at 0.99 cosine similarity, no threshold separates them, and Kmemo reads the candidates as text instead of trusting the number.&lt;/p&gt;

&lt;p&gt;That post shipped &lt;code&gt;1.0&lt;/code&gt; and ended with two things I could not report. &lt;code&gt;2.0.0&lt;/code&gt; is out now, and both of them have an answer. One of the answers is not the one I was hoping for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gap one: I never said what a verifier catches
&lt;/h2&gt;

&lt;p&gt;The figures in the first post were 67% of near misses rejected and 88% of paraphrases kept, and I labelled them guard-only. The lexical chain runs for free; the optional &lt;code&gt;Verifier&lt;/code&gt; is a model call that sees whatever the guards let through. I described that residual as the verifier's job without ever measuring how much of it a verifier actually stops.&lt;/p&gt;

&lt;p&gt;Against a named reference implementation, &lt;code&gt;sentence_transformers.CrossEncoder&lt;/code&gt; over &lt;code&gt;cross-encoder/quora-distilroberta-base&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Corpus&lt;/th&gt;
&lt;th&gt;Residual the guards serve&lt;/th&gt;
&lt;th&gt;The verifier stops&lt;/th&gt;
&lt;th&gt;False-hit rate&lt;/th&gt;
&lt;th&gt;Rephrasings kept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;held-out&lt;/td&gt;
&lt;td&gt;50 lookups&lt;/td&gt;
&lt;td&gt;40 (80%)&lt;/td&gt;
&lt;td&gt;0.291 → &lt;strong&gt;0.058&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;0.881 → &lt;strong&gt;0.452&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;validation&lt;/td&gt;
&lt;td&gt;66 lookups&lt;/td&gt;
&lt;td&gt;51 (77%)&lt;/td&gt;
&lt;td&gt;0.324 → &lt;strong&gt;0.074&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;0.882 → &lt;strong&gt;0.686&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four fifths of what the guards miss, and a third of what they had kept. The second column is why this belongs in a table rather than a sentence, and why the verifier stays a seam you opt into rather than a default. Your verifier is not this one; what the table shows is how much of the residual is reachable at all by a model that reads the two prompts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gap two: the comparison was against a baseline I built myself
&lt;/h2&gt;

&lt;p&gt;The first post compared Kmemo to a threshold-only cache. That is the thing every "add a semantic cache" tutorial builds, so it is a fair thing to beat, but I wrote it, which makes it a weak claim.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;2.0&lt;/code&gt; compares against GPTCache on the same blind corpora, same pairs, retrieval factored out so both sides are handed the identical candidate and asked only whether to serve it.&lt;/p&gt;

&lt;p&gt;At zero cost my free lexical chain loses the headline number. GPTCache's ONNX cross-encoder serves fewer false positives than &lt;code&gt;standard()&lt;/code&gt;: 0.221 against 0.291 on one split, 0.108 against 0.324 on the other. It gets there by refusing more than half the genuine rephrasings it is shown, where my chain keeps 88%, so roughly half the cache savings is what that strictness costs.&lt;/p&gt;

&lt;p&gt;At comparable cost the picture reverses. Their cross-encoder is a transformer inference per candidate, so the row that costs what theirs costs is my guards plus a verifier, which also spends a model call:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Corpus&lt;/th&gt;
&lt;th&gt;Kmemo guards + verifier&lt;/th&gt;
&lt;th&gt;GPTCache &lt;code&gt;OnnxModelEvaluation&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;held-out&lt;/td&gt;
&lt;td&gt;false-hit &lt;strong&gt;0.058&lt;/strong&gt;, rephrasings kept 0.452&lt;/td&gt;
&lt;td&gt;false-hit 0.221, kept 0.476&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;validation&lt;/td&gt;
&lt;td&gt;false-hit &lt;strong&gt;0.074&lt;/strong&gt;, rephrasings kept 0.686&lt;/td&gt;
&lt;td&gt;false-hit 0.108, kept 0.451&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both columns are in the README, including the one I lose. A benchmark reported by the metric that flatters its author does not get believed on any of the others.&lt;/p&gt;

&lt;p&gt;One thing worth knowing if you run that comparison yourself. &lt;code&gt;OnnxModelEvaluation.evaluation&lt;/code&gt; wraps its entire body in &lt;code&gt;except Exception: return 0&lt;/code&gt;, so every internal failure comes back as a similarity of zero and looks identical to a confident refusal. A harness that trusted the documented entry point would have recorded a false-hit rate of 0.000 for GPTCache, which reads like a broken competitor and would have been entirely my own bug. The harness in the repository proves the evaluator works before it believes a single score, and the gate is mechanical rather than semantic: it asks whether the evaluator functions, never whether it agrees with me about a hard pair.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comments found something I had missed
&lt;/h2&gt;

&lt;p&gt;Someone read the first post and pointed out that every guard receives two strings, and both of them are prompts. The stored answer is right there on the entry and nothing in the match path reads it.&lt;/p&gt;

&lt;p&gt;They were right, and it is the one shape the whole chain is blind to by construction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;what is the capital gains tax rate when i sell a second home
what is the capital gains tax rate when i sell a primary residence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No number differs. No unit, no negation, no flipped comparison. Nothing either prompt says is evidence, so every guard abstains, correctly. The cached answer opens &lt;code&gt;Gain on a second home is taxable in full&lt;/code&gt;, and it goes to somebody asking about a different house.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;2.0&lt;/code&gt; adds a guard that reads it. When the two prompts differ only by a substitution and the cached answer names the word the query replaced, that answer was written for the other question.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;SemanticCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guards&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MatchGuards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;responseAware&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It refuses 14 of the 116 near-miss lookups the default chain still serves and none of the 164 rephrasing lookups, and it is still opt-in. The reason is the evidence rather than the result: those answers were written by me for this measurement, because no corpus of real paired answers exists to harvest. A semantic cache corpus records prompts, and the near misses worth catching are exactly the ones whose prompts look alike. That makes it a regression check rather than the blind measurement every other guard is held to, and folding the two together under one default would quietly downgrade the evidence behind all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What else shipped
&lt;/h2&gt;

&lt;p&gt;The core is no longer JVM-only. &lt;code&gt;kmemo-core&lt;/code&gt; and &lt;code&gt;InMemoryStore&lt;/code&gt; compile for the JVM, iOS, macOS, Linux, Windows, JS and WasmJS, at the cost of no new dependency, since &lt;code&gt;kotlin.time.Instant&lt;/code&gt; and &lt;code&gt;kotlin.time.Clock&lt;/code&gt; went stable in Kotlin 2.4. The Redis, Postgres and Spring adapters stay JVM-only, because they wrap drivers that exist nowhere else.&lt;/p&gt;

&lt;p&gt;The interesting part of that port was not &lt;code&gt;java.time&lt;/code&gt;. It was the access-ordered &lt;code&gt;LinkedHashMap&lt;/code&gt;, which does not exist outside the JVM and which the store's eviction, the exact-match layer and the verifier's memo were all built on.&lt;/p&gt;

&lt;p&gt;The default chain has an eleventh guard, for the near miss where nothing changed and something was added. &lt;code&gt;How do I deploy a Rails app&lt;/code&gt; against the same question &lt;code&gt;on Heroku&lt;/code&gt; has perfect word overlap, so no lexical check saw it. Measured at zero false rejections across all three corpora.&lt;/p&gt;

&lt;p&gt;And four opt-in pieces sit around the match path: reranking so each candidate tried adds something the last did not, quantized retrieval that decides which candidates are looked at and never whether one is served, deduplication on write, and adaptive per-scope thresholds that refuse to run without a verifier watching what comes through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading from 1.x
&lt;/h2&gt;

&lt;p&gt;Five named breaks, each with who it affects and the edit that resolves it, in &lt;a href="https://github.com/NaCode-Studios/Kmemo/blob/main/docs/MIGRATION.md" rel="noopener noreferrer"&gt;the migration guide&lt;/a&gt;. The one easiest to miss: Maven users need &lt;code&gt;kmemo-core-jvm&lt;/code&gt;, since Maven does not read Gradle module metadata. Gradle builds change only the version number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.nacode-studios:kmemo-core:2.0.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apache 2.0, on Maven Central. Repository: &lt;a href="https://github.com/NaCode-Studios/Kmemo" rel="noopener noreferrer"&gt;NaCode-Studios/Kmemo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If something here is wrong, or a configuration is missing from the comparison, say so. That is how the guard in the third section got written.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>llm</category>
      <category>caching</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Kdrant 2.0: I removed the reason to use the official client</title>
      <dc:creator>AS</dc:creator>
      <pubDate>Fri, 31 Jul 2026 15:04:41 +0000</pubDate>
      <link>https://dev.to/tonytonycoder11/kdrant-20-i-removed-the-reason-to-use-the-official-client-1gjh</link>
      <guid>https://dev.to/tonytonycoder11/kdrant-20-i-removed-the-reason-to-use-the-official-client-1gjh</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/tonytonycoder11/kdrant-an-idiomatic-coroutine-first-kotlin-client-for-qdrant-4i49"&gt;the post where I introduced Kdrant&lt;/a&gt; there was a paragraph I never liked writing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For raw throughput and streaming, gRPC/HTTP2 still wins. If that's your bottleneck, use the official client.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It was true, and it was also the one line in the post that sent people somewhere else. Kdrant 2.0 answers it. There's a gRPC engine now, sitting behind the same &lt;code&gt;QdrantClient&lt;/code&gt; as the REST one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same client, your choice of wire
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;qdrant&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Kdrant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// REST, port 6333&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;qdrant&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KdrantGrpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// gRPC, port 6334&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the migration. Every call after that line is identical, because the API was never protocol-aware: the wire has been behind a &lt;code&gt;QdrantTransport&lt;/code&gt; interface since the first release.&lt;/p&gt;

&lt;p&gt;Let me be precise about what that's worth, because "we have a clean abstraction" costs nothing to say. Adding the gRPC engine changed zero lines of &lt;code&gt;kdrant-core&lt;/code&gt;. Not a handful of small changes. The diff on that module is empty. It's the only test of a seam that means anything, and running it required writing a second implementation.&lt;/p&gt;

&lt;p&gt;The engine generates its own stubs from Qdrant's &lt;code&gt;.proto&lt;/code&gt; files instead of wrapping &lt;code&gt;io.qdrant:client&lt;/code&gt;. Two reasons for that. grpc-kotlin emits &lt;code&gt;suspend&lt;/code&gt; functions and &lt;code&gt;Flow&lt;/code&gt;s, which is the shape the transport already had, so there's no &lt;code&gt;ListenableFuture&lt;/code&gt; to adapt back into coroutines. And generating decides the dependency set rather than inheriting it: this engine resolves &lt;code&gt;grpc-okhttp&lt;/code&gt;, not the shaded Netty jar that accounts for about 9 MB of the official client on its own.&lt;/p&gt;

&lt;p&gt;If you don't want gRPC you pay nothing for it. It's a separate artifact, and a build that depends on &lt;code&gt;kdrant-transport-rest&lt;/code&gt; resolves no gRPC, no protobuf and no Netty. That's checked on every build rather than asserted in a README. There's a Gradle task that fails if anything from those groups shows up on the REST engine's runtime classpath, and I confirmed it works by adding &lt;code&gt;grpc-stub&lt;/code&gt; to that module and watching it name the five artifacts that arrived.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test suite turned out to be the interesting part
&lt;/h2&gt;

&lt;p&gt;Two engines are only interchangeable if something proves it, and the existing tests couldn't. They assert HTTP request bodies, which a gRPC engine can't satisfy by construction.&lt;/p&gt;

&lt;p&gt;So there's a shared suite now: about 30 behavioural tests that take a &lt;code&gt;QdrantClient&lt;/code&gt; from outside, talk to a real Qdrant in Docker, and never mention a protocol. Both engines run the same file against the same server.&lt;/p&gt;

&lt;p&gt;It earned its keep on the first run. One test failed on gRPC, and only on gRPC: an ordered &lt;code&gt;scroll&lt;/code&gt; came back with no payloads at all.&lt;/p&gt;

&lt;p&gt;The cause is worth knowing if you ever write against Qdrant directly. It defaults &lt;code&gt;with_payload&lt;/code&gt; to true on &lt;code&gt;scroll&lt;/code&gt; and &lt;code&gt;retrieve&lt;/code&gt;. The REST engine just omits the field when you haven't asked for anything, so you get that default. My gRPC engine was translating "the caller said nothing" into an explicit &lt;code&gt;enable = false&lt;/code&gt;, which is a different request. Every scroll and every retrieve that didn't ask for a payload was quietly coming back empty.&lt;/p&gt;

&lt;p&gt;You don't find that by reading code, and no unit test on either engine would have caught it. It needs a test that runs on both and compares them against a server that has opinions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core is multiplatform
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;kdrant-core&lt;/code&gt; builds for the JVM and eight Kotlin/Native targets, covering iOS, macOS, Linux and Windows. Same reason as everything above: code that never knew there was a wire has nothing platform-specific to port. Moving it into &lt;code&gt;commonMain&lt;/code&gt; changed exactly one declaration, the default dispatcher, because only the JVM publishes &lt;code&gt;Dispatchers.IO&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I want to be clear about what that does and doesn't buy you today. The engines are still JVM-only, since Ktor CIO and grpc-java are. An Android app and an iOS app can share their models, their filter DSL and their query building, and the iOS side brings its own transport. That is not the same as "Kdrant runs on iOS", and I'd rather say so than let a list of target names imply it.&lt;/p&gt;

&lt;p&gt;Kotlin/JS is deliberately absent. With no JS engine it would ship a DSL with nothing to send, and its test tooling was the only npm dependency graph in the repo, which arrived carrying a high-severity advisory. One line brings it back the day there's an engine worth having there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What upgrading costs you
&lt;/h2&gt;

&lt;p&gt;Every 1.x call site compiles unchanged, so source compatibility is intact. Binary compatibility is not, in two places, and it's worth being exact about both.&lt;/p&gt;

&lt;p&gt;The first is the artifact layout. &lt;code&gt;kdrant-core&lt;/code&gt; is multiplatform now, so its coordinate carries Gradle module metadata and the JVM classes live in &lt;code&gt;kdrant-core-jvm&lt;/code&gt;. On Gradle you change the version number and nothing else, because Gradle reads that metadata and picks the variant. On Maven, if you named &lt;code&gt;kdrant-core&lt;/code&gt; directly, you move to &lt;code&gt;kdrant-core-jvm&lt;/code&gt;. Depending on &lt;code&gt;kdrant-transport-rest&lt;/code&gt; or &lt;code&gt;kdrant-transport-grpc&lt;/code&gt;, which is what I'd recommend anyway, spares you both.&lt;/p&gt;

&lt;p&gt;The second is smaller and easier to miss. &lt;code&gt;ScrollRequest&lt;/code&gt; and &lt;code&gt;SearchRequest&lt;/code&gt; gained a &lt;code&gt;shardKey&lt;/code&gt; parameter when cluster support landed, which changes their generated &lt;code&gt;copy&lt;/code&gt; and &lt;code&gt;componentN&lt;/code&gt;. The constructor keeps its defaults so your code still compiles, but if you call &lt;code&gt;copy()&lt;/code&gt; on either type against a jar built for 1.x, recompile.&lt;/p&gt;

&lt;p&gt;The multiplatform migration itself changed no public API at all. The &lt;code&gt;*.api&lt;/code&gt; dump is identical either side of it, which is worth saying because it's the part that sounds like it should have broken something.&lt;/p&gt;

&lt;p&gt;One limit to know before you switch engines. Qdrant serves eleven operations over HTTP only: telemetry, Prometheus metrics, the issues endpoint, snapshot recovery, snapshot download and upload, and the shard-scope snapshots. On the gRPC engine each one throws, naming itself and naming REST. A snapshot download that quietly returned nothing would be a backup that quietly doesn't exist, so I'd rather it fail loudly.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.nacode-studios:kdrant-transport-rest:2.0.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// or, for gRPC:&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.nacode-studios:kdrant-transport-grpc:2.0.0"&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;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/NaCode-Studios/Kdrant" rel="noopener noreferrer"&gt;https://github.com/NaCode-Studios/Kdrant&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;API docs: &lt;a href="https://nacode-studios.github.io/Kdrant/" rel="noopener noreferrer"&gt;https://nacode-studios.github.io/Kdrant/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Changelog: &lt;a href="https://github.com/NaCode-Studios/Kdrant/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;https://github.com/NaCode-Studios/Kdrant/blob/main/CHANGELOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are Spring Boot, Spring AI, LangChain4j, Koog and Micrometer modules too, all on the same version.&lt;/p&gt;

&lt;p&gt;Feedback still welcome, and especially from anyone running the gRPC engine under real load. That's the case I built it for and the one I can't properly test on my own.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>ai</category>
      <category>vectordatabase</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The cleanup that could never run: a silent Web Push leak in Go</title>
      <dc:creator>AS</dc:creator>
      <pubDate>Mon, 27 Jul 2026 22:16:24 +0000</pubDate>
      <link>https://dev.to/tonytonycoder11/the-cleanup-that-could-never-run-a-silent-web-push-leak-in-go-1kaj</link>
      <guid>https://dev.to/tonytonycoder11/the-cleanup-that-could-never-run-a-silent-web-push-leak-in-go-1kaj</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nothing reported this bug. No crash, no stack trace, no failing test, no alert. I found it by reading through the notifications path in &lt;a href="https://github.com/agentrq/agentrq" rel="noopener noreferrer"&gt;agentrq&lt;/a&gt;, a Go backend for a human-in-the-loop task manager for AI agents, and noticing that a block of error handling could not execute.&lt;/p&gt;

&lt;p&gt;The code was not missing a case. It handled the case, in the wrong branch, and had a comment explaining what it was doing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;push.sendToUser&lt;/code&gt; loops over a user's browser push subscriptions and delivers a notification to each one. Browsers hand out push endpoints that expire: the user clears site data, uninstalls the PWA, or the subscription lapses on its own. When that happens the push service starts answering &lt;code&gt;410 Gone&lt;/code&gt; or &lt;code&gt;404 Not Found&lt;/code&gt;, and the sender is supposed to drop the subscription.&lt;/p&gt;

&lt;p&gt;Here is what the code did, abbreviated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;webpush&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SendNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;zlog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="c"&gt;// Remove invalid/expired subscriptions (410 Gone)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;410&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeletePushSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read on its own, that block is fine. It knows about &lt;code&gt;410 Gone&lt;/code&gt;. It nil-checks the response before touching it. It calls the right repository method with the right arguments. Someone understood the problem well enough to write a comment naming it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it can never run
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;webpush.SendNotification&lt;/code&gt; comes from &lt;code&gt;SherClockHolmes/webpush-go&lt;/code&gt; (v1.4.0), and it ends with &lt;code&gt;return client.Do(req)&lt;/code&gt;. So the function inherits the &lt;code&gt;http.Client.Do&lt;/code&gt; contract, which splits results in a way that matters here:&lt;/p&gt;

&lt;p&gt;A transport-level failure, like a DNS error or a refused connection, returns &lt;code&gt;(nil, err)&lt;/code&gt;. &lt;code&gt;err&lt;/code&gt; is non-nil, so we enter the branch, but &lt;code&gt;resp&lt;/code&gt; is nil, so &lt;code&gt;resp != nil &amp;amp;&amp;amp; resp.StatusCode == 410&lt;/code&gt; is false.&lt;/p&gt;

&lt;p&gt;A request that completes returns &lt;code&gt;(resp, nil)&lt;/code&gt;, and that includes a &lt;code&gt;410 Gone&lt;/code&gt; and a &lt;code&gt;404 Not Found&lt;/code&gt;. As far as &lt;code&gt;net/http&lt;/code&gt; is concerned, receiving a &lt;code&gt;410&lt;/code&gt; is a success: the round trip worked, the server answered. &lt;code&gt;err&lt;/code&gt; is nil, so the entire &lt;code&gt;if err != nil&lt;/code&gt; block is skipped.&lt;/p&gt;

&lt;p&gt;The two conditions the code needs are in different universes. Whenever &lt;code&gt;resp&lt;/code&gt; is non-nil, &lt;code&gt;err&lt;/code&gt; is nil. Whenever &lt;code&gt;err&lt;/code&gt; is non-nil, &lt;code&gt;resp&lt;/code&gt; is nil. &lt;code&gt;DeletePushSubscription&lt;/code&gt; was unreachable from the moment it was written.&lt;/p&gt;

&lt;p&gt;What I find interesting is where the mistake actually sits. Everything about Web Push in that block is correct: the right status code, the right RFC semantics, the right cleanup call. The wrong assumption is one layer down, about what &lt;code&gt;err&lt;/code&gt; means in Go's HTTP client. The author was right about the domain and wrong about the platform, and the compiler has no opinion about that.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc8030#section-7.3" rel="noopener noreferrer"&gt;RFC 8030 §7.3&lt;/a&gt; says a push service returns &lt;code&gt;404&lt;/code&gt; or &lt;code&gt;410&lt;/code&gt; once a subscription no longer exists. Since those never triggered a delete, and since &lt;code&gt;DeletePushSubscription&lt;/code&gt; is otherwise only called from the explicit user-initiated unsubscribe handlers, nothing in the system ever removed a dead subscription.&lt;/p&gt;

&lt;p&gt;Two things followed. Stale rows accumulated in &lt;code&gt;push_subscriptions&lt;/code&gt; with no upper bound. And every future task or message event kept re-attempting delivery to endpoints that were already gone, which is one wasted outbound HTTP request per event, per dead subscription, forever.&lt;/p&gt;

&lt;p&gt;This is the part I keep thinking about. The cost is not a spike, it is a slope. Each dead subscription adds a small permanent tax on every event the system will ever process, and the tax compounds as more subscriptions die. There is no moment where it breaks, so there is no moment where anyone looks.&lt;/p&gt;

&lt;p&gt;It is also invisible to error monitoring, and not by accident. A dead endpoint answering &lt;code&gt;410&lt;/code&gt; is a completed HTTP request. There is no exception to capture, no non-2xx client error thrown in the app, no log line above warning level. A dashboard would show a healthy delivery path with a slowly rising request count, which is exactly what a growing product looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The cleanup moves off the error branch and onto the successful one, where the status code actually lives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;zlog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"endpoint"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Msg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[push] failed to send notification"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c"&gt;// webpush.SendNotification only returns a non-nil error for transport-level&lt;/span&gt;
&lt;span class="c"&gt;// failures; a delivered-but-rejected request comes back here with err == nil and&lt;/span&gt;
&lt;span class="c"&gt;// the failure encoded in the status code. Per RFC 8030 §7.3 the push service&lt;/span&gt;
&lt;span class="c"&gt;// returns 404 Not Found or 410 Gone once a subscription no longer exists, so&lt;/span&gt;
&lt;span class="c"&gt;// prune it — otherwise it lingers in the DB and every future event re-attempts&lt;/span&gt;
&lt;span class="c"&gt;// delivery to a dead endpoint.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusGone&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DeletePushSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Endpoint&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;I widened &lt;code&gt;410&lt;/code&gt; to &lt;code&gt;404&lt;/code&gt; because RFC 8030 treats both as gone, and the original comment had only mentioned one of them. The transport-error path keeps doing what it did, logging and continuing, since a network failure says nothing about whether the subscription is still valid. Retrying later is correct there.&lt;/p&gt;

&lt;p&gt;The comment is longer than the code. That was deliberate. The thing that made this bug survive is that the broken version looked reasonable, so I wanted the next person reading it to find the reasoning about &lt;code&gt;err&lt;/code&gt; and status codes right there, rather than having to rediscover the &lt;code&gt;client.Do&lt;/code&gt; contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tests, and the part I am glad I did the slow way
&lt;/h2&gt;

&lt;p&gt;This delivery path had no test coverage at all, which is the other reason the bug lasted.&lt;/p&gt;

&lt;p&gt;The easy version of the test mocks out &lt;code&gt;SendNotification&lt;/code&gt; and asserts the delete. It would have passed against the broken code just as happily, because it never exercises the branch structure that was wrong. So I stood up a real &lt;code&gt;httptest.Server&lt;/code&gt; returning &lt;code&gt;410&lt;/code&gt; and &lt;code&gt;404&lt;/code&gt; and let the actual library make the actual round trip.&lt;/p&gt;

&lt;p&gt;That has a cost: &lt;code&gt;webpush.SendNotification&lt;/code&gt; encrypts the payload before sending, so it needs real keys or it fails long before any HTTP happens. The tests generate them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// newTestSubscriptionKeys returns a valid P-256 ECDH public key (p256dh) and a&lt;/span&gt;
&lt;span class="c"&gt;// 16-byte auth secret, base64url-encoded exactly as a browser PushSubscription&lt;/span&gt;
&lt;span class="c"&gt;// provides them. Real keys are required because webpush.SendNotification encrypts&lt;/span&gt;
&lt;span class="c"&gt;// the payload before sending, so bogus keys would fail before any HTTP round-trip.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;newTestSubscriptionKeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p256dh&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Helper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ecdh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;P256&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GenerateKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawURLEncoding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PublicKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bytes&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RawURLEncoding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authBytes&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;&lt;code&gt;TestSendToUser_PrunesExpiredSubscription&lt;/code&gt; asserts &lt;code&gt;DeletePushSubscription(userID, endpoint)&lt;/code&gt; is called for both statuses. &lt;code&gt;TestSendToUser_KeepsSubscriptionOnSuccess&lt;/code&gt; asserts a &lt;code&gt;201&lt;/code&gt; does not delete, which is the assertion that would catch someone later "simplifying" the condition into something that prunes healthy subscriptions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;go test -race ./internal/...&lt;/code&gt; is green, &lt;code&gt;gofmt&lt;/code&gt; and &lt;code&gt;go vet&lt;/code&gt; clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The PR
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/agentrq/agentrq/pull/252" rel="noopener noreferrer"&gt;agentrq/agentrq#252&lt;/a&gt;, merged on July 9. It closes &lt;a href="https://github.com/agentrq/agentrq/issues/251" rel="noopener noreferrer"&gt;#251&lt;/a&gt;, the issue I opened with the diagnosis before writing the fix. Two files, 99 lines added, 4 removed, and most of the additions are tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;p&gt;A comment is a claim, and a claim can be false. &lt;code&gt;// Remove invalid/expired subscriptions (410 Gone)&lt;/code&gt; was not a description of what the code did. It was a description of what someone meant it to do, and it made the block read as solved to everyone who came after.&lt;/p&gt;

&lt;p&gt;The other thing is about where to look. This bug was not in unfamiliar or clever code. It was at a seam, in the handful of lines where application logic meets a library's error convention. The domain reasoning above the seam was right and the assumption below it was wrong, and that combination produces code that reviews well, tests green, and quietly does nothing.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>go</category>
      <category>debugging</category>
    </item>
    <item>
      <title>The half-fixed bug: two upstream crashes in dio and flutter/packages</title>
      <dc:creator>AS</dc:creator>
      <pubDate>Sun, 26 Jul 2026 23:02:35 +0000</pubDate>
      <link>https://dev.to/tonytonycoder11/the-half-fixed-bug-two-upstream-crashes-in-dio-and-flutterpackages-lj6</link>
      <guid>https://dev.to/tonytonycoder11/the-half-fixed-bug-two-upstream-crashes-in-dio-and-flutterpackages-lj6</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Both of these bugs were already known before I touched them. One had a linked issue and a fix that covered half the cases. The other had a unit test that asserted the crash, with a comment above it saying &lt;code&gt;// Remove when fixed!&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They also had the same shape. In both projects the correct behavior already existed elsewhere in the same file: a sibling transformer that handled the input, a sibling branch that reported the failure instead of dying. One path had been left out of it.&lt;/p&gt;

&lt;p&gt;So neither fix invents behavior. Each one extends a decision the project had already made to the branch that missed it.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/cfug/dio" rel="noopener noreferrer"&gt;dio&lt;/a&gt; (12.8k stars) is the HTTP client most Dart and Flutter apps are built on: interceptors, form data, request cancellation, timeouts, and a pluggable &lt;code&gt;Transformer&lt;/code&gt; layer that turns raw response bytes into the typed object your code receives.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/flutter/packages" rel="noopener noreferrer"&gt;flutter/packages&lt;/a&gt; (5.3k stars) is the Flutter team's own monorepo of first-party plugins. The one here is &lt;code&gt;file_selector_android&lt;/code&gt;, the Android implementation behind &lt;code&gt;file_selector&lt;/code&gt;, the plugin you call when the user needs to pick a file from their device.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  1. dio: &lt;code&gt;FormatException&lt;/code&gt; on an empty response body
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FusedTransformer&lt;/code&gt; is dio's default transformer. When a request sets a custom &lt;code&gt;responseDecoder&lt;/code&gt; and the server returns an empty body with a JSON content type (a normal &lt;code&gt;200&lt;/code&gt;, &lt;code&gt;204&lt;/code&gt;, or &lt;code&gt;304&lt;/code&gt;), the transformer's slow path fed the empty string straight into &lt;code&gt;jsonDecode&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FormatException: Unexpected end of input (at character 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;SyncTransformer&lt;/code&gt; and &lt;code&gt;BackgroundTransformer&lt;/code&gt; are handed the identical input and both guard the empty string and return it, which dio then normalizes to &lt;code&gt;null&lt;/code&gt; for typed JSON responses. Only the default transformer crashes.&lt;/p&gt;

&lt;p&gt;This was the residual half of &lt;a href="https://github.com/cfug/dio/issues/2279" rel="noopener noreferrer"&gt;#2279&lt;/a&gt;. &lt;a href="https://github.com/cfug/dio/pull/2285" rel="noopener noreferrer"&gt;PR #2285&lt;/a&gt; had already fixed the empty-body behavior for the fast path, the one with no custom decoder, but the slow path was never given the same guard. The issue looked closed and the fix looked shipped, and anyone who happened to set a &lt;code&gt;responseDecoder&lt;/code&gt; still got an exception.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. flutter/packages: a crash when a picked file can't be copied
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;file_selector_android&lt;/code&gt; crashed the app when a selected file couldn't be copied to a readable location: a &lt;code&gt;SecurityException&lt;/code&gt; from a content provider that has withdrawn permission, or an &lt;code&gt;IOException&lt;/code&gt; on a full disk.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;FileSelectorApiImpl.toFileResponse&lt;/code&gt; looked like it handled this. Abbreviated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;uriPath&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="nc"&gt;FileSelectorNativeException&lt;/span&gt; &lt;span class="n"&gt;nativeError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;uriPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FileUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPathFromCopyOfFileFromUri&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;activityPluginBinding&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getActivity&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// If closing the output stream fails, we cannot be sure that the&lt;/span&gt;
  &lt;span class="c1"&gt;// target file was written in full. […]&lt;/span&gt;
  &lt;span class="n"&gt;uriPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SecurityException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Calling `ContentResolver#openInputStream()` has been reported to throw a&lt;/span&gt;
  &lt;span class="c1"&gt;// `SecurityException` on some devices in certain circumstances. Instead of crashing, we&lt;/span&gt;
  &lt;span class="c1"&gt;// return `null`.&lt;/span&gt;
  &lt;span class="c1"&gt;//&lt;/span&gt;
  &lt;span class="c1"&gt;// See https://github.com/flutter/flutter/issues/100025 for more details.&lt;/span&gt;
  &lt;span class="n"&gt;uriPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IllegalArgumentException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;uriPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;FILE_SELECTOR_EXCEPTION_PLACEHOLDER_PATH&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;nativeError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileSelectorNativeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* … */&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FileResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uriPath&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;contentResolver&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nativeError&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the two branches that crash, &lt;code&gt;uriPath = null&lt;/code&gt; is not an oversight. It is deliberate, and commented as such: &lt;em&gt;"Instead of crashing, we return &lt;code&gt;null&lt;/code&gt;."&lt;/em&gt; The comment cites &lt;a href="https://github.com/flutter/flutter/issues/100025" rel="noopener noreferrer"&gt;flutter/flutter#100025&lt;/a&gt;, an earlier report of the same class of failure.&lt;/p&gt;

&lt;p&gt;The method then builds a &lt;code&gt;FileResponse&lt;/code&gt; on that null anyway. &lt;code&gt;path&lt;/code&gt; is non-null in the Pigeon-generated &lt;code&gt;FileResponse&lt;/code&gt;, so the constructor rejects it at runtime, and the branch written to avoid a crash produces one.&lt;/p&gt;

&lt;p&gt;The third branch survives because &lt;code&gt;IllegalArgumentException&lt;/code&gt; assigns a placeholder path instead of &lt;code&gt;null&lt;/code&gt;, so it reaches Dart with a populated &lt;code&gt;nativeError&lt;/code&gt;. The file already had a way to report an unreadable file without dying. The two branches that chose &lt;code&gt;null&lt;/code&gt; just didn't use it.&lt;/p&gt;

&lt;p&gt;The crash had also shifted shape over time, which is part of why it survived. &lt;a href="https://github.com/flutter/flutter/issues/159568" rel="noopener noreferrer"&gt;flutter/flutter#159568&lt;/a&gt; reported it as an &lt;code&gt;IllegalStateException&lt;/code&gt;, thrown by the older Java Pigeon generator; since the move to the Kotlin generator, the same failure surfaces as an NPE.&lt;/p&gt;

&lt;p&gt;The test suite knew about it too. There was a passing test named &lt;code&gt;openFileThrowsNullPointerException_whenSecurityExceptionInGetPathFromCopyOfFileFromUri&lt;/code&gt;, which asserted the crash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;assertThrows&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;NullPointerException&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;listenerArgumentCaptor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getValue&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
              &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onActivityResult&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;222&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Activity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RESULT_OK&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resultMockIntent&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Above it, a comment: &lt;em&gt;"The behavior is actually an error case and should be fixed … Remove when fixed!"&lt;/em&gt; So the crash was documented, asserted, and passing CI.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;dio: &lt;a href="https://github.com/cfug/dio/pull/2550" rel="noopener noreferrer"&gt;cfug/dio#2550&lt;/a&gt;, merged&lt;/li&gt;
&lt;li&gt;flutter/packages: &lt;a href="https://github.com/flutter/packages/pull/12107" rel="noopener noreferrer"&gt;flutter/packages#12107&lt;/a&gt;, merged and shipped in &lt;code&gt;file_selector_android&lt;/code&gt; 0.5.2+9&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  dio: widen the guard instead of adding a branch
&lt;/h3&gt;

&lt;p&gt;The whole fix is one extra condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isJsonContent&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="n"&gt;decodedResponse&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="n"&gt;decodedResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNotEmpty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// slow path decoder, since there was a custom decoder specified&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jsonDecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decodedResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customResponseDecoder&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decodedResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The obvious patch is a new early return for the empty case. I didn't write one, because the branch that already handles it correctly sits directly underneath: &lt;code&gt;else if (customResponseDecoder != null) { return decodedResponse; }&lt;/code&gt;. Adding &lt;code&gt;isNotEmpty&lt;/code&gt; to the guard lets an empty body fall through into it, and dio's existing normalization turns that into &lt;code&gt;null&lt;/code&gt; for a typed JSON response.&lt;/p&gt;

&lt;p&gt;So the fix adds no new code path. It narrows a claim that was too broad: &lt;em&gt;"this string is JSON"&lt;/em&gt; becomes &lt;em&gt;"this non-empty string is JSON."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A non-empty malformed body still throws, as it did before and as &lt;code&gt;SyncTransformer&lt;/code&gt; and &lt;code&gt;BackgroundTransformer&lt;/code&gt; do. I wanted the three transformers to agree on this input. Making the default one more forgiving than the other two would have been a different change.&lt;/p&gt;

&lt;p&gt;The regression test pins the behavior to the specific combination that was broken (empty body, JSON content type, custom decoder) rather than to the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RequestOptions&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ResponseBody&lt;/span&gt; &lt;span class="n"&gt;rb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;allowMalformed:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;FusedTransformer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transformResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;RequestOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nl"&gt;responseType:&lt;/span&gt; &lt;span class="n"&gt;ResponseType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nl"&gt;responseDecoder:&lt;/span&gt; &lt;span class="n"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;ResponseBody&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromBytes&lt;/span&gt;&lt;span class="p"&gt;([],&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;headers:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contentTypeHeader&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jsonContentType&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  flutter/packages: return null instead of an invalid object
&lt;/h3&gt;

&lt;p&gt;Again the correct behavior already existed one level up. The callers of &lt;code&gt;toFileResponse&lt;/code&gt; handle a null return: they complete the Dart result with &lt;code&gt;completeWithError("Failed to read file: …")&lt;/code&gt;, which is what they do everywhere else a file can't be read. &lt;code&gt;toFileResponse&lt;/code&gt; was the only place that pushed on regardless.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uriPath&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// `getPathFromCopyOfFileFromUri` can fail to produce a path: either by&lt;/span&gt;
  &lt;span class="c1"&gt;// throwing (handled above by returning a null `uriPath`) or by returning&lt;/span&gt;
  &lt;span class="c1"&gt;// null directly. Return null so the caller surfaces the failure to Dart,&lt;/span&gt;
  &lt;span class="c1"&gt;// instead of building a `FileResponse` with a null `path`, which the&lt;/span&gt;
  &lt;span class="c1"&gt;// non-null field rejects at runtime.&lt;/span&gt;
  &lt;span class="c1"&gt;// See https://github.com/flutter/flutter/issues/159568.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking &lt;code&gt;uriPath == null&lt;/code&gt; rather than catching the exception at the call site was deliberate. &lt;code&gt;getPathFromCopyOfFileFromUri&lt;/code&gt; can also &lt;em&gt;return&lt;/em&gt; null without throwing, and that path led to the same crash, so one check covers both.&lt;/p&gt;

&lt;p&gt;The failure now reaches Dart as a &lt;code&gt;PlatformException&lt;/code&gt; you can catch, which is what the &lt;code&gt;uriPath = null&lt;/code&gt; branches were aiming for.&lt;/p&gt;

&lt;p&gt;I inverted the test that asserted the crash rather than deleting it. It now drives &lt;code&gt;onActivityResult&lt;/code&gt; for real and asserts that the callback completes with a failure whose message contains &lt;code&gt;"Failed to read file"&lt;/code&gt;. I added a second one for the single-file &lt;code&gt;openFile&lt;/code&gt; path, which had the same defect and no coverage at all.&lt;/p&gt;

&lt;p&gt;I also kept the PR scoped to the crash. &lt;a href="https://github.com/flutter/flutter/issues/159568" rel="noopener noreferrer"&gt;#159568&lt;/a&gt; suggests routing these branches through the typed &lt;code&gt;FileSelectorNativeException&lt;/code&gt; mechanism, the way &lt;a href="https://github.com/flutter/packages/pull/8184" rel="noopener noreferrer"&gt;#8184&lt;/a&gt; did elsewhere. That is the better long-term design, but it needs a Pigeon regeneration and a Dart-facing API change, which would put a breaking change in the same PR as a crash fix. The API cleanup is worth doing on its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from both
&lt;/h2&gt;

&lt;p&gt;The reflex when you find a crash is to add a check. In both of these the check already existed, in the transformer next door or in the caller one frame up, so most of the work was reading enough of the surrounding code to find it. Once you have, the patch is small enough to review in one sitting.&lt;/p&gt;

&lt;p&gt;Both bugs had also been looked at before. dio's had an issue and a fix that covered the fast path. The Android one had a test asserting the crash and a comment saying to remove it when fixed. Writing a bug down is not the same as fixing it, and it makes the bug much harder to see the second time around.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>dart</category>
      <category>flutter</category>
    </item>
    <item>
      <title>We built the benchmark we'd want any AI estimation vendor to pass. Then we failed it.</title>
      <dc:creator>AS</dc:creator>
      <pubDate>Sun, 26 Jul 2026 09:59:29 +0000</pubDate>
      <link>https://dev.to/tonytonycoder11/we-built-the-benchmark-wed-want-any-ai-estimation-vendor-to-pass-then-we-failed-it-43a5</link>
      <guid>https://dev.to/tonytonycoder11/we-built-the-benchmark-wed-want-any-ai-estimation-vendor-to-pass-then-we-failed-it-43a5</guid>
      <description>&lt;p&gt;Large IT projects run about 45% over budget on average. That figure comes from McKinsey and Oxford looking at more than 5,400 of them, and it is the reason a wave of tooling now promises to read your spec and tell you what it will cost.&lt;/p&gt;

&lt;p&gt;We were about to build one of those tools. Before writing the product, we wrote the test.&lt;/p&gt;

&lt;p&gt;The claim is falsifiable, which is the useful thing about it. The effort-estimation research community has been publishing datasets with real recorded effort for decades. So we asked the version of the question that can come back "no":&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On public data with real logged effort, can a statistical engine beat the standard baselines and the human expert, out of sample, without leakage?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It cannot. Not with any model class we threw at it. &lt;a href="https://github.com/NaCode-Studios/metis-benchmark" rel="noopener noreferrer"&gt;The benchmark is public&lt;/a&gt;, MIT, and reproducible from a pinned environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bar, frozen before any result
&lt;/h2&gt;

&lt;p&gt;We fixed what "passing" meant and committed it to git before running each gate experiment. The git history is the pre-registration.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PRED(25), the share of estimates within 25% of actual&lt;/td&gt;
&lt;td&gt;≥ 55% on at least 2 datasets per channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MdAPE, median absolute percentage error&lt;/td&gt;
&lt;td&gt;≤ 22% on at least 2 datasets per channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coverage, actuals inside the nominal-90% interval&lt;/td&gt;
&lt;td&gt;within 5 points of 90%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Baselines&lt;/td&gt;
&lt;td&gt;beat median-by-category and log-size regression&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Expert&lt;/td&gt;
&lt;td&gt;beat the recorded human estimate, in aggregate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;PRED(25) and MdAPE are the standard metrics in this literature, so the results sit next to published papers without translation. And 55% is deliberately lenient: it is a lower bar than the KPIs we would set on one organization's own history, because public datasets mix organizations and are messier by construction.&lt;/p&gt;

&lt;p&gt;Nine datasets, two channels. Tabular project attributes (PROMISE Desharnais, COCOMO81, China, Kitchenham, Maxwell, Albrecht, plus SEERA) and the natural-language text of requirements (JOSSE, SiP, with Deep-SE as secondary evidence).&lt;/p&gt;

&lt;h2&gt;
  
  
  Four choices that make a negative worth reading
&lt;/h2&gt;

&lt;p&gt;A disappointing number is not a finding. These are the parts that turn one into the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time-ordered evaluation, never random.&lt;/strong&gt; Random splits let the model peek at the future and inflate every metric. We used rolling-origin cross-validation on the dated datasets and leave-projects-out grouped k-fold on the task datasets. The second one is deliberately the cold-start case: a new project retrieves only from other projects' history, which is exactly the situation a vendor walks into with a new client.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An adversarial leakage audit.&lt;/strong&gt; Plenty of dataset attributes secretly encode the outcome. SEERA's &lt;code&gt;Requirements stability&lt;/code&gt; is computed from modifications during testing and deployment. &lt;code&gt;Team continuity&lt;/code&gt; counts developers who left the project. &lt;code&gt;Programmers' capability&lt;/code&gt; is built from realized work accuracy. None of that is knowable before a project starts. An independent three-way automated audit classified all 76 SEERA attributes against their published formulas, and we kept only the ones that survived unanimously: 51 of 76.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conformalized intervals.&lt;/strong&gt; We do not promise a point, we promise a range that holds. CQR wraps any model and calibrates a distribution-free interval, and we check the empirical coverage rather than the assumed one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A stop rule committed in advance.&lt;/strong&gt; Before seeing results we fixed this: if the honest feasibility ceiling, meaning the best out-of-sample PRED(25) across a flexible zoo (Gaussian process, gradient boosting, random forest, k-NN, TF-IDF), sits below 55% on more than one gate dataset, then 55% is unreachable on this data and the channel closes as "threshold unreachable, demonstrated".&lt;/p&gt;

&lt;p&gt;That last one is what converts "we couldn't get a good number" into "the number is not there".&lt;/p&gt;

&lt;h2&gt;
  
  
  Channel A: tabular
&lt;/h2&gt;

&lt;p&gt;The best engine was a log-space Gaussian process. Gradient boosting, a size mean function and hierarchical pooling across datasets were all implemented and none of them helped, because the GP already captures size through its features.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset&lt;/th&gt;
&lt;th&gt;n test&lt;/th&gt;
&lt;th&gt;GP PRED(25)&lt;/th&gt;
&lt;th&gt;Honest ceiling [CI95]&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Desharnais&lt;/td&gt;
&lt;td&gt;41&lt;/td&gt;
&lt;td&gt;36.6%&lt;/td&gt;
&lt;td&gt;46.3% [31.7, 61.0]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kitchenham&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;td&gt;45.2%&lt;/td&gt;
&lt;td&gt;45.2% [34.2, 56.2]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maxwell&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;51.6%&lt;/td&gt;
&lt;td&gt;51.6% [35.5, 67.7]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;COCOMO81&lt;/td&gt;
&lt;td&gt;62&lt;/td&gt;
&lt;td&gt;41.9%&lt;/td&gt;
&lt;td&gt;41.9% [30.6, 54.8]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEERA (sealed holdout)&lt;/td&gt;
&lt;td&gt;59&lt;/td&gt;
&lt;td&gt;25.4%&lt;/td&gt;
&lt;td&gt;33.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;China (no signal)&lt;/td&gt;
&lt;td&gt;499&lt;/td&gt;
&lt;td&gt;19.2%&lt;/td&gt;
&lt;td&gt;~21%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Aggregated over the four gate development datasets (n=207) the engine reaches 43.5% [36.7, 50.2] and beats the log-size baseline by 12.1 points [5.3, 18.8]. That lift is real and confident. It is also not enough: no dataset's central estimate reaches 55%, and the ceiling's point estimate is below 55% on all five including the sealed holdout, which fired the stop rule.&lt;/p&gt;

&lt;p&gt;I want to be careful about what that does and does not say. The tabular test blocks are small, 12 to 73 projects, and Maxwell's ceiling has an upper bound of 67.7%. We are not claiming 55% is impossible on any single small dataset. We are claiming its central estimate is below 55% everywhere we looked, on data whose noise floor is high: the best model's out-of-sample residual scatter is σ ≈ 0.6 in log space, which is roughly ±60% multiplicative, on every dataset.&lt;/p&gt;

&lt;p&gt;Richer models do not move that. There is nothing left to extract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Channel B: the requirement text
&lt;/h2&gt;

&lt;p&gt;The hypothesis was reasonable. Semantically similar requirements should take similar effort, so embed the text, retrieve nearest neighbours, predict a kernel-weighted mean.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;SiP&lt;/th&gt;
&lt;th&gt;JOSSE&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Semantic k-NN (embeddings)&lt;/td&gt;
&lt;td&gt;17.9%&lt;/td&gt;
&lt;td&gt;14.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learned regressor on embeddings (GBM)&lt;/td&gt;
&lt;td&gt;19.5%&lt;/td&gt;
&lt;td&gt;15.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learned regressor on embeddings (RF)&lt;/td&gt;
&lt;td&gt;18.8%&lt;/td&gt;
&lt;td&gt;14.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TF-IDF k-NN (lexical)&lt;/td&gt;
&lt;td&gt;19.6%&lt;/td&gt;
&lt;td&gt;14.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Honest ceiling&lt;/td&gt;
&lt;td&gt;19.6%&lt;/td&gt;
&lt;td&gt;15.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text-blind median (floor)&lt;/td&gt;
&lt;td&gt;18.0%&lt;/td&gt;
&lt;td&gt;17.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human expert&lt;/td&gt;
&lt;td&gt;41.6%&lt;/td&gt;
&lt;td&gt;45.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No text method beats the text-blind median on real logged effort. On SiP it is a statistical tie. On JOSSE the entire ceiling, 15.0%, sits below the floor of 17.2%, which means the requirement text is worse than guessing the median. Cross-encoder reranking made it worse still, though that one is on us: the standard &lt;code&gt;ms-marco&lt;/code&gt; reranker is a query-document model and task pairs are symmetric.&lt;/p&gt;

&lt;p&gt;There is one positive in the whole channel. On Deep-SE story points the semantic channel beats the floor by 2.7 points [2.0, 3.4]. Text carries some signal. But story points are a team's own relative scale, not hours, and the signal does not transfer to logged time.&lt;/p&gt;

&lt;p&gt;The likely mechanism is not mysterious. Logged time is dominated by who did the work, how often they were interrupted, and local logging habits. None of that is in the requirement text, and across organizations it is most of the variance. Two identically worded tasks take very different logged time on two different teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two things survived
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The intervals are honest.&lt;/strong&gt; A nominal-90% conformal interval covers the truth 94.6% of the time [91.5, 97.7] on unseen projects, distribution-free. It over-covers at lower nominal levels, so the guarantee is kept but loose. The failure here is accuracy, not calibration, and those are different products. "Here is a range that holds 90% of the time" is deliverable today. "Here is the number" is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The human expert wins.&lt;/strong&gt; Where a dataset records a strong expert estimate, the expert beats every model: Kitchenham 61.6%, JOSSE 45%, SiP 41.6%. Experts use team and codebase context that public features and public text simply do not contain. The one place the model holds its own is the SEERA holdout, where the recorded expert is itself weak at 25.4% and the model ties it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The follow-up that surprised me
&lt;/h2&gt;

&lt;p&gt;If the expert beats the model, the obvious product move is to blend them. Average the two, keep the best of both.&lt;/p&gt;

&lt;p&gt;We tested it properly and it does not work. Pooled over 10,551 paired projects, an inverse-variance blend loses to the expert alone with McNemar p &amp;lt; 0.0001, at power ≈ 1.00. That is a real negative, not a wide confidence interval. The diagnostic is that the model and expert residuals correlate, so the diversification a blend is banking on is largely fictitious.&lt;/p&gt;

&lt;p&gt;Worth stating the scope: on SiP and JOSSE the "model" in that test is the Track B engine that had already failed to beat the text-blind floor, so this measures blending &lt;em&gt;that&lt;/em&gt; model, not blending in general. The weighting machinery is model-agnostic. If some future model measures a smaller residual spread than the expert on a client's own calibration data, the same formula shifts weight toward it automatically.&lt;/p&gt;

&lt;p&gt;The first version of this test, at G0, was under-powered: n=73, about 11 discordant pairs, power ≈ 0.16, McNemar p=0.2266. It showed a +6.8 point difference with a confidence interval of [-1.4, +15.1] that included zero. An earlier draft of our own report framed that as a positive direction. It is retracted in the repo, because absence of proof is not proof of presence either, and a 0.16-power test is not evidence of anything.&lt;/p&gt;

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

&lt;p&gt;Every gate split was a cross-organization cold start on public data. That is the hardest case, and it is precisely not where an estimation tool gets deployed.&lt;/p&gt;

&lt;p&gt;A single organization's own history, same team, same logging conventions, same codebase, is the regime public datasets cannot proxy. This benchmark does not measure it and I make no claim about it here. It is the obvious next experiment, and it needs data we do not have.&lt;/p&gt;

&lt;p&gt;If you have that history and it interests you, the README says how to reach us. What I can promise is the same discipline you can read in the repo, applied to your data, and a straight answer either way.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/NaCode-Studios/metis-benchmark
&lt;span class="nb"&gt;cd &lt;/span&gt;metis-benchmark
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[models,semantic,plots,dev]"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; constraints-g0.txt
make reproduce-g0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;constraints-g0.txt&lt;/code&gt; is a &lt;code&gt;pip freeze&lt;/code&gt; of the environment the numbers were produced in, on CPython 3.14.4. Bootstrap seeds are fixed, but a library upgrade can still move third-decimal behaviour in the GP optimizer or the XGBoost splits, so exact reproduction needs the constraints file.&lt;/p&gt;

&lt;p&gt;Every number in this post is produced by those scripts. The protocol, its amendments and the pre-registration timestamps are in &lt;a href="https://github.com/NaCode-Studios/metis-benchmark/blob/main/reports/protocol.md" rel="noopener noreferrer"&gt;&lt;code&gt;reports/protocol.md&lt;/code&gt;&lt;/a&gt;, the per-phase results are in &lt;a href="https://github.com/NaCode-Studios/metis-benchmark/tree/main/reports/results" rel="noopener noreferrer"&gt;&lt;code&gt;reports/results/&lt;/code&gt;&lt;/a&gt;, and the full write-up with the errata section is &lt;a href="https://github.com/NaCode-Studios/metis-benchmark/blob/main/reports/REPORT.md" rel="noopener noreferrer"&gt;&lt;code&gt;reports/REPORT.md&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;We wrote a test designed to be hard, ran it on ourselves, and it came back no. Publishing that is cheaper than the alternative, which is shipping a claim about cold-start estimation from a spec and finding out in front of a client.&lt;/p&gt;

&lt;p&gt;There is also a shortage of this. Negative results in applied ML mostly do not get written up, so the same hypothesis keeps getting re-tested privately by people who cannot see each other's results. If you are evaluating an effort-estimation vendor, the benchmark is a thing you can point at and ask them to run.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/NaCode-Studios/metis-benchmark" rel="noopener noreferrer"&gt;github.com/NaCode-Studios/metis-benchmark&lt;/a&gt;. Issues are open, and disagreement with the method is the most useful kind.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>datascience</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Kmemo: a semantic cache for LLM calls that refuses to serve you the wrong answer</title>
      <dc:creator>AS</dc:creator>
      <pubDate>Sat, 25 Jul 2026 18:50:51 +0000</pubDate>
      <link>https://dev.to/tonytonycoder11/kmemo-a-semantic-cache-for-llm-calls-that-refuses-to-serve-you-the-wrong-answer-54h7</link>
      <guid>https://dev.to/tonytonycoder11/kmemo-a-semantic-cache-for-llm-calls-that-refuses-to-serve-you-the-wrong-answer-54h7</guid>
      <description>&lt;p&gt;An exact-match cache misses "how do I reverse a list in Python" when it has already answered "python list reverse". A semantic cache doesn't: it embeds the prompt, finds the closest one it has seen, and replays that answer instead of calling the model. Fewer API calls, lower latency, same answers.&lt;/p&gt;

&lt;p&gt;Except for the part where it hands back the wrong one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Convert 100 USD to EUR"
"Convert 250 USD to EUR"      cosine similarity: ~0.99
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every mainstream embedding model scores that pair around 0.99. No threshold separates it from a genuine paraphrase, because on the similarity axis the near miss sits closer than most paraphrases do. Raise the threshold and you lose real hits before you lose that one.&lt;/p&gt;

&lt;p&gt;So a cache built on a threshold alone will tell someone that 250 dollars is 92 euros. Quickly, with no error, and nothing in the logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Kmemo does about it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/NaCode-Studios/Kmemo" rel="noopener noreferrer"&gt;Kmemo&lt;/a&gt; treats that as the main event rather than an edge case. Similarity is only the first filter. Candidates that clear it get read as text by a chain of ten guards looking for concrete evidence that the two answers must differ: swapped numbers, mismatched units, different entities, different time references, negation, flipped antonyms, reversed comparisons, or a different kind of answer being asked for.&lt;/p&gt;

&lt;p&gt;The defaults follow from the cost asymmetry. A wrong rejection costs one API call. A wrong acceptance costs a wrong answer. So the guards abstain rather than guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;

&lt;p&gt;Requires JDK 17+.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.nacode-studios:kmemo-core:1.0.0"&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;&lt;code&gt;kmemo-core&lt;/code&gt; declares &lt;code&gt;kotlinx-coroutines-core&lt;/code&gt; as its only dependency. You bring the embedding source, which is any function from &lt;code&gt;String&lt;/code&gt; to &lt;code&gt;FloatArray&lt;/code&gt;. Kmemo ships none and depends on no provider SDK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SemanticCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;embedder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Embedder&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;openAi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;embed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InMemoryStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxEntries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;answer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&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;&lt;code&gt;getOrPut&lt;/code&gt; embeds the prompt once and reuses the vector for both the lookup and the write. Concurrent callers asking the same thing get coalesced: the first one computes, the rest wait and are served its answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every miss tells you why
&lt;/h2&gt;

&lt;p&gt;A cache with a 4% hit rate is untunable unless you know what caused the misses, because the fix is opposite for a threshold miss and a guard rejection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;CacheLookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Hit&lt;/span&gt;  &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;
    &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nc"&gt;CacheLookup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Miss&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;MissReason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BELOW_THRESHOLD&lt;/span&gt;   &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// traffic repeats less than you assumed, or the threshold is too tight&lt;/span&gt;
        &lt;span class="nc"&gt;MissReason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;REJECTED_BY_GUARD&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="c1"&gt;// a guard found a concrete difference; result.detail says which&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;null&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;There's also &lt;code&gt;cache.explain(prompt)&lt;/code&gt;, a read-only companion that shows every candidate with every guard's verdict. It's what you reach for when a hit you expected didn't happen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scopes
&lt;/h2&gt;

&lt;p&gt;Anything that changes what a correct answer looks like belongs in the scope: model, temperature, system prompt, tenant, language. Leave it out and the cache will serve one model's answer to another model's caller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOrPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"gpt-4o|t=0.0|v3"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&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;h2&gt;
  
  
  Choosing how strict to be
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;SemanticCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                                    &lt;span class="c1"&gt;// MatchGuards.standard()&lt;/span&gt;
&lt;span class="nc"&gt;SemanticCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guards&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MatchGuards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strict&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;     &lt;span class="c1"&gt;// trades hit rate for margin&lt;/span&gt;
&lt;span class="nc"&gt;SemanticCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guards&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MatchGuards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;none&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;       &lt;span class="c1"&gt;// the naive similarity-only baseline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guards work outside English too. Curated packs ship for Italian, Spanish, German and French, each measured against a localized near-miss corpus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nc"&gt;SemanticCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guards&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MatchGuards&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Locale&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ITALIAN&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What lexical guards can't see
&lt;/h2&gt;

&lt;p&gt;About a third of near misses need world knowledge. Deworming a puppy is not the same as deworming an adult dog. The boiling point of ethanol is not the boiling point of methanol. No amount of token comparison catches those.&lt;/p&gt;

&lt;p&gt;For that there's an optional &lt;code&gt;Verifier&lt;/code&gt;, typically a cheap model call. It runs only on candidates that already cleared the threshold and every guard, so it costs nothing in the common case, and it fails closed: a timeout or an error rejects rather than serving something unconfirmed.&lt;/p&gt;

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

&lt;p&gt;The guards are judged against three labelled corpora with a blind validation split that no guard was tuned against, run as a CI regression gate on every build.&lt;/p&gt;

&lt;p&gt;On the blind split, near misses are rejected 67% of the time and paraphrases are kept 88% of the time.&lt;/p&gt;

&lt;p&gt;Neither number is 100%, and I would rather publish them than a marketing claim. The near misses that get through are mostly the world-knowledge cases the verifier covers. Reproduce them yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./gradlew :kmemo-core:test &lt;span class="nt"&gt;--tests&lt;/span&gt; &lt;span class="s1"&gt;'*CorpusTest*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How the blind splits grow without getting contaminated is written up in &lt;a href="https://github.com/NaCode-Studios/Kmemo/blob/main/docs/CORPUS.md" rel="noopener noreferrer"&gt;docs/CORPUS.md&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Calibrate the threshold, don't copy it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ThresholdCalibrator&lt;/code&gt; measures the right threshold for your embedding model. The value you found in a blog post was tuned for somebody else's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stores, resilience, observability
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Embedder&lt;/code&gt; and &lt;code&gt;CacheStore&lt;/code&gt; are one-method seams, so you can start in memory and move to a vector database without touching the match logic. Redis (RediSearch KNN) and Postgres (pgvector) stores ship, plus an opt-in in-process HNSW store for when the exact scan stops scaling.&lt;/p&gt;

&lt;p&gt;The embedder is a network call on every lookup, so Kmemo lets you own its failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SemanticCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;embedder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;myEmbedder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrying&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxAttempts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;embedFailurePolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmbedFailurePolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FALL_BACK_TO_COMPUTE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;negativeCacheSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;faqPairs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nc"&gt;WarmEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For dashboards and logs, subscribe to the event stream instead of polling &lt;code&gt;stats()&lt;/code&gt;. It costs nothing when unused:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;metrics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;KmemoMetrics&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;also&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bindTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meterRegistry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// kmemo-micrometer&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;cache&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SemanticCache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listeners&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Slf4jCacheListener&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Integrations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Spring Boot starter that auto-configures a &lt;code&gt;SemanticCache&lt;/code&gt; bean&lt;/li&gt;
&lt;li&gt;A Spring AI caching &lt;code&gt;Advisor&lt;/code&gt; for &lt;code&gt;ChatClient&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;A LangChain4j caching &lt;code&gt;ChatModel&lt;/code&gt; wrapper&lt;/li&gt;
&lt;li&gt;A Ktor server plugin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/NaCode-Studios/Kmemo/tree/main/examples" rel="noopener noreferrer"&gt;&lt;code&gt;examples/&lt;/code&gt;&lt;/a&gt; is a runnable demo that needs no API key. It shows a guard catching a live near miss, with a &lt;code&gt;docker-compose&lt;/code&gt; for the Redis store.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/NaCode-Studios/Kmemo" rel="noopener noreferrer"&gt;https://github.com/NaCode-Studios/Kmemo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Maven Central: &lt;code&gt;io.github.nacode-studios:kmemo-core:1.0.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;API docs: &lt;a href="https://nacode-studios.github.io/Kmemo/" rel="noopener noreferrer"&gt;https://nacode-studios.github.io/Kmemo/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apache-2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;1.0&lt;/code&gt; is stable under SemVer. If you have run a semantic cache in production and hit a false hit I haven't thought about, I want to hear about it. Open an issue with the pair that broke it.&lt;/p&gt;

&lt;p&gt;If you'd want something like this to exist, a star is what makes it findable for&lt;br&gt;
the next person looking.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>ai</category>
      <category>llm</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Kdrant: an idiomatic, coroutine-first Kotlin client for Qdrant</title>
      <dc:creator>AS</dc:creator>
      <pubDate>Tue, 21 Jul 2026 22:36:55 +0000</pubDate>
      <link>https://dev.to/tonytonycoder11/kdrant-an-idiomatic-coroutine-first-kotlin-client-for-qdrant-4i49</link>
      <guid>https://dev.to/tonytonycoder11/kdrant-an-idiomatic-coroutine-first-kotlin-client-for-qdrant-4i49</guid>
      <description>&lt;p&gt;If you build on the JVM and want to use &lt;a href="https://qdrant.tech" rel="noopener noreferrer"&gt;Qdrant&lt;/a&gt;, the official client is &lt;code&gt;io.qdrant:client&lt;/code&gt;, and it's built for Java. Every call returns a &lt;code&gt;ListenableFuture&lt;/code&gt;, requests are assembled with protobuf builders, and it drags a gRPC/Netty stack onto your classpath. From Kotlin that means fighting the language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// official Java client, from Kotlin&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;future&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ListenableFuture&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UpdateResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsertAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"articles"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;points&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// block, or bolt on a future→coroutine bridge yourself&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You reach for coroutines, you get futures. You want a DSL, you get protobuf builders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kdrant
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/NaCode-Studios/Kdrant" rel="noopener noreferrer"&gt;Kdrant&lt;/a&gt; is the client you'd actually want to write Kotlin against:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every operation is a &lt;code&gt;suspend&lt;/code&gt; function, so cancellation and timeouts are cooperative.&lt;/li&gt;
&lt;li&gt;Collections, points, payloads, and filters are built through type-safe DSLs instead of request objects.&lt;/li&gt;
&lt;li&gt;The transport is a pure Kotlin REST engine on Ktor and &lt;code&gt;kotlinx-serialization&lt;/code&gt;, which keeps gRPC, Netty, and protobuf off your classpath.&lt;/li&gt;
&lt;li&gt;Failures surface as a sealed &lt;code&gt;KdrantException&lt;/code&gt; you can handle exhaustively.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's stable (&lt;code&gt;1.1.0&lt;/code&gt;, SemVer) and published to Maven Central under &lt;code&gt;io.github.nacode-studios&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;

&lt;p&gt;Requires JDK 17+. One dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nf"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;implementation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"io.github.nacode-studios:kdrant-transport-rest:1.1.0"&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;Spin up Qdrant locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 6333:6333 qdrant/qdrant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connect, create a collection, upsert, search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;qdrant&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Kdrant&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6333&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;apiKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"QDRANT_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// omit for a local, unauthenticated node&lt;/span&gt;
    &lt;span class="n"&gt;requestTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;qdrant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCollection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"articles"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;vector&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1_536&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Distance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;COSINE&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"articles"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// your List&amp;lt;Float&amp;gt; from any embedding model&lt;/span&gt;
            &lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"title"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="s"&gt;"Introduction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"lang"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"year"&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;hits&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"articles"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryVector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
        &lt;span class="nf"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;must&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"lang"&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="s"&gt;"en"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kdrant stores and searches vectors you already have. It does not generate embeddings.&lt;/p&gt;

&lt;h2&gt;
  
  
  A filter DSL that reads like Kotlin
&lt;/h2&gt;

&lt;p&gt;Qdrant's full filtering model, written declaratively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;must&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"lang"&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="s"&gt;"en"&lt;/span&gt;
        &lt;span class="s"&gt;"year"&lt;/span&gt; &lt;span class="n"&gt;gte&lt;/span&gt; &lt;span class="mi"&gt;2024&lt;/span&gt;
        &lt;span class="s"&gt;"price"&lt;/span&gt; &lt;span class="n"&gt;between&lt;/span&gt; &lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mf"&gt;99.0&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;should&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;matchAny&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tag"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"featured"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"promo"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;geoRadius&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;GeoPoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lon&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;13.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;52.52&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;radius&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5_000.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;mustNot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"archived"&lt;/span&gt; &lt;span class="n"&gt;eq&lt;/span&gt; &lt;span class="k"&gt;true&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;h2&gt;
  
  
  Decode results straight into your types
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Serializable&lt;/span&gt; &lt;span class="kd"&gt;data class&lt;/span&gt; &lt;span class="nc"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Hit&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qdrant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;searchAs&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="s"&gt;"articles"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queryVector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Article&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;firstOrNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Hybrid search (dense + sparse)
&lt;/h2&gt;

&lt;p&gt;Kdrant uses Qdrant's modern &lt;code&gt;/points/query&lt;/code&gt; engine, so you can fuse several prefetch sources with Reciprocal Rank Fusion and get real dense plus keyword hybrid search:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;hits&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;qdrant&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"articles"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;prefetch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;denseVector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;using&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;prefetch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;querySparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;indices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;using&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;rrf&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// or dbsf()&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;recommend&lt;/code&gt; / &lt;code&gt;discover&lt;/code&gt; / &lt;code&gt;context&lt;/code&gt;, grouped and batch search, multi-vectors, and a &lt;code&gt;Flow&lt;/code&gt;-based &lt;code&gt;scroll&lt;/code&gt; are there too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integrations
&lt;/h2&gt;

&lt;p&gt;If you're building RAG, you probably don't want to wire the client in by hand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Spring Boot starter that auto-configures a &lt;code&gt;QdrantClient&lt;/code&gt; bean&lt;/li&gt;
&lt;li&gt;A Spring AI &lt;code&gt;VectorStore&lt;/code&gt; implementation&lt;/li&gt;
&lt;li&gt;A LangChain4j &lt;code&gt;EmbeddingStore&lt;/code&gt; implementation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a runnable &lt;a href="https://github.com/NaCode-Studios/Kdrant/tree/main/example-rag" rel="noopener noreferrer"&gt;&lt;code&gt;example-rag&lt;/code&gt;&lt;/a&gt; service (ingest, embed, store, retrieve) with a &lt;code&gt;docker-compose&lt;/code&gt; for Qdrant, so you can run the whole thing locally.&lt;/p&gt;

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

&lt;p&gt;For raw throughput and streaming, gRPC/HTTP2 still wins. If that's your bottleneck, use the official client. Kdrant trades it for idiomatic Kotlin and a much smaller footprint:&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;Kdrant&lt;/th&gt;
&lt;th&gt;Official &lt;code&gt;io.qdrant:client&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Wire protocol&lt;/td&gt;
&lt;td&gt;REST over Ktor CIO&lt;/td&gt;
&lt;td&gt;gRPC (HTTP/2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Heavy deps&lt;/td&gt;
&lt;td&gt;none, pure Kotlin&lt;/td&gt;
&lt;td&gt;shaded Netty, protobuf, gRPC, Guava&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Added footprint&lt;/td&gt;
&lt;td&gt;~3-5 MB&lt;/td&gt;
&lt;td&gt;~15-20 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API style&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;suspend&lt;/code&gt; + &lt;code&gt;Flow&lt;/code&gt;, DSL&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ListenableFuture&lt;/code&gt;, protobuf builders&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GraalVM native&lt;/td&gt;
&lt;td&gt;friendly&lt;/td&gt;
&lt;td&gt;needs gRPC/Netty/protobuf config&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For typical RAG and embedding-search workloads, that's a trade I'll take.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/NaCode-Studios/Kdrant" rel="noopener noreferrer"&gt;https://github.com/NaCode-Studios/Kdrant&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Maven Central: &lt;code&gt;io.github.nacode-studios:kdrant-transport-rest:1.1.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;API docs: &lt;a href="https://nacode-studios.github.io/Kdrant/" rel="noopener noreferrer"&gt;https://nacode-studios.github.io/Kdrant/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Apache-2.0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback is welcome, especially on API ergonomics. If there's something you'd want from a Kotlin-native Qdrant client, open an issue and tell me.&lt;/p&gt;

&lt;p&gt;If you'd want something like this to exist, a star is what makes it findable for&lt;br&gt;
the next person looking.&lt;/p&gt;

</description>
      <category>kotlin</category>
      <category>ai</category>
      <category>vectordatabase</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Stripe-connect-reckon – a read-only monitor for Stripe Connect balance risk</title>
      <dc:creator>AS</dc:creator>
      <pubDate>Mon, 29 Jun 2026 02:23:37 +0000</pubDate>
      <link>https://dev.to/tonytonycoder11/stripe-connect-reckon-a-read-only-monitor-for-stripe-connect-balance-risk-3i25</link>
      <guid>https://dev.to/tonytonycoder11/stripe-connect-reckon-a-read-only-monitor-for-stripe-connect-balance-risk-3i25</guid>
      <description>&lt;p&gt;I worked on a marketplace where a connected account went negative, Stripe suspended its payouts, and refunds silently queued as pending — no error, no alert, discovered via a customer complaint.&lt;/p&gt;

&lt;p&gt;stripe-connect-reckon is a small read-only TypeScript library that watches connected accounts and reports issues with severity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NEGATIVE_BALANCE_RISK (per-currency)&lt;/li&gt;
&lt;li&gt;FAILED_PAYOUT&lt;/li&gt;
&lt;li&gt;UNRECONCILED_REFUND (vs. the state your app reports)&lt;/li&gt;
&lt;li&gt;EVENT_GAP (within Stripe's 30-day events window)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Design notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read-only by design: only calls list/retrieve endpoints, never writes. v0 takes no corrective action on purpose.&lt;/li&gt;
&lt;li&gt;Clean architecture: a pure core (no I/O, no stripe import) behind a thin read-only adapter, so detection is unit-tested with fixtures and zero network (47 tests).&lt;/li&gt;
&lt;li&gt;Not a sync engine — it works a level up: read, evaluate, report.
Pinned to stripe-node v22 / API version 2026-06-24.dahlia. Dual ESM+CJS, MIT.&lt;/li&gt;
&lt;li&gt;It's v0.1.0 and honest about its limits (two detectors need your app's state; the event window is 30 days; it never remediates). I'd love feedback on what other read-only signals are worth catching.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;npm: &lt;a href="https://www.npmjs.com/package/stripe-connect-reckon" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/stripe-connect-reckon&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/tonytonycoder11/stripe-connect-reckon" rel="noopener noreferrer"&gt;https://github.com/tonytonycoder11/stripe-connect-reckon&lt;/a&gt;&lt;/p&gt;

</description>
      <category>stripe</category>
      <category>stripeconnect</category>
      <category>typescript</category>
      <category>payments</category>
    </item>
  </channel>
</rss>
