<?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: Daniel</title>
    <description>The latest articles on DEV Community by Daniel (@dannyamah).</description>
    <link>https://dev.to/dannyamah</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%2F4070262%2Fccd05ec3-044d-474f-81ad-cc386a04cfea.png</url>
      <title>DEV Community: Daniel</title>
      <link>https://dev.to/dannyamah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dannyamah"/>
    <language>en</language>
    <item>
      <title>Fifty Green Tests, Two Real Bugs, and the Oracle One `npm install` Away</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Mon, 31 Aug 2026 16:27:34 +0000</pubDate>
      <link>https://dev.to/dannyamah/fifty-green-tests-two-real-bugs-and-the-oracle-one-npm-install-away-4082</link>
      <guid>https://dev.to/dannyamah/fifty-green-tests-two-real-bugs-and-the-oracle-one-npm-install-away-4082</guid>
      <description>&lt;p&gt;A QR code cannot hide a bug from you, because checking it costs nothing. You point a phone at it. A URL pops up or it doesn't.&lt;/p&gt;

&lt;p&gt;That's why I picked a QR encoder for a hackathon whose entire rule is &lt;em&gt;standard library only&lt;/em&gt;: no packages, an empty dependency manifest, verified at submission. I rebuilt &lt;code&gt;qrcode&lt;/code&gt;, the library nearly every JavaScript project reaches for, in 844 lines of Node and an empty dependency manifest. By the end I had fifty tests, all green, and two bugs that not one of them could see. A phone found the first in four seconds, on day three, the first time I bothered to try. A tool one command away would have found both on the first afternoon. I just wouldn't run it, because running it felt like cheating.&lt;/p&gt;

&lt;p&gt;The encoder was the easy part. This is a write-up about the hard part, which was verification, which I also got wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;npm install qrcode&lt;/code&gt; actually installs
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;qrcode&lt;/code&gt; gets 24 million downloads a week. Before rebuilding it I looked at what it was.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm install qrcode&lt;/code&gt; writes 29 packages to disk, 2.4 MB. Three of them do the actual work: &lt;code&gt;qrcode&lt;/code&gt; itself, &lt;code&gt;pngjs&lt;/code&gt; for image output, and &lt;code&gt;dijkstrajs&lt;/code&gt;, a shortest-path graph algorithm it uses to choose the most compact way to split text across QR's encoding modes. The other 26 are &lt;code&gt;yargs&lt;/code&gt; and its dependency tree, there for a command-line interface: &lt;code&gt;cliui&lt;/code&gt;, &lt;code&gt;string-width&lt;/code&gt;, &lt;code&gt;wrap-ansi&lt;/code&gt;, &lt;code&gt;emoji-regex&lt;/code&gt;, &lt;code&gt;y18n&lt;/code&gt;, &lt;code&gt;camelcase&lt;/code&gt;, and down. They install on your server whether or not you ever run &lt;code&gt;qrcode&lt;/code&gt; from a terminal. Call &lt;code&gt;require('qrcode')&lt;/code&gt; and you have shipped an internationalized argument parser and everything under it.&lt;/p&gt;

&lt;p&gt;This isn't a complaint about &lt;code&gt;qrcode&lt;/code&gt;. It's that "one dependency" was a fiction. The number was 29, and I hadn't read a line of any of them, which is the normal condition of every Node service I've worked on: a few thousand packages of strangers' code, unread, running as me. The polite word is "dependency." The accurate word is borrowed trust. I trust these people because auditing them is more work than the feature is worth.&lt;/p&gt;

&lt;p&gt;The hackathon's premise is that we borrow far more than we need. So how much of the 29 is load-bearing?&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts you build, and the one you don't
&lt;/h2&gt;

&lt;p&gt;Most of it. The specification (ISO/IEC 18004) is a stack of small closed problems: Reed-Solomon error correction over GF(2⁸), which is two lookup tables and polynomial division; the module matrix with its finder squares and timing lines; eight mask patterns and a penalty function to choose one; fifteen and eighteen bits of metadata wrapped in BCH codes. None of it needs anything outside &lt;code&gt;std&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The one place I stopped was DEFLATE. PNG compresses its pixels with zlib, and a DEFLATE encoder is a hackathon project by itself. &lt;code&gt;node:zlib&lt;/code&gt; is standard library; I called &lt;code&gt;deflateSync&lt;/code&gt; and wrote the rest of the PNG by hand: signature, chunks, CRCs. That's the single asterisk on "zero dependencies." It's the opening line of my README and the last time I'll mention it, because it isn't where this goes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fifty green tests and a phone that said no
&lt;/h2&gt;

&lt;p&gt;By the end of day two I had a test suite I trusted. It wasn't lazy.&lt;/p&gt;

&lt;p&gt;Galois-field arithmetic, checked against the values printed in the standard. Reed-Solomon output, checked byte for byte against the standard's worked example: encode &lt;code&gt;"01234567"&lt;/code&gt; at version 1, level M, and Annex I gives you the exact codewords. The matrix, checked by an invariant: total modules minus fixed-pattern modules must equal the data-bit count, for every version. Format and version bits, checked against the standard's BCH tables. And a round-trip: a small decoder in the test file that walked the data path and pulled the bytes back, asserting &lt;code&gt;decode(encode(text)) === text&lt;/code&gt; for every text, every error level, every mask.&lt;/p&gt;

&lt;p&gt;Fifty tests. Green. I rendered &lt;code&gt;"01234567"&lt;/code&gt;, held up my phone, and got nothing. Bigger, more light, a printout, a different scanner app. Nothing scanned.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug a scanner reads first
&lt;/h2&gt;

&lt;p&gt;QR format information is fifteen bits (mask, error level, ten of BCH check) written twice for redundancy, one copy in an L around the top-left finder square. A scanner reads it before it reads anything else.&lt;/p&gt;

&lt;p&gt;This placed the first six:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&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;modules[row][col]&lt;/code&gt;. That runs along row 8. It should run down column 8:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Row and column, swapped, in both copies. Every bit a scanner needs to begin was on the wrong axis.&lt;/p&gt;

&lt;p&gt;The round-trip test didn't catch it because it never read the format bits. They're fixed-function modules, and the data-path walk skips them by definition. The payload round-tripped perfectly, and the payload was never where the bug was.&lt;/p&gt;

&lt;p&gt;The BCH test didn't catch it because it checked the value, not the address. My fifteen bits were the correct fifteen bits. I had verified what they were and assumed that told me where they went.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fifty tests, one blind spot
&lt;/h2&gt;

&lt;p&gt;The tests were not weak. They were the wrong shape.&lt;/p&gt;

&lt;p&gt;Every one was rigorous about computation: fields, polynomials, BCH. Computation is the part of a QR code that looks hard, and it's the part I'd worked to understand. None checked placement, because by the time I wrote tests, placement felt beneath checking. Put the bits where the figure shows them. I wasn't going to get that wrong.&lt;/p&gt;

&lt;p&gt;A test suite is built from its author's model of where the danger is. Mine faithfully encoded mine. The bug lived in the exact gap between "I checked this closely" and "this didn't need checking," and fifty green marks correctly reported that everything I'd thought to test was fine.&lt;/p&gt;

&lt;p&gt;That generalizes past QR codes. A passing suite is evidence about the parts you thought to check. Its silence is not evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The oracle I refused to install
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;qrcode&lt;/code&gt; was one &lt;code&gt;npm install&lt;/code&gt; away the whole time. I could have generated the same code with both, in a scratch directory that never touched my submission, and diffed the matrices. The transposed bits would have shown immediately.&lt;/p&gt;

&lt;p&gt;I didn't, because on a zero-dependency project, installing the thing I was replacing felt like cheating. Not against a rule (there is no such rule), but against the point. I was doing this properly. From the spec. Myself.&lt;/p&gt;

&lt;p&gt;That reflex is the same one the hackathon criticizes, pointed inward. "Don't blindly trust a stranger's package" and "don't blindly trust your own code" are one statement. Both kinds of trust need an outside check, and I had argued myself out of the best one on the table.&lt;/p&gt;

&lt;p&gt;Writing this, I finally ran the diff. The broken build differs from &lt;code&gt;qrcode&lt;/code&gt; on &lt;code&gt;"01234567"&lt;/code&gt; by twelve modules, all in the format region, all visible in the first comparison anyone would run. A phone in four seconds, or one &lt;code&gt;diff&lt;/code&gt; on day one. I took neither.&lt;/p&gt;

&lt;p&gt;The diff also found a second bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second bug, the one that scanned fine
&lt;/h2&gt;

&lt;p&gt;With the format bits fixed, my codes scanned, and I thought I was done. Then I diffed &lt;code&gt;qrkit&lt;/code&gt; against &lt;code&gt;qrcode&lt;/code&gt; properly: every byte-mode input I could think of, versions 1 through 10, all four error levels. 47 of 48 came back byte-identical. The one that didn't was the string &lt;code&gt;"zero dependency"&lt;/code&gt;: same version, same mask, same mode, 124 modules different.&lt;/p&gt;

&lt;p&gt;It was the padding. When your data doesn't fill the symbol, the spec tops it up with two alternating codewords, &lt;code&gt;0xEC&lt;/code&gt; and &lt;code&gt;0x11&lt;/code&gt;, and it says the first is always &lt;code&gt;0xEC&lt;/code&gt;. Mine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I picked the pad byte from its absolute position in the stream. When the real data ended on an even byte count the first pad came out &lt;code&gt;0xEC&lt;/code&gt;, correct by accident. When it ended odd, like &lt;code&gt;"zero dependency"&lt;/code&gt; does, the first pad came out &lt;code&gt;0x11&lt;/code&gt; and the entire tail inverted.&lt;/p&gt;

&lt;p&gt;This bug cannot break a scan. Pad codewords are discarded before the decoder reads your data. The round-trip test walked over these exact bytes on every run and could not see it, because "correct" meant "the text comes back," and the text always came back. Wrong, and fully functional. Only a byte comparison against an implementation I didn't write could surface it, and that was the comparison I'd spent three days avoiding.&lt;/p&gt;

&lt;p&gt;The fix counts the pad bytes instead of the stream bytes, plus a fifty-first test so it can't come back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The spec that disagrees with itself
&lt;/h2&gt;

&lt;p&gt;There's a third source of truth here, and it's also unreliable.&lt;/p&gt;

&lt;p&gt;Mask selection is meant to be deterministic: apply eight masks, score each, keep the lowest. But the rule that penalizes finder-lookalike patterns is loose enough that implementations disagree on what counts. &lt;code&gt;qrkit&lt;/code&gt;, whose scoring I ported from &lt;a href="https://www.nayuki.io/page/qr-code-generator-library" rel="noopener noreferrer"&gt;Nayuki's&lt;/a&gt; reference implementation, and &lt;code&gt;qrcode&lt;/code&gt;, which rolls its own, choose different masks on three of every eight inputs I tested. Both scan.&lt;/p&gt;

&lt;p&gt;And a 2008 note by Johan Persson works through the standard's own example, the &lt;code&gt;"01234567"&lt;/code&gt; I'd been testing against, and shows the mask it picks is not the lowest-scoring one by the standard's own rules. The reference fails the reference.&lt;/p&gt;

&lt;p&gt;So the spec is an oracle you cross-check too. There's no document that hands you the answer. There's a quorum: the spec, its examples, other implementations, a physical scanner. Where they disagree, you decide. I lost an evening treating my mask disagreement with &lt;code&gt;qrcode&lt;/code&gt; as a defect before I understood that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scorecard, told honestly
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm install qrcode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;29 packages, 2.4 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;yargs&lt;/code&gt; and its subtree&lt;/td&gt;
&lt;td&gt;26 of the 29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;qrkit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 dependencies, one 844-line file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Format matrix vs &lt;code&gt;qrcode&lt;/code&gt;, all 4 EC levels by 8 masks&lt;/td&gt;
&lt;td&gt;32 / 32 byte-identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Byte-mode output vs &lt;code&gt;qrcode&lt;/code&gt;, versions 1 to 10 by 4 EC levels, after the fix&lt;/td&gt;
&lt;td&gt;48 / 48 byte-identical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mask choice matching &lt;code&gt;qrcode&lt;/code&gt; when both choose freely&lt;/td&gt;
&lt;td&gt;5 / 8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encode speed, &lt;code&gt;qrkit&lt;/code&gt; vs &lt;code&gt;qrcode&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;677 µs vs 271 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bugs shipped&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;found by 50 unit tests&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;found by a phone&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;found by a diff available on day one&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The speed number held at 2.5x across four runs. That's not measurement noise, &lt;code&gt;qrkit&lt;/code&gt; is genuinely slower. It re-scores all eight masks with the full penalty function on every build; &lt;code&gt;qrcode&lt;/code&gt; has had years to not do that. The value here is the third row of the table, not the seventh.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I didn't do
&lt;/h2&gt;

&lt;p&gt;I wrote this list before I wrote the README, because the failure mode of a solo project is finding out your limits were load-bearing.&lt;/p&gt;

&lt;p&gt;Byte and numeric encoding modes only, no alphanumeric, no kanji, so an all-caps string that &lt;code&gt;qrcode&lt;/code&gt; packs tighter comes out larger from &lt;code&gt;qrkit&lt;/code&gt;. Versions 1 through 10, not 40. No ECI, no structured append. No decoder: reading a code back out of a photo is thresholding, perspective correction, and error location, and it's the same large, fragile surface that would have sunk the project if I'd tried it in a weekend.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone building one of these
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Reach for the oracle you didn't write first, before the first unit test. A phone, a reference implementation, a printed example. If you're rebuilding X, &lt;code&gt;npm install X&lt;/code&gt; in a scratch directory and diff. That's the control group.&lt;/li&gt;
&lt;li&gt;Treat a green suite as a map of the ground you thought to cover. Ask what class of bug it structurally cannot see.&lt;/li&gt;
&lt;li&gt;"Different function" is not "independent implementation." An encoder and a decoder written by one person from one reading of one spec agree with each other, not with the world.&lt;/li&gt;
&lt;li&gt;Some bugs never fail a functional test, because the function still works. Conformance is a separate question, and only a foreign implementation answers it.&lt;/li&gt;
&lt;li&gt;Write down what you didn't do.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where trust actually comes from
&lt;/h2&gt;

&lt;p&gt;I expected the lesson to be about dependencies, that I'd finish having shown you don't need the tree.&lt;/p&gt;

&lt;p&gt;That isn't it. Deleting the dependencies bought me nothing; an empty manifest is just a different set of things to be wrong about. Writing it myself bought me less than I thought, because every check I built came from the same head as the bug. My test file is 769 lines against 844 of source. I spent nearly as long proving the thing as building it, and I still shipped two defects, because the proof and the code had the same author and the same blind spot.&lt;/p&gt;

&lt;p&gt;The bugs died when the code met something with no stake in whether I was right: a phone camera, a committee's standard, a stranger's hand arithmetic from 2008, and the library I was too proud to install.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;ISO/IEC 18004, QR Code bar code symbology specification. Current revision 2024; the &lt;code&gt;01234567&lt;/code&gt; worked example and codeword tables cited here are in Annex I of the 2015 revision. Paid standard.&lt;/li&gt;
&lt;li&gt;Johan Persson, "A note on minor errors in the International QR Barcode standard" (2008). Source for the claim that the standard's own worked example does not select the lowest-penalty mask under its own rules, and for the hand-computed matrix &lt;code&gt;qrkit&lt;/code&gt; is tested against. &lt;a href="https://www.coastalmonitoring.org/resources/jpclass/QR/qr-comment.pdf" rel="noopener noreferrer"&gt;https://www.coastalmonitoring.org/resources/jpclass/QR/qr-comment.pdf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Project Nayuki, "QR Code generator library." The reference implementation &lt;code&gt;qrkit&lt;/code&gt;'s mask-penalty scoring follows. &lt;a href="https://www.nayuki.io/page/qr-code-generator-library" rel="noopener noreferrer"&gt;https://www.nayuki.io/page/qr-code-generator-library&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;qrcode&lt;/code&gt; (npm), MIT. The package &lt;code&gt;qrkit&lt;/code&gt; replaces and is diffed against. &lt;a href="https://github.com/soldair/node-qrcode" rel="noopener noreferrer"&gt;https://github.com/soldair/node-qrcode&lt;/a&gt;. Download figure for the week of 23 to 29 August 2026, from &lt;a href="https://www.npmjs.com/package/qrcode" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/qrcode&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;qrkit&lt;/code&gt;, MIT. &lt;a href="https://github.com/Dannyamah/qrkit" rel="noopener noreferrer"&gt;https://github.com/Dannyamah/qrkit&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built for the Zero Dependency Hackathon by Hackathon Raptors.&lt;/em&gt; &lt;code&gt;#hackathonraptors&lt;/code&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>programming</category>
      <category>testing</category>
    </item>
    <item>
      <title>140 Bugs Were Hiding in One Function, and My Tests Couldn't See Any of Them</title>
      <dc:creator>Daniel</dc:creator>
      <pubDate>Mon, 10 Aug 2026 09:57:25 +0000</pubDate>
      <link>https://dev.to/dannyamah/140-bugs-were-hiding-in-one-function-and-my-tests-couldnt-see-any-of-them-a0p</link>
      <guid>https://dev.to/dannyamah/140-bugs-were-hiding-in-one-function-and-my-tests-couldnt-see-any-of-them-a0p</guid>
      <description>&lt;p&gt;Anyone can port a library. Point a translator at the source, clean up the output, get it to compile, and you have something that looks like a port. The actual engineering problem is different and much harder: proving that the new code means the same thing as the old code, across thirty algorithms, hundreds of edge cases, and a test suite written by people who were not thinking about you.&lt;/p&gt;

&lt;p&gt;This is the story of porting &lt;a href="https://github.com/life4/textdistance" rel="noopener noreferrer"&gt;textdistance&lt;/a&gt;, a Python library for measuring string similarity, to Rust. The result is &lt;a href="https://github.com/Len3hq/textdistance-rs" rel="noopener noreferrer"&gt;textdistance-rs&lt;/a&gt;. The porting took a fraction of the time. Everything else: the differential fuzzing, the 140 divergences, the 35 year old threshold I violated, the floating-point drift at the 15th decimal place, is what this writeup is actually about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thirty algorithms and one architectural bet
&lt;/h2&gt;

&lt;p&gt;The original &lt;code&gt;textdistance&lt;/code&gt; covers a lot of ground: edit-based distances (Levenshtein, Damerau-Levenshtein, Hamming, Jaro-Winkler), token-based measures (Jaccard, Sørensen-Dice, cosine, Tversky), sequence-based methods (LCS, Ratcliff-Obershelp), phonetic algorithms (MRA, Editex), and compression-based distances built on normalized compression distance. Over thirty algorithms in total, all reimplemented in Rust.&lt;/p&gt;

&lt;p&gt;But before writing a single algorithm, I had to make the decision that shaped everything downstream: how does the existing Python test suite (397 tests I did not write) talk to the Rust code?&lt;/p&gt;

&lt;p&gt;The obvious answer is PyO3: wrap every algorithm in a &lt;code&gt;#[pyclass]&lt;/code&gt;, build a native extension, and the Python tests import Rust directly. The answer I chose instead was a subprocess CLI. The Rust core is a standalone binary that speaks JSON over stdin/stdout, and a thin Python adapter shells out to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The entire cross-language surface is one struct.&lt;/span&gt;
&lt;span class="nd"&gt;#[derive(Deserialize)]&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;qval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;external&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.algorithm&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"hamming"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;hamming&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"levenshtein"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;levenshtein&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;distance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s"&gt;"jaro_winkler"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;jaro&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;winkler_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="py"&gt;.s2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;// …thirty more arms&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"unknown algorithm"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reasoning: PyO3 binds you to Python's ABI, complicates the build for anyone who just wants the Rust crate, and turns every signature mismatch into a compile-and-link problem. A JSON pipe is crude, but it is debuggable with your eyes. You can replay any failing case by piping a JSON string into the binary from your shell. When the goal is verifying behavioral equivalence against a foreign test suite, that inspectability is worth a lot.&lt;/p&gt;

&lt;p&gt;It was a speed decision, and I knew it. What I did not know was that this bet had quietly closed a door I would walk straight into later on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that was older than the port
&lt;/h2&gt;

&lt;p&gt;The single worst debugging session of the project traced back to Jaro-Winkler, and specifically to a paper published in 1990.&lt;/p&gt;

&lt;p&gt;Jaro similarity comes from Matthew Jaro's 1989 work on record linkage for census data. William Winkler's 1990 refinement adds a bonus for strings that share a common prefix, on the empirical observation that clerical typos rarely occur in the first few characters of a name. My first implementation applied that prefix boost the way the formula usually gets quoted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What I wrote first: boost applied unconditionally.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;jaro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jaro_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;jaro&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix_len&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;jaro&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks right. It passes the examples people paste into blog posts. And it is wrong, because Winkler's actual method only applies the boost when the base Jaro score exceeds a threshold of 0.7 in his published work. Below that, the strings are too dissimilar for a shared prefix to mean anything, and boosting them inflates scores for garbage matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What Winkler actually specified in 1990.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;jaro&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;jaro_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&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;jaro&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;BOOST_THRESHOLD&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 0.7&lt;/span&gt;
    &lt;span class="n"&gt;jaro&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix_len&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;jaro&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;jaro&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combined with a second, smaller mistake: the original library treats two empty strings as perfectly similar (1.0), and my port returned 0.0. This one function accounted for 140 divergences in fuzz testing. One commit fixed all of them.&lt;/p&gt;

&lt;p&gt;The lesson that stuck with me: I was not diverging from the Python library. I was diverging from a 35-year-old paper that the Python library had implemented faithfully and I had implemented from memory. When you port a library, you inherit its citations, not just its code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof by ten thousand disagreements
&lt;/h2&gt;

&lt;p&gt;That raises the obvious question: how did I even know there were 140 divergences? The unit tests for Jaro-Winkler passed. Example-based tests check the inputs somebody thought of, and nobody thinks of &lt;code&gt;"ab"&lt;/code&gt; vs &lt;code&gt;"aaab"&lt;/code&gt; with a coincidental prefix and a sub-threshold Jaro score.&lt;/p&gt;

&lt;p&gt;The answer was differential fuzzing — running both implementations on the same randomized inputs and diffing the outputs. The technique goes back to McKeeman's work on differential testing for compilers: when you have two implementations that claim to compute the same function, disagreement between them is a test oracle you get for free. You don't need to know the correct answer; you only need to know they should match.&lt;/p&gt;

&lt;p&gt;The harness is embarrassingly simple, which is the point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;hypothesis&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;given&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strategies&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;textdistance&lt;/span&gt;          &lt;span class="c1"&gt;# the Python original
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;textdistance_rs&lt;/span&gt;       &lt;span class="c1"&gt;# the port, via the JSON pipe
&lt;/span&gt;
&lt;span class="nd"&gt;@given&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_jaro_winkler_parity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textdistance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jaro_winkler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textdistance_rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;jaro_winkler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hypothesis, Python's property-based testing library, descended from Claessen and Hughes' QuickCheck, generates the adversarial inputs: empty strings, single characters, Unicode combining marks, strings that are prefixes of each other. Every property-based failure gets shrunk to a minimal reproducing case, which is how a six-hour bug hunt eventually collapsed into a two-character counterexample I could reason about on paper.&lt;/p&gt;

&lt;p&gt;After the Jaro-Winkler fix and a handful of smaller ones, the fuzzer reached zero divergences across every algorithm and input class it could generate. That number, not the passing unit tests, is the claim I actually stand behind. But notice the caveat hiding in that sentence: zero divergences on everything that could cross the pipe. Two things could not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The door I closed on myself
&lt;/h2&gt;

&lt;p&gt;Several of &lt;code&gt;textdistance&lt;/code&gt;'s alignment algorithms — Needleman-Wunsch, Smith-Waterman, Gotoh — accept a &lt;code&gt;sim_func&lt;/code&gt; parameter: an arbitrary Python callable that scores the similarity of two characters. The test suite exercises it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# From the original test suite. This cannot be serialized.
&lt;/span&gt;&lt;span class="n"&gt;alg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;NeedlemanWunsch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sim_func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lambda cannot be serialized into JSON and sent down a pipe to a Rust subprocess. It is a closure over a live Python interpreter. There is no workaround that isn't a horror, no eval-strings-in-Rust, no RPC-callback-per-character scheme that would be slower than just running the Python.&lt;/p&gt;

&lt;p&gt;So those tests fail. 22 of them, permanently, by architecture rather than by bug. This is the direct, foreseeable cost of the subprocess decision from the beginning of this article. PyO3 would have handled it natively: a &lt;code&gt;#[pyfunction]&lt;/code&gt; can hold a reference to a Python callable and invoke it from Rust mid-algorithm. I traded that capability for build simplicity and debuggability, and for 28 algorithms the trade was clearly right. For these 4, I built a wall and then walked into it.&lt;/p&gt;

&lt;p&gt;If I ran this project again, I would keep the subprocess architecture for development and verification, it earned it's keep during fuzzing, and add a PyO3 layer at the end specifically for callable-taking algorithms. That hybrid was always possible. I just didn't see the need until the tests told me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 15th decimal place
&lt;/h2&gt;

&lt;p&gt;The other place the port compromised was subtler, and it's the result I'm least happy reporting: the "pure Rust" story does not fully hold for the compression-based algorithms.&lt;/p&gt;

&lt;p&gt;Normalized compression distance, from Cilibrasi and Vitányi's work on clustering by compression, approximates the information distance between strings using a real compressor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NCD(x, y) = (C(xy) − min(C(x), C(y))) / max(C(x), C(y))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By construction, the output is exquisitely sensitive to the compressor's exact byte counts and the arithmetic done on them.&lt;/p&gt;

&lt;p&gt;The original library computes arithmetic-coding probabilities using Python's &lt;code&gt;Fraction&lt;/code&gt;, exact rational arithmetic, no rounding. My Rust implementation used &lt;code&gt;f64&lt;/code&gt;. The fuzzer caught divergences on the order of 1e-15: not wrong in any way a human would notice, but not equal, and for NCD "close" is doing real work in the formula, because those values get subtracted and divided in ways that can amplify the drift.&lt;/p&gt;

&lt;p&gt;I tried rational arithmetic in Rust. I tried reordering operations to match Python's evaluation order. What shipped is the honest defeat: for the arithmetic-coding NCD path, the final distance computation moved back into the Python adapter, where &lt;code&gt;Fraction&lt;/code&gt; guarantees exact parity. Rust still does the heavy lifting; Python does the last arithmetic mile. Eight algorithms carry that asterisk, and I'd rather document the asterisk than pretend it isn't there.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the port actually proved
&lt;/h2&gt;

&lt;p&gt;None of these four things — the JSON pipe, the Winkler threshold, the fuzzing harness, the &lt;code&gt;sim_func&lt;/code&gt; wall were about writing Rust. They were about the much slower work of finding every place a fast, plausible looking reimplementation quietly disagrees with the thing it's replacing, and then deciding, for each one, whether to fix it, work around it, or write it down and ship anyway.&lt;/p&gt;

&lt;p&gt;That's the actual deliverable here, more than the crate itself: not thirty algorithms in Rust, but a documented account of where equivalence held, where it took a 35 year old paper to restore, and where it simply didn't. A port you can't interrogate isn't really verified, it's just translated. This one, at zero fuzz divergences and two known, named exceptions, is the former.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/life4/textdistance" rel="noopener noreferrer"&gt;&lt;code&gt;life4/textdistance&lt;/code&gt;&lt;/a&gt; — the original Python library.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/Len3hq/textdistance-rs" rel="noopener noreferrer"&gt;&lt;code&gt;Len3hq/textdistance-rs&lt;/code&gt;&lt;/a&gt; — the Rust port discussed throughout.&lt;/li&gt;
&lt;li&gt;Jaro, M. A. (1989). &lt;em&gt;Advances in record-linking methodology as applied to matching the 1985 census of Tampa, Florida.&lt;/em&gt; Journal of the American Statistical Association, 84(406), 414–420.&lt;/li&gt;
&lt;li&gt;Winkler, W. E. (1990). &lt;em&gt;String Comparator Metrics and Enhanced Decision Rules in the Fellegi-Sunter Model of Record Linkage.&lt;/em&gt; Proceedings of the Section on Survey Research Methods, American Statistical Association.&lt;/li&gt;
&lt;li&gt;McKeeman, W. M. (1998). &lt;em&gt;Differential Testing for Software.&lt;/em&gt; Digital Technical Journal, 10(1), 100–107.&lt;/li&gt;
&lt;li&gt;Claessen, K., &amp;amp; Hughes, J. (2000). &lt;em&gt;QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs.&lt;/em&gt; Proceedings of the Fifth ACM SIGPLAN International Conference on Functional Programming (ICFP '00).&lt;/li&gt;
&lt;li&gt;Cilibrasi, R., &amp;amp; Vitányi, P. M. B. (2005). &lt;em&gt;Clustering by Compression.&lt;/em&gt; IEEE Transactions on Information Theory, 51(4), 1523–1545.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full port and the DECISIONS log this writeup is pulled from is up at &lt;a href="https://github.com/Len3hq/textdistance-rs" rel="noopener noreferrer"&gt;Len3hq/textdistance-rs&lt;/a&gt; if you want to dig in yourself. &lt;br&gt;
Written for &lt;a href="https://x.com/raptors_hack" rel="noopener noreferrer"&gt;Hackathon Raptors&lt;/a&gt;, worth a follow if you're into this kind of writeup.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
