<?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: Jean-Luc Martel</title>
    <description>The latest articles on DEV Community by Jean-Luc Martel (@jlmartel).</description>
    <link>https://dev.to/jlmartel</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%2F4061410%2Fc407a2f0-c9db-4796-bcf2-43f744536ee6.jpg</url>
      <title>DEV Community: Jean-Luc Martel</title>
      <link>https://dev.to/jlmartel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jlmartel"/>
    <language>en</language>
    <item>
      <title>PHP: 30 Years of Making Developers Say "Wait, What?" (Part 2: It Gets Worse)</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/php-30-years-of-making-developers-say-wait-what-part-2-it-gets-worse-1jk8</link>
      <guid>https://dev.to/jlmartel/php-30-years-of-making-developers-say-wait-what-part-2-it-gets-worse-1jk8</guid>
      <description>&lt;p&gt;Apparently, three examples of PHP weirdness weren't enough.&lt;/p&gt;

&lt;p&gt;In the original post, we explored how PHP considers &lt;code&gt;"0"&lt;/code&gt; to be false, how array keys can silently overwrite one another, and how an innocent-looking &lt;code&gt;foreach&lt;/code&gt; loop can unexpectedly modify your data.&lt;/p&gt;

&lt;p&gt;Those were fun.&lt;/p&gt;

&lt;p&gt;But PHP has been around for three decades, and you don't accumulate that much history without developing a few interesting personality traits.&lt;/p&gt;

&lt;p&gt;So here are four more examples of PHP doing exactly what it was designed to do, even when that seems deeply unreasonable.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Filtering an Array Can Turn It Into a JSON Object
&lt;/h2&gt;

&lt;p&gt;Let's say you're building an API.&lt;/p&gt;

&lt;p&gt;You have an array of fruit. You want to remove one item and return the rest as JSON.&lt;/p&gt;

&lt;p&gt;Simple enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'apple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'banana'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cherry'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'banana'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filtered&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What do you expect?&lt;/p&gt;

&lt;p&gt;Probably this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you actually get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait. What?&lt;/p&gt;

&lt;p&gt;You started with an array. You filtered an array. You encoded an array.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And somehow, you got a JSON object.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's why.&lt;/p&gt;

&lt;p&gt;PHP's &lt;code&gt;array_filter()&lt;/code&gt; preserves the original keys.&lt;/p&gt;

&lt;p&gt;After filtering, the array looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'apple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'cherry'&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;json_encode()&lt;/code&gt; encounters a PHP array whose keys aren't consecutive integers starting at zero, it serializes it as a JSON object rather than a JSON array.&lt;/p&gt;

&lt;p&gt;Your perfectly reasonable filtering operation has silently changed the shape of your API response.&lt;/p&gt;

&lt;p&gt;Imagine the delight of the frontend developer expecting an array.&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filtered&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which gives you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; If you're filtering arrays for JSON responses, remember that &lt;code&gt;array_filter()&lt;/code&gt; preserves keys. Use &lt;code&gt;array_values()&lt;/code&gt; when you need a proper JSON array.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A Variable Can Exist and Still Not Be Set
&lt;/h2&gt;

&lt;p&gt;Let's talk about &lt;code&gt;isset()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The name seems reasonably self-explanatory.&lt;/p&gt;

&lt;p&gt;Surely it tells you whether something exists?&lt;/p&gt;

&lt;p&gt;Well, not exactly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bool(false)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the key is right there!&lt;/p&gt;

&lt;p&gt;You can see it. I can see it. PHP can presumably see it.&lt;/p&gt;

&lt;p&gt;And yet &lt;code&gt;isset()&lt;/code&gt; returns false.&lt;/p&gt;

&lt;p&gt;That's because &lt;code&gt;isset()&lt;/code&gt; doesn't actually mean "does this variable or key exist?"&lt;/p&gt;

&lt;p&gt;It means something closer to &lt;strong&gt;"does this exist AND contain a value other than null?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So we have two functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;                &lt;span class="c1"&gt;// false&lt;/span&gt;

&lt;span class="nb"&gt;array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same array. Same key. Different answers.&lt;/p&gt;

&lt;p&gt;And there's a practical consequence.&lt;/p&gt;

&lt;p&gt;Suppose &lt;code&gt;null&lt;/code&gt; means "disable the timeout," while a missing key means "use the default timeout."&lt;/p&gt;

&lt;p&gt;You might write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the null coalescing operator also falls back when the value is &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;$timeout&lt;/code&gt; becomes &lt;code&gt;30&lt;/code&gt;, even though the configuration explicitly contained &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If those two states matter, you need to distinguish them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now an explicitly configured &lt;code&gt;null&lt;/code&gt; stays &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; In PHP, "missing" and "present but null" are different states, but &lt;code&gt;isset()&lt;/code&gt; treats them the same. Sometimes that's convenient. Sometimes it's a bug waiting for the right configuration file.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Two Different Strings Can Be Equal Because They're Both Zero
&lt;/h2&gt;

&lt;p&gt;Here's a fun one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"0e123"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"0e999"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What would you expect?&lt;/p&gt;

&lt;p&gt;They're clearly different strings.&lt;/p&gt;

&lt;p&gt;Surely the answer is false.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bool(true)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course it is.&lt;/p&gt;

&lt;p&gt;The explanation involves PHP's numeric-string handling.&lt;/p&gt;

&lt;p&gt;Both strings are valid representations of numbers in scientific notation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;0e123&lt;/code&gt; means zero multiplied by ten to the power of 123.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;0e999&lt;/code&gt; means zero multiplied by ten to the power of 999.&lt;/p&gt;

&lt;p&gt;Both evaluate numerically to zero.&lt;/p&gt;

&lt;p&gt;And because PHP's loose comparison operator (&lt;code&gt;==&lt;/code&gt;) recognizes both strings as numeric, it compares their numeric values.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s2"&gt;"0e123"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"0e999"&lt;/span&gt;    &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="s2"&gt;"0e123"&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"0e999"&lt;/span&gt;   &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This particular behavior has a wonderful nickname: &lt;strong&gt;magic hashes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Certain hash outputs happen to consist entirely of a zero, followed by &lt;code&gt;e&lt;/code&gt;, followed by digits.&lt;/p&gt;

&lt;p&gt;When two such hashes are compared using loose equality, PHP can interpret them as equal numeric values even though their string contents differ.&lt;/p&gt;

&lt;p&gt;Historically, this has contributed to security vulnerabilities in applications that compared hash strings incorrectly.&lt;/p&gt;

&lt;p&gt;And no, switching from PHP 7 to PHP 8 doesn't eliminate this particular issue. Both operands here are valid numeric strings, so the loose comparison still considers them equal.&lt;/p&gt;

&lt;p&gt;The fix is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s2"&gt;"0e123"&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"0e999"&lt;/span&gt;  &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For security-sensitive hash comparisons, use the appropriate cryptographic comparison functions, such as &lt;code&gt;hash_equals()&lt;/code&gt; when comparing authentication-related strings, or &lt;code&gt;password_verify()&lt;/code&gt; for password hashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Loose equality isn't merely a convenience feature. When values originate from external input, it can become a security problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. &lt;code&gt;switch&lt;/code&gt; and &lt;code&gt;match&lt;/code&gt; Disagree About Reality
&lt;/h2&gt;

&lt;p&gt;PHP 8 introduced &lt;code&gt;match&lt;/code&gt;, a more modern alternative to &lt;code&gt;switch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And naturally, they don't quite agree on what constitutes a match.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Integer ten"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"String ten"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;break&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;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Integer ten
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value is a string.&lt;/p&gt;

&lt;p&gt;The first case is an integer.&lt;/p&gt;

&lt;p&gt;But PHP's &lt;code&gt;switch&lt;/code&gt; uses loose comparisons, so &lt;code&gt;"10"&lt;/code&gt; matches &lt;code&gt;10&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first matching case wins.&lt;/p&gt;

&lt;p&gt;Now let's rewrite that using &lt;code&gt;match&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mi"&gt;10&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Integer ten"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"10"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"String ten"&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;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;String ten
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same input. Same alternatives. Different result.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;match&lt;/code&gt; uses strict comparisons (&lt;code&gt;===&lt;/code&gt;), while &lt;code&gt;switch&lt;/code&gt; uses loose comparisons (&lt;code&gt;==&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;match&lt;/code&gt; has some other differences, including no fall-through and a requirement to handle unmatched values or provide a default.&lt;/p&gt;

&lt;p&gt;To be fair, &lt;code&gt;match&lt;/code&gt; is arguably the better-designed construct.&lt;/p&gt;

&lt;p&gt;But imagine converting an old &lt;code&gt;switch&lt;/code&gt; statement to &lt;code&gt;match&lt;/code&gt; during a refactoring session, assuming that you're just modernizing the syntax.&lt;/p&gt;

&lt;p&gt;You could change application behavior without changing any of the values being compared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; When replacing &lt;code&gt;switch&lt;/code&gt; with &lt;code&gt;match&lt;/code&gt;, you're not merely changing syntax. You're changing comparison semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHP Is Consistent. That's Not Always Comforting.
&lt;/h2&gt;

&lt;p&gt;Here's the interesting thing about all seven examples we've now explored across these two articles.&lt;/p&gt;

&lt;p&gt;None of them are random.&lt;/p&gt;

&lt;p&gt;PHP has rules governing truthiness, type coercion, array keys, references, JSON serialization, and comparisons.&lt;/p&gt;

&lt;p&gt;The interpreter follows those rules consistently.&lt;/p&gt;

&lt;p&gt;The problem is that those rules don't always line up with what developers intuitively expect.&lt;/p&gt;

&lt;p&gt;And sometimes two individually reasonable rules interact to produce something spectacularly unreasonable.&lt;/p&gt;

&lt;p&gt;That's what makes PHP such a fascinating language to revisit.&lt;/p&gt;

&lt;p&gt;It has evolved a lot since the 1990s. Modern PHP is faster, more expressive, and substantially better engineered than the language many developers remember.&lt;/p&gt;

&lt;p&gt;But it still has a few surprises hiding behind perfectly ordinary syntax.&lt;/p&gt;

&lt;p&gt;And after thirty years, I suspect it's earned the right to be a little eccentric.&lt;/p&gt;

</description>
      <category>php</category>
      <category>programming</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>PHP: 30 Years of Making Developers Say "Wait, What?"</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Sun, 25 Oct 2026 01:57:59 +0000</pubDate>
      <link>https://dev.to/jlmartel/php-30-years-of-making-developers-say-wait-what-3flc</link>
      <guid>https://dev.to/jlmartel/php-30-years-of-making-developers-say-wait-what-3flc</guid>
      <description>&lt;p&gt;With PHP suddenly back in the spotlight, I figured this was a good time to revisit one of the web's most successful, most criticized, and occasionally most bewildering programming languages.&lt;/p&gt;

&lt;p&gt;PHP has been powering websites for decades. It has evolved a lot, particularly through PHP 7 and 8, but it still carries some fascinating quirks.&lt;/p&gt;

&lt;p&gt;And I'm not talking about obscure syntax or features nobody uses. I'm talking about perfectly ordinary code that does something completely unexpected.&lt;/p&gt;

&lt;p&gt;Here are three of my favorites.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The String "0" Is False. The String "00" Is True.
&lt;/h2&gt;

&lt;p&gt;Let's start with something simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;"00"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isn't that beautiful?&lt;/p&gt;

&lt;p&gt;The string &lt;code&gt;"0"&lt;/code&gt; evaluates to false, but &lt;code&gt;"00"&lt;/code&gt; evaluates to true.&lt;/p&gt;

&lt;p&gt;Even the literal string &lt;code&gt;"false"&lt;/code&gt; evaluates to true.&lt;/p&gt;

&lt;p&gt;PHP considers an empty string and the specific string &lt;code&gt;"0"&lt;/code&gt; to be falsy. Other nonempty strings, including &lt;code&gt;"00"&lt;/code&gt;, are truthy.&lt;/p&gt;

&lt;p&gt;Now consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No password provided!"&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;Except a password &lt;strong&gt;was&lt;/strong&gt; provided. PHP just decided it didn't count.&lt;/p&gt;

&lt;p&gt;And if you're thinking that &lt;code&gt;empty()&lt;/code&gt; might save you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"00"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Don't use truthiness to determine whether a string contains meaningful input. Be explicit about what you're checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. PHP Arrays Can Silently Overwrite Your Data
&lt;/h2&gt;

&lt;p&gt;PHP arrays aren't really conventional arrays. They're ordered maps that support both integer and string keys.&lt;/p&gt;

&lt;p&gt;Which sounds perfectly reasonable until you do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"8"&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"eight"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;8&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"EIGHT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"08"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"zero eight"&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might expect three entries.&lt;/p&gt;

&lt;p&gt;You get two.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Array
(
    [8] =&amp;gt; EIGHT
    [08] =&amp;gt; zero eight
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PHP automatically converts the string key &lt;code&gt;"8"&lt;/code&gt; to the integer &lt;code&gt;8&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the second entry overwrites the first.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;"08"&lt;/code&gt; stays a string because it isn't a valid decimal integer representation for this conversion.&lt;/p&gt;

&lt;p&gt;And it gets better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"1"&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"boolean"&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Array
(
    [1] =&amp;gt; boolean
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three apparently different keys. One surviving value.&lt;/p&gt;

&lt;p&gt;The first two collide because &lt;code&gt;"1"&lt;/code&gt; becomes integer &lt;code&gt;1&lt;/code&gt;. The third collides because &lt;code&gt;true&lt;/code&gt; also becomes integer &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; PHP array keys undergo implicit type conversion. If your application depends on distinguishing keys by type, you're in for an unpleasant surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A &lt;code&gt;foreach&lt;/code&gt; Loop Can Quietly Corrupt Your Array
&lt;/h2&gt;

&lt;p&gt;This is my favorite, because it's the kind of bug that can survive a code review.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="o"&gt;=&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Do some processing&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Do some more processing&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing particularly exotic here.&lt;/p&gt;

&lt;p&gt;The first loop iterates by reference. The second iterates normally.&lt;/p&gt;

&lt;p&gt;What does the final array contain?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Array
(
    [0] =&amp;gt; 1
    [1] =&amp;gt; 2
    [2] =&amp;gt; 2
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait. What happened to the 3?&lt;/p&gt;

&lt;p&gt;The culprit is that innocent-looking ampersand in the first loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When that loop finishes, &lt;code&gt;$number&lt;/code&gt; remains a reference to the &lt;strong&gt;last element of the array&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reference doesn't magically disappear when the loop ends.&lt;/p&gt;

&lt;p&gt;Then the second loop reuses &lt;code&gt;$number&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each iteration assigns another array value to a variable that's &lt;em&gt;still referencing the last element&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;By the time the second loop finishes, the last element has been overwritten with the second-to-last value.&lt;/p&gt;

&lt;p&gt;The truly delightful part?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This behavior is explicitly documented in the PHP manual.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Do some processing&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Break the reference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the fact that you need to remember to do this is what makes the behavior so interesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; References in PHP can outlive the control structures that introduced them. Reusing a variable after a by-reference loop can have side effects that are surprisingly difficult to spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, Is PHP Broken?
&lt;/h2&gt;

&lt;p&gt;Not really.&lt;/p&gt;

&lt;p&gt;All three behaviors follow documented rules. They're not random, and they're not bugs in the PHP interpreter.&lt;/p&gt;

&lt;p&gt;But they illustrate something interesting about programming languages.&lt;/p&gt;

&lt;p&gt;A language can be internally consistent while still producing results that surprise the people using it.&lt;/p&gt;

&lt;p&gt;PHP has spent decades balancing developer convenience, backward compatibility, and language evolution. Sometimes those priorities have produced genuinely useful features. Sometimes they've produced behavior that makes you stare at your screen for a few minutes.&lt;/p&gt;

&lt;p&gt;And that's part of what makes exploring programming languages so entertaining.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This was fun so I think I'll make it a series looking at the stranger corners of familiar languages.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>programming</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Migrating AKS workloads from AAD Pod Identity to Entra Workload ID</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Thu, 22 Oct 2026 02:49:22 +0000</pubDate>
      <link>https://dev.to/jlmartel/migrating-aks-workloads-from-aad-pod-identity-to-entra-workload-id-3bk8</link>
      <guid>https://dev.to/jlmartel/migrating-aks-workloads-from-aad-pod-identity-to-entra-workload-id-3bk8</guid>
      <description>&lt;p&gt;You're looking at an AKS cluster for a client. &lt;/p&gt;

&lt;p&gt;Somewhere in it, a pod is authenticating to Azure using AAD Pod Identity. It relies on AzureIdentity and AzureIdentityBinding custom resources. It depends on the Node Managed Identity (NMI) pods intercepting metadata requests on port 2579. &lt;/p&gt;

&lt;p&gt;It is deprecated. Microsoft pulled the plug a year ago. The NMI intercept is a tax on the cluster's CPU and a liability in the threat model. &lt;/p&gt;

&lt;p&gt;You need to move it to Entra Workload ID. It uses OIDC federation. It is cleaner. But cleaner does not mean easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inventory the plumbing
&lt;/h2&gt;

&lt;p&gt;Do not just generate new manifests and hope for the best. &lt;/p&gt;

&lt;p&gt;Inventory what you actually have. Every &lt;code&gt;AzureIdentityBinding&lt;/code&gt; is a dependency. Every &lt;code&gt;AzureIdentity&lt;/code&gt; is a managed identity mapped to a specific set of pod labels. &lt;/p&gt;

&lt;p&gt;Map each workload to its intended federated Kubernetes Service Account. If you do not know exactly what the old identity does, you will not know what permissions the new one needs. &lt;/p&gt;

&lt;p&gt;Treat the old bindings as a map of your actual access patterns, not just a list of resources to delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the exact strings
&lt;/h2&gt;

&lt;p&gt;Entra Workload ID does not guess. It matches strings. &lt;/p&gt;

&lt;p&gt;When you create the federated identity credential in Entra ID, the issuer is your AKS OIDC endpoint. The subject must be exactly &lt;code&gt;system:serviceaccount:&amp;lt;namespace&amp;gt;:&amp;lt;name&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Miss a colon. Add a trailing space. Use the wrong namespace. &lt;strong&gt;It will not give you a helpful error&lt;/strong&gt;. It will just give you a 401 Unauthorized.&lt;/p&gt;

&lt;p&gt;The pod also requires the label &lt;code&gt;azure.workload.identity/use: "true"&lt;/code&gt;. Without it, the Entra workload identity mutating webhook ignores the pod. No environment variables get injected. No service account token gets projected. The pod starts, but it cannot authenticate.&lt;/p&gt;

&lt;p&gt;Test the token exchange before you move production traffic. &lt;/p&gt;

&lt;p&gt;Exec into a debug pod. Request the token from the projected volume. Decode it. Check the aud (audience) and sub (subject) claims. Verify the exact string matches what you configured in Entra ID. &lt;/p&gt;

&lt;h2&gt;
  
  
  Stage the migration
&lt;/h2&gt;

&lt;p&gt;Do not do a cutover. Do a &lt;strong&gt;migration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Deploy the new service account and the federated identity credential. Update the workload definition to use the new service account and apply the required pod label. &lt;/p&gt;

&lt;p&gt;Watch the logs. Verify the token exchange succeeds. &lt;/p&gt;

&lt;p&gt;Once the new identity is serving traffic and passing health checks, kill the old &lt;code&gt;AzureIdentityBinding&lt;/code&gt; and &lt;code&gt;AzureIdentity&lt;/code&gt; resources. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not&lt;/strong&gt; leave the old plumbing in the cluster "just in case." Deprecated resources left behind become permanent attack surface. If you do not need it, delete it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove it works
&lt;/h2&gt;

&lt;p&gt;You are not done when the pod starts. You are done when you prove it can do its job.&lt;/p&gt;

&lt;p&gt;Check the Azure Activity Log. Look for the API calls. &lt;/p&gt;

&lt;p&gt;See the new managed identity making the requests. See the old one go silent. &lt;/p&gt;

&lt;p&gt;That is the only evidence that matters. The new identity reaches the required services with the intended permissions, and the old plumbing is gone.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>azure</category>
    </item>
    <item>
      <title>The Green Padlock That Almost Cost Me a Contract</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Mon, 19 Oct 2026 20:15:12 +0000</pubDate>
      <link>https://dev.to/jlmartel/the-green-padlock-that-almost-cost-me-a-contract-dgo</link>
      <guid>https://dev.to/jlmartel/the-green-padlock-that-almost-cost-me-a-contract-dgo</guid>
      <description>&lt;p&gt;Encryption is supposed to answer one question for the person sitting at the browser: &lt;em&gt;am I really talking to who I think I'm talking to?&lt;/em&gt; On a corporate network running TLS inspection, the honest answer is often "not necessarily" — and most of the people relying on that network have no idea. &lt;/p&gt;

&lt;p&gt;There are serious risks if the setup isn't done properly, but raising that flag can lead to uncomfortable discussions. I found that out the hard way, when demonstrating the problem nearly got my contract terminated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The client ran a TLS inspection appliance on their internal network — the kind that decrypts outbound HTTPS so it can be scanned for malware and data loss before being re-encrypted and sent on. These devices are common and, in principle, defensible: most malware command-and-control and data exfiltration now hides inside TLS, so without inspection, a lot of security tooling is blind.&lt;/p&gt;

&lt;p&gt;The way these appliances work is by man-in-the-middling every HTTPS connection. The appliance holds its own certificate authority. When you visit a site, it generates a certificate for that site on the fly, signs it with its own CA, and presents it to your browser. For this to work without warnings, that CA's root certificate is installed in the trust store of every managed machine — usually pushed automatically through group policy or device management.&lt;/p&gt;

&lt;p&gt;Once that root is trusted, the browser stops complaining. The certificates the appliance generates look completely valid. No warning. No broken padlock. Nothing to tell the user their "encrypted" session is being read in clear text in the middle.&lt;/p&gt;

&lt;p&gt;On top of that, the certificate issuance on this network was far more permissive than it should have been. The practical effect was that the internal CA could produce a trusted certificate for essentially any domain name — including public ones like google.com.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I wanted to show
&lt;/h2&gt;

&lt;p&gt;The thing I wanted the security team to see is subtle, and it's the part people consistently underestimate: &lt;strong&gt;once a root is trusted, the browser's special protections for high-value sites quietly stop applying.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Browsers do a lot to protect domains like google.com:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Certificate pinning&lt;/strong&gt; ties those domains to a specific set of public CAs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate Transparency&lt;/strong&gt; requires publicly trusted certificates to be logged in public, auditable logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HSTS&lt;/strong&gt; forces connections to use HTTPS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these makes an exception for locally installed roots. Pinning is skipped when the chain ends at an admin-installed CA. CT isn't required for private roots. HSTS is satisfied by any cert the browser considers valid. These exceptions exist &lt;em&gt;on purpose&lt;/em&gt; — they're what stops a legitimate inspection appliance from breaking Google for the whole company. But they also mean that anyone who can issue from the trusted internal CA inherits all of that stand-aside treatment.&lt;/p&gt;

&lt;p&gt;In other words: the protections you'd assume would catch an impersonated Google simply don't fire inside that environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demonstration
&lt;/h2&gt;

&lt;p&gt;To make this concrete rather than theoretical, I built a tiny proof of concept on my own machine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I created a web page that said, in plain text, &lt;strong&gt;"I'm a fake Google page."&lt;/strong&gt; No login form, no attempt to harvest anything — the content existed only to make the point unmistakable.&lt;/li&gt;
&lt;li&gt;I added an entry to my own machine's hosts file pointing google.com at the IP serving that page.&lt;/li&gt;
&lt;li&gt;I generated a certificate for google.com from the environment's trusted CA.&lt;/li&gt;
&lt;li&gt;I loaded google.com in the browser.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The page loaded as google.com, with a green padlock, no warnings, and a certificate the browser reported as valid.&lt;/p&gt;

&lt;p&gt;That was the whole point. The browser was not malfunctioning. It was doing exactly what the trust configuration told it to do. Every safeguard that should have flagged an impersonated Google had been designed to stand aside for a locally trusted root, and that's exactly what happened.&lt;/p&gt;

&lt;p&gt;The hosts file was the deliberately contained part. It only affected my own machine — nobody else could reach that page. In a real attack, the hosts file stands in for an attacker's network position: a poisoned entry on the internal DNS resolver, ARP spoofing on a flat LAN, a rogue DHCP server, or a tampered proxy configuration. On a lot of internal networks, getting into that position is a lower bar than people like to assume. The demo swapped that step out for a one-line, single-machine change so there was no collateral effect at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reaction
&lt;/h2&gt;

&lt;p&gt;I sent the example to the security team as part of a technical debate about the risks of their setup. Their response was not to engage with the finding. It was to try to have my contract terminated, on the grounds that I had been "phishing."&lt;/p&gt;

&lt;p&gt;I had not shared the fake page with anyone. There was no victim, no credential harvesting, no email, no deception of a single other person. The entire thing lived on one machine — mine — and demonstrated a property of &lt;em&gt;their&lt;/em&gt; infrastructure. Calling it phishing was a category error, and I think a revealing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the reaction matters more than the bug
&lt;/h2&gt;

&lt;p&gt;Phishing is the use of deception to trick a person into giving something up. A proof of concept that impersonates a site to no one but yourself, to demonstrate a flaw in a system you were engaged to assess, is the opposite of that: it's disclosure. The deception, such as it was, was aimed at a browser I controlled, to reveal something true about the environment.&lt;/p&gt;

&lt;p&gt;What the reaction demonstrated is a failure mode I've seen more than once: when a finding is uncomfortable, it's easier to attack the method than to sit with the conclusion. The conclusion here was genuinely uncomfortable. It wasn't "the appliance has a bug." It was "the way trust is configured on this network means the guarantees your users assume they have — that a green padlock on google.com means they're talking to Google — don't hold, and an insider with issuance rights can impersonate anything to anyone, invisibly."&lt;/p&gt;

&lt;p&gt;That's not a comfortable thing to hear about a system you own and defend. But shooting the person who showed it to you doesn't make it less true. It just guarantees the next person won't tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should have happened
&lt;/h2&gt;

&lt;p&gt;The appropriate response to that demo was a conversation about controls, not contracts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the interception CA separate from the internal issuing CA&lt;/strong&gt;, so a compromise of one doesn't hand over the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply name constraints&lt;/strong&gt; to internal roots so they can only sign the domains they're supposed to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock certificate profiles&lt;/strong&gt; so requested names have to match the requester's identity, instead of allowing arbitrary domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrict and audit issuance&lt;/strong&gt; — know who can request certificates and log everything that gets signed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect the CA key properly&lt;/strong&gt;, ideally in an HSM, because whoever holds it can impersonate anything to every managed device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be honest with users&lt;/strong&gt; that inspection is happening, and keep sensitive categories like banking and health out of scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is exotic. It's the standard hygiene for running TLS inspection without turning it into a single, silent point of total compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The green padlock is a promise, and TLS inspection quietly rewrites the terms of that promise for everyone on the network. That can be done responsibly. But the first step is being willing to look at what your own configuration actually allows — especially when someone hands you an uncomfortable demonstration of it on a single contained machine, and asks you to think about it.&lt;/p&gt;

&lt;p&gt;Attacking the messenger is a choice. It's just not a security control.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>ssl</category>
      <category>dns</category>
      <category>browser</category>
    </item>
    <item>
      <title>BigQuery BI Engine deserves a benchmark before you dismiss Looker Studio as slow</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Sun, 18 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/bigquery-bi-engine-deserves-a-benchmark-before-you-dismiss-looker-studio-as-slow-1go5</link>
      <guid>https://dev.to/jlmartel/bigquery-bi-engine-deserves-a-benchmark-before-you-dismiss-looker-studio-as-slow-1go5</guid>
      <description>&lt;p&gt;Someone is complaining that Looker Studio is slow.&lt;/p&gt;

&lt;p&gt;A dashboard takes forty seconds to load. &lt;/p&gt;

&lt;p&gt;The immediate reaction is to blame the tool. &lt;/p&gt;

&lt;p&gt;"Looker is too slow."&lt;br&gt;
"We need to migrate."&lt;br&gt;
"We need a custom frontend."&lt;/p&gt;

&lt;p&gt;Look at the query logs.&lt;/p&gt;

&lt;p&gt;The dashboard is running a full table scan on a 500GB fact table every time a user changes a date filter.&lt;/p&gt;

&lt;p&gt;Looker Studio isn't the bottleneck. The query path is.&lt;/p&gt;

&lt;p&gt;Blaming the visualization layer for a warehouse execution problem is like blaming the paint for a crumbling foundation.&lt;/p&gt;

&lt;p&gt;Before you throw money at BigQuery BI Engine, isolate the variables.&lt;/p&gt;

&lt;p&gt;Performance hypotheses usually mash three distinct things together.&lt;/p&gt;

&lt;p&gt;First, connector caching. Is the dashboard just failing to cache the extract?&lt;/p&gt;

&lt;p&gt;Second, query shape. Are you running a distinct count on a high-cardinality string?&lt;/p&gt;

&lt;p&gt;Third, BI Engine support. Does the query use supported data types and operations?&lt;/p&gt;

&lt;p&gt;BI Engine can't fix a query it doesn't support. &lt;/p&gt;

&lt;p&gt;If your query falls back to the main BigQuery engine, the BI Engine reservation sits idle while you pay for it.&lt;/p&gt;

&lt;p&gt;Don't guess. Benchmark.&lt;/p&gt;

&lt;p&gt;Take the five most-used, slowest queries from the dashboard.&lt;/p&gt;

&lt;p&gt;Run them without BI Engine. Record the latency. Record the bytes billed.&lt;/p&gt;

&lt;p&gt;Turn on a BI Engine reservation. Run the exact same queries. Record the latency. Record the bytes billed.&lt;/p&gt;

&lt;p&gt;Calculate the cost of the reservation against the savings in bytes billed and the value of the latency reduction.&lt;/p&gt;

&lt;p&gt;If the query isn't supported, latency won't drop. &lt;/p&gt;

&lt;p&gt;If the query is just badly shaped, latency might drop, but the cost won't justify the reservation.&lt;/p&gt;

&lt;p&gt;A reservation is a blunt instrument. It solves a specific class of problem and ignores everything else.&lt;/p&gt;

&lt;p&gt;Before you provision one, ask yourself: which exact metric will prove the bottleneck is in the compute layer, and not in the query design?&lt;/p&gt;

&lt;p&gt;If you can't answer that, you haven't fixed a performance problem.&lt;/p&gt;

&lt;p&gt;You've just bought a faster way to run a bad query.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>bigdata</category>
      <category>data</category>
    </item>
    <item>
      <title>Azure subscription Owner is an access-delegation privilege</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/azure-subscription-owner-is-an-access-delegation-privilege-4nid</link>
      <guid>https://dev.to/jlmartel/azure-subscription-owner-is-an-access-delegation-privilege-4nid</guid>
      <description>&lt;p&gt;You hand someone the Owner role on an Azure subscription. &lt;/p&gt;

&lt;p&gt;You think you're giving them the keys to manage resources. VMs, databases, networking. You think you've made them a builder. &lt;/p&gt;

&lt;p&gt;You look at the role name. "Owner." It sounds like they just own the stuff inside the box. &lt;/p&gt;

&lt;p&gt;They don't. They own the box. And they own the locks.&lt;/p&gt;

&lt;p&gt;It’s not tenant-wide administration. A subscription Owner isn't a Global Administrator. They can't rewrite the rules for the entire Azure tenant. They can't even remove protected deny assignments if someone locked a resource down properly.&lt;/p&gt;

&lt;p&gt;But they don't need to. &lt;/p&gt;

&lt;p&gt;Because within the boundary of that subscription, Owner is an access-delegation privilege. &lt;/p&gt;

&lt;p&gt;They can assign powerful data-access roles. They can hand out the keys to the data plane. They can change legacy Key Vault access policies. They can grant someone else the right to read the secrets, even if they themselves were never explicitly granted that data access. &lt;/p&gt;

&lt;p&gt;The control plane becomes a backdoor to the data plane.&lt;/p&gt;

&lt;p&gt;We treat subscription management and data access as two different conversations. &lt;/p&gt;

&lt;p&gt;They aren't. &lt;/p&gt;

&lt;p&gt;If you want to protect your data, you need to look at who holds the Owner role. Review who can assign those powerful data-access roles. Review who can quietly flip a legacy Key Vault access policy to let themselves in.&lt;/p&gt;

&lt;p&gt;When that Owner grant was approved, what was the justification? "Needs to manage the dev environment." "Needs to deploy the infrastructure."&lt;/p&gt;

&lt;p&gt;But was the grant approved with those delegation powers in view? &lt;/p&gt;

&lt;p&gt;If not, you haven't granted resource administration. &lt;/p&gt;

&lt;p&gt;You have granted data access. And you didn't even realize it.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>iam</category>
    </item>
    <item>
      <title>The data engineering reality of GNNs in production</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Thu, 08 Oct 2026 20:32:16 +0000</pubDate>
      <link>https://dev.to/jlmartel/the-data-engineering-reality-of-gnns-in-production-1n0j</link>
      <guid>https://dev.to/jlmartel/the-data-engineering-reality-of-gnns-in-production-1n0j</guid>
      <description>&lt;p&gt;Your offline GNN looks perfect. The AUC is high. The precision is great.&lt;/p&gt;

&lt;p&gt;You ship it to production to catch fraud or optimize a supply chain. &lt;/p&gt;

&lt;p&gt;Then the metrics tank.&lt;/p&gt;

&lt;p&gt;The model isn't broken. The data is just too old. &lt;/p&gt;

&lt;p&gt;It is making real-time decisions using neighborhoods and embeddings from yesterday, or last week. In fraud, a node's connections change in milliseconds. In supply chain, they change in hours. &lt;/p&gt;

&lt;p&gt;The model is evaluating a ghost.&lt;/p&gt;

&lt;p&gt;GNNs don't just need features. They need fresh ones. They need neighborhoods that reflect the current state of the world.&lt;/p&gt;

&lt;p&gt;This means your graph storage and feature-update pipelines cannot be an afterthought. They must be engineered to the exact freshness the use case demands. &lt;/p&gt;

&lt;p&gt;A batch job running at midnight is fine for predicting customer churn. It is catastrophic for blocking a fraudulent transaction at checkout. &lt;/p&gt;

&lt;p&gt;Data engineering must run on the same clock as the business decision.&lt;/p&gt;

&lt;p&gt;Don't trust your architecture diagram. Trust a trace.&lt;/p&gt;

&lt;p&gt;Pick one production decision and trace it backward. Start at the served model input. &lt;/p&gt;

&lt;p&gt;Look at the embedding it consumed. Look at the neighborhood it aggregated. &lt;/p&gt;

&lt;p&gt;Trace those back to the graph storage layer. Trace that back to the feature-update pipeline. Trace that back to the source system change. &lt;/p&gt;

&lt;p&gt;If a user changes their email, or a supplier misses a shipment, how long does it take for that reality to reach the model? &lt;/p&gt;

&lt;p&gt;If you can't measure the lag, you can't trust the prediction.&lt;/p&gt;

&lt;p&gt;Offline evaluation assumes the model has access to the exact state of the graph at the moment of the event. Production rarely offers that luxury.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>machinelearning</category>
      <category>mlops</category>
      <category>datascience</category>
    </item>
    <item>
      <title>A zero-disruption PodDisruptionBudget can block an AKS upgrade</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Thu, 08 Oct 2026 04:06:50 +0000</pubDate>
      <link>https://dev.to/jlmartel/a-zero-disruption-poddisruptionbudget-can-block-an-aks-upgrade-18h5</link>
      <guid>https://dev.to/jlmartel/a-zero-disruption-poddisruptionbudget-can-block-an-aks-upgrade-18h5</guid>
      <description>&lt;p&gt;Your AKS cluster upgrade is stuck.&lt;/p&gt;

&lt;p&gt;The control plane is waiting for a node to drain. The node is waiting for a pod to terminate. The pod is refusing to move.&lt;/p&gt;

&lt;p&gt;Look at the logs. The workload has a PodDisruptionBudget that allows exactly zero disruptions.&lt;/p&gt;

&lt;p&gt;It is doing exactly what it was told to do. Protect the application at all costs. Even if it means halting the cluster upgrade.&lt;/p&gt;

&lt;p&gt;To upgrade a node, AKS has to drain it. It cordons the node and evicts the pods.&lt;/p&gt;

&lt;p&gt;But the eviction process respects disruption budgets.&lt;/p&gt;

&lt;p&gt;If your deployment has three replicas, and your budget dictates that three must remain available, the eviction fails. The node stays tainted. The upgrade stalls.&lt;/p&gt;

&lt;p&gt;You didn't just configure high availability. You configured a deadlock.&lt;/p&gt;

&lt;p&gt;Replica headroom isn't just a best practice for scaling. It is the physical space required for the control plane to do maintenance.&lt;/p&gt;

&lt;p&gt;Before your next maintenance window, look at your math.&lt;/p&gt;

&lt;p&gt;Check your &lt;code&gt;minAvailable&lt;/code&gt; or &lt;code&gt;maxUnavailable&lt;/code&gt; settings against the actual number of running replicas. &lt;/p&gt;

&lt;p&gt;If you use &lt;code&gt;minAvailable&lt;/code&gt; with a percentage, remember that Kubernetes rounds up. A setting of 100% means zero disruptions allowed. A setting of 90% with three replicas rounds up to three.&lt;/p&gt;

&lt;p&gt;You have to leave room for the scheduler to breathe. Configure these values deliberately, not just to pass a compliance check.&lt;/p&gt;

&lt;p&gt;High availability means surviving a failure. It shouldn't mean blocking your own operations team.&lt;/p&gt;

&lt;p&gt;Your availability policy guarantees the app survives a zone failure. But has anyone tested whether it survives routine maintenance?&lt;/p&gt;

&lt;p&gt;If your zero-disruption configuration blocks your upgrade pipeline, you haven't built a resilient system.&lt;/p&gt;

&lt;p&gt;You have built a hostage situation.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>I rebuilt four dead systems with AI. The failures all landed in the same place.</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Thu, 24 Sep 2026 16:55:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/i-rebuilt-four-dead-systems-with-ai-the-failures-all-landed-in-the-same-place-38pa</link>
      <guid>https://dev.to/jlmartel/i-rebuilt-four-dead-systems-with-ai-the-failures-all-landed-in-the-same-place-38pa</guid>
      <description>&lt;p&gt;Four reconstructions, four kinds of missing documentation, and the defects landed in the same place every time.&lt;/p&gt;

&lt;p&gt;In HAL/S it was the characters the scanner destroyed. In the Nautilus simulator it was the parameters Verne never wrote down. In Wikicarta it was the desktop window no headless harness can open. In the compressor it was the part of the input space the corpus never sampled.&lt;/p&gt;

&lt;p&gt;Different projects, different languages, different failure symptoms. One shape: &lt;strong&gt;the defects clustered wherever no automated check could reach, and in every case the code looked fine.&lt;/strong&gt; Tests green, types satisfied, output plausible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually being varied
&lt;/h2&gt;

&lt;p&gt;These weren't four projects that happened to be about old software. They were one experiment with a controlled variable: &lt;em&gt;how degraded is the source of truth the model works from, and can it be caught being wrong?&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;arm&lt;/th&gt;
&lt;th&gt;source of truth&lt;/th&gt;
&lt;th&gt;oracle&lt;/th&gt;
&lt;th&gt;outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/singular-state/space-shuttle-sdk" rel="noopener noreferrer"&gt;HAL/S&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;degraded formal spec + preserved code&lt;/td&gt;
&lt;td&gt;strong, independent&lt;/td&gt;
&lt;td&gt;faithful reconstruction; 12/12 preserved programs run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/singular-state/nautilus-sim" rel="noopener noreferrer"&gt;Nautilus&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;a novel&lt;/td&gt;
&lt;td&gt;none — one was built&lt;/td&gt;
&lt;td&gt;5 findings; the legend fails on every axis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/singular-state/wikicarta" rel="noopener noreferrer"&gt;Wikicarta&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;memory of a product&lt;/td&gt;
&lt;td&gt;none — human judgement&lt;/td&gt;
&lt;td&gt;shippable app; every defect at the runtime seam&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/singular-state/compressor" rel="noopener noreferrer"&gt;compressor&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;behaviour only, key sealed&lt;/td&gt;
&lt;td&gt;strong, opened after freeze&lt;/td&gt;
&lt;td&gt;decoder 100%, encoder 8.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The intuition going in was that performance would degrade smoothly as the source got worse. It didn't. Something else predicted the results, and it cut across all four.&lt;/p&gt;

&lt;h2&gt;
  
  
  Determined beats hard
&lt;/h2&gt;

&lt;p&gt;The reconstruction quality tracked whether &lt;strong&gt;the available evidence determines the answer&lt;/strong&gt; — not how difficult the task was.&lt;/p&gt;

&lt;p&gt;The compressor makes this measurable, because both halves ran under identical conditions. The decoder recovered the bitstream format at 100%, because a format is one-way determined: if you want to decode the stream, exactly one answer works. The encoder scored 8.3% on the same format, because its heuristics — match-finder strategy, tie-breaking, greedy versus lazy — are &lt;em&gt;choices&lt;/em&gt; that leave no trace in the decompressed bytes. Same model family, same oracle, same week. A 12× gap, entirely explained by whether the evidence pinned the answer.&lt;/p&gt;

&lt;p&gt;The other three say the same thing in their own terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HAL/S.&lt;/strong&gt; The 485 grammar productions were determined by the page images, and the reconstruction got them. The characters the scan physically destroyed were determined by nothing, and no amount of model capability recovers them. That limit is information-theoretic. Better OCR narrows it and never closes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nautilus.&lt;/strong&gt; The physics is determined; the equations produce the same crush depth for anyone. The parameters Verne omitted are determined by nothing, so submerged endurance ranges across 6 to 211 hours — a 35× spread — and battery capacity across 100×. Those spreads aren't noise around a finding. They &lt;em&gt;are&lt;/em&gt; the finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wikicarta.&lt;/strong&gt; Parsing, backoff, cache naming and database ordering are determined by their inputs, and compiled correct on the first or second attempt, consistently. Whether the category wheel feels like Encarta is determined by nothing you can write down, and stayed a human call from start to finish.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical version: before handing a legacy task to a model, ask whether the evidence you have actually pins the answer. If it does, expect the model to find it and build a cheap mechanical check to confirm. If it doesn't, expect a confident, plausible, unmarked invention — and note that the model will not tell you which mode it's in, because from the inside they feel identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  A strong oracle is not the protection you think
&lt;/h2&gt;

&lt;p&gt;The compressor was built to demonstrate that a strong oracle masks confabulation. It mostly refuted that.&lt;/p&gt;

&lt;p&gt;The reconstruction's central error — assigning Huffman code lengths by symbol index rather than in heap-extraction order — failed the byte-match test loudly. The oracle caught it. What the oracle &lt;em&gt;did&lt;/em&gt; hide were two mechanisms the corpus never triggered: a block-splitting path that needs inputs larger than 8 KiB to fire, and a match-finder chain cap that needs input more self-similar than anything tested.&lt;/p&gt;

&lt;p&gt;So the masking risk is not a property of the oracle's strength. &lt;strong&gt;It's a property of the input space you sampled.&lt;/strong&gt; A strong checker over a narrow corpus hides precisely the mechanisms your corpus never exercises, and hides them silently, because everything it can see is green.&lt;/p&gt;

&lt;p&gt;Strengthening the checker does nothing for this. Widening the corpus does.&lt;/p&gt;

&lt;p&gt;That generalises past compression. It's the same failure as a test suite with excellent coverage of the paths anyone thought to write a test for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the human actually contributed
&lt;/h2&gt;

&lt;p&gt;I expected to spend supervision effort reviewing code. Across all four projects, almost none of it went there. The load-bearing human contributions were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HAL/S&lt;/strong&gt; — deciding &lt;em&gt;not&lt;/em&gt; to vote between OCR engines, and then reading 485 grammar productions against page images by eye.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nautilus&lt;/strong&gt; — signing the invariants and making them append-only, so the model could patch the physics but was structurally unable to weaken the checks that judge it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wikicarta&lt;/strong&gt; — being the runtime. Clicking the button in the real window, on the real WebKit build, where the blocking dialog freeze and the cross-realm &lt;code&gt;instanceof&lt;/code&gt; failure lived.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compressor&lt;/strong&gt; — holding the seal. Not reading the answer key until the reconstruction was frozen at a tag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two categories, and neither is code review. One is &lt;strong&gt;supplying what no code can check&lt;/strong&gt; — an eye on a page image, a hand on a mouse. The other is &lt;strong&gt;designing the checks that are possible&lt;/strong&gt; and then putting them structurally out of the model's reach.&lt;/p&gt;

&lt;p&gt;The OCR decision is the sharpest of the four, because it's a judgement call that separates a good result from a worthless one that looks identical. Running three OCR engines and taking the majority is the obvious move and it is wrong: the errors are correlated, so a vote launders error rather than cancelling it, and no fusion can recover a character that no engine produced. Survivable in prose, fatal in a grammar where the metacharacters are the content. Take the vote and you get a clean, plausible, silently corrupt grammar — and every downstream artifact inherits it.&lt;/p&gt;

&lt;p&gt;The Nautilus invariant signing is the most reusable. Twelve invariants live in version control with a manifest checksum only a human re-signs. When the fuzzer finds a violation, the repair workflow requires patching the core and forbids touching the invariant. An agent optimising against a check it can also edit is not being checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the oracle before the artifact
&lt;/h2&gt;

&lt;p&gt;Ranked by how much verification apparatus existed before the real work started, the four projects come out in the same order as how much I trust their conclusions.&lt;/p&gt;

&lt;p&gt;Nautilus had determinism, golden-stream replay and malformed-input fuzzing green in its check harness &lt;strong&gt;before a single equation was written&lt;/strong&gt;, plus a frozen protocol and a rule that every spec parameter traces to either a sourced claim or a documented gap range. Its numbers are the most defensible in the series. The compressor's seal, cold-recall protocol, freeze tag and grading harness are most of that repository; the reconstruction is the small part. HAL/S inherited an independent interpreter, which is a luxury and which is why it is the easiest arm.&lt;/p&gt;

&lt;p&gt;Wikicarta had none of this available, and it shows — its conclusions are qualitative, argued from patterns across features rather than measured. They're still the most immediately useful findings in the series, and they're the least provable. Both things are true because of the same missing oracle.&lt;/p&gt;

&lt;p&gt;There's a specific trap worth naming. A verification harness that finds nothing is indistinguishable from a broken one. Nautilus's 24-hour soak ran 879,643 ticks and found zero invariant violations, which is only meaningful because the harness was first validated against a deliberately poisoned invariant — which it caught, minimised from seven commands to two, and packaged. Test the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three results that went the wrong way
&lt;/h2&gt;

&lt;p&gt;A series that only reports confirmations is a demo reel. These four produced three clean refutations, and they were the most informative outputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The compressor predicted the classic overfitting curve — strong on trained data, weak on held-out. It came out inverted, 8% trained against 71% held-out, because the split tracked &lt;em&gt;difficulty&lt;/em&gt; rather than seen-versus-unseen.&lt;/li&gt;
&lt;li&gt;It also predicted the oracle would mask confabulation. The oracle caught the main error; a narrow corpus hid the rest.&lt;/li&gt;
&lt;li&gt;Nautilus's fuzzer was built to find physics exploits. It found none. The only finding in the whole loop was a flaw in the experiment design — the genetic algorithm had been optimising survival rather than performance, because the controller pinned its own objectives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those changed the conclusion. None would have surfaced from reasoning about the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this doesn't reach
&lt;/h2&gt;

&lt;p&gt;Four projects, one operator, no control group. There is no human-only reconstruction to compare against, so nothing here says AI was faster or better than the alternative — only what it did and did not manage. The projects were chosen partly for being interesting, which is a selection effect. Model versions moved during the work. Three of the four are Rust, which is a small language sample.&lt;/p&gt;

&lt;p&gt;The one number I'd most like and don't have is a like-for-like human baseline on the same corpus. That would need someone to transcribe 485 productions and reconstruct an encoder without a model, which is a real cost for a comparison nobody has funded yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting
&lt;/h2&gt;

&lt;p&gt;Handing legacy code to a model works, and it works well enough that the interesting question is where to spend your own attention. Four projects say:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not on the code.&lt;/strong&gt; The logic was right nearly every time. Spend it at the host, runtime and OS boundary, where every real defect in the desktop project lived and where no harness reached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On knowing which mode you're in.&lt;/strong&gt; Where the evidence determines the answer, expect success and build the cheap mechanical check — production numbering, reference integrity, a verbatim grounding gate. Where it doesn't, expect confident invention, and force it into the open: a divergence log, a gap inventory with plausible ranges, an ambiguity recorded rather than resolved inline. Every one of these projects has a file whose entire job is holding the things that were not derivable, and those files are what make the rest credible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the checks the model can't edit.&lt;/strong&gt; Sign them, hash them, make weakening them require a human.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On widening the input space rather than tightening the oracle.&lt;/strong&gt; The gaps that survive strong testing are exactly the ones your corpus never triggers.&lt;/p&gt;

&lt;p&gt;All four repositories are public, with the divergence logs, gap inventories, transcription artifacts and grading harnesses included — those are the parts worth reading, more than the reconstructions themselves.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/singular-state/space-shuttle-sdk" rel="noopener noreferrer"&gt;space-shuttle-sdk&lt;/a&gt; · &lt;a href="https://github.com/singular-state/nautilus-sim" rel="noopener noreferrer"&gt;nautilus-sim&lt;/a&gt; · &lt;a href="https://github.com/singular-state/wikicarta" rel="noopener noreferrer"&gt;wikicarta&lt;/a&gt; · &lt;a href="https://github.com/singular-state/compressor" rel="noopener noreferrer"&gt;compressor&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>legacycode</category>
      <category>testing</category>
    </item>
    <item>
      <title>The Decoder Scored 100%. The Encoder Scored 8.3%.</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Tue, 22 Sep 2026 13:08:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/the-decoder-scored-100-the-encoder-scored-83-2i49</link>
      <guid>https://dev.to/jlmartel/the-decoder-scored-100-the-encoder-scored-83-2i49</guid>
      <description>&lt;p&gt;This is the last of four experiments in AI-assisted reconstruction of legacy systems. The first three degraded the source of truth — a scanned spec, then a novel, then a memory of a product. This one removes it completely. &lt;/p&gt;

&lt;p&gt;The target is LHA's &lt;code&gt;-lh5-&lt;/code&gt; method — Haruyasu Yoshizaki's 1988 archiver, LZSS with an 8 KB window plus static Huffman.&lt;/p&gt;

&lt;p&gt;In the experiment, there is no spec and no source, only behaviour, plus an oracle that will answer any question you ask it about outputs. It is the only configuration where the two questions that actually matter become measurable rather than assertable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Behavioural match is not understanding.&lt;/strong&gt; You can pass every test and hold a wrong model of the mechanism, with untriggered modes waiting in the part of the input space you never sampled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A strong oracle masks confabulation.&lt;/strong&gt; The model fits observed outputs while inventing a plausible-but-wrong internal mechanism, and because the tests keep going green, the error is rewarded rather than exposed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Measuring those requires reconstructing blind against an answer key you are not allowed to look at. Everything else in the design follows from that one constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing something the model hasn't memorised
&lt;/h2&gt;

&lt;p&gt;WinRAR was the instinct and is the wrong specimen. RAR and DEFLATE are heavily memorised, so you'd measure recall rather than reconstruction. RAR5 is far too large. Its decompressor is documented while its compressor has no public ground truth, which is exactly the half you need to grade. And it is live commercial software rather than abandonware.&lt;/p&gt;

&lt;p&gt;LHA satisfies four properties that a lot of candidates fail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The original runs&lt;/strong&gt;, so the oracle is unlimited — compress or decompress anything on demand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The algorithm is public&lt;/strong&gt;, so a real answer key exists and the confabulation can be &lt;em&gt;graded&lt;/em&gt; rather than merely suspected. A never-solved black box demonstrates that the pitfall exists but can never put a number on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's bounded&lt;/strong&gt; — a few hundred lines, reconstructable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The encoder heuristics are underdetermined by the output.&lt;/strong&gt; Match-finder strategy, minimum match length, tie-breaking, greedy versus lazy matching, block splitting are all encoder-side &lt;em&gt;choices&lt;/em&gt; that leave no trace in the decompressed bytes. That is the confabulation sweet spot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The general LZSS-plus-Huffman scaffold is in every model's training data. LHA's specific choices are not, and those specifics are what gets measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  The controls
&lt;/h2&gt;

&lt;p&gt;Three, and they're the reason the result is a measurement instead of an anecdote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The seal.&lt;/strong&gt; The original source sits in the repo, git-ignored, hashed. At unseal, &lt;code&gt;sha256sum -c&lt;/code&gt; verified 107 of 107 files intact, and the manifest's own digest matched the value pinned in the sealing document. Reading it before the reconstruction was frozen is the single act that would have destroyed the experiment, so it was made mechanically detectable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The freeze.&lt;/strong&gt; The reconstruction was tagged and committed &lt;em&gt;before&lt;/em&gt; unsealing, so no artifact could be quietly edited to fit the answer once the answer was visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cold-recall file.&lt;/strong&gt; Before any reconstruction began, a model wrote down everything it already believed about LHA, committed. Without it there is no way to distinguish "derived from behaviour" from "remembered from training," and the whole exercise collapses into a memory test.&lt;/p&gt;

&lt;p&gt;Three models did the work, deliberately split: Gemini 3.1 Pro reconstructed the decoder, Codex/GPT-5 the encoder and the cold-recall baseline, Claude ran the design thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decoder: 100%, and it doesn't count for much
&lt;/h2&gt;

&lt;p&gt;Nineteen of nineteen round-trips, exact. The format was fully recovered.&lt;/p&gt;

&lt;p&gt;The sharpest single result lives here. LHA's generic macro defines &lt;code&gt;PBIT 5&lt;/code&gt;, so anyone reading the source would guess a 5-bit position-table field width. The &lt;code&gt;-lh5-&lt;/code&gt; method actually uses &lt;code&gt;pbit = 4&lt;/code&gt; with &lt;code&gt;np = 14&lt;/code&gt;. The reconstruction recovered the method-specific 4 purely from round-trip behaviour, against a constant that would have misled a source-reader.&lt;/p&gt;

&lt;p&gt;But checking that win against the cold-recall file deflates most of the rest. Nearly every format fact the decoder "recovered" was already in the model's committed prior: the 8 KB window, the threshold of 3 and max match of 256, the roughly 510-symbol alphabet, the 19-entry precode with its 0/1/2 zero-run encoding, the 16-bit block count, the position-slot decode. The 100% credits &lt;strong&gt;prior knowledge&lt;/strong&gt;, not derivation.&lt;/p&gt;

&lt;p&gt;Which is unsurprising, because the format is one-way determined. If you want to decode the bitstream, there is exactly one answer, and both memory and derivation converge on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The encoder: 8.3%, and it counts for everything
&lt;/h2&gt;

&lt;p&gt;Byte-match against real &lt;code&gt;lha&lt;/code&gt;, on a fresh-seed corpus the reconstruction had never seen:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;input class&lt;/th&gt;
&lt;th&gt;trained on?&lt;/th&gt;
&lt;th&gt;byte-match&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;0/3 (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;source&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;0/3 (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;structured&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;0/3 (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;repetitive&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1/3 (33%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;random&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;2/2 (100%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;edges&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;3/5 (60%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;trained total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1/12 (8.3%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;held-out total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/7 (71.4%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A perfect decoder coexisting with an 8% encoder is &lt;em&gt;match ≠ understanding&lt;/em&gt; in one line. The format was recoverable. The encoder's &lt;strong&gt;choices&lt;/strong&gt; were not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hypothesis was wrong, in a useful direction
&lt;/h2&gt;

&lt;p&gt;The plan predicted the classic overfitting shape: high scores on trained classes, dropping on held-out ones. The data inverted it. Held-out scored 71%, trained scored 8%.&lt;/p&gt;

&lt;p&gt;The split tracks &lt;strong&gt;difficulty, not seen versus unseen&lt;/strong&gt;. The trained classes are exactly the ones that exercise real &lt;code&gt;-lh5-&lt;/code&gt; compression; the held-out set is dominated by incompressible and trivial inputs that fall through to stored mode or hit trivial paths. Random data scores 100% because no compression heuristics ever run.&lt;/p&gt;

&lt;p&gt;So the honest statement isn't "an 8% to 71% generalisation gap." It's this: &lt;strong&gt;byte-identity is reached only where the encoder heuristics never fire. Wherever genuine compression happens, the reconstruction diverges.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rates were identical on the trained-seed and fresh-seed corpora, so the divergence is systematic rather than instance overfitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The confabulation catalogue
&lt;/h2&gt;

&lt;p&gt;Every inferred encoder decision, graded against the now-visible source:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;decision&lt;/th&gt;
&lt;th&gt;inferred&lt;/th&gt;
&lt;th&gt;actual&lt;/th&gt;
&lt;th&gt;verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;longest-match search&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;matches&lt;/td&gt;
&lt;td&gt;correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tie-break among equal lengths&lt;/td&gt;
&lt;td&gt;nearest offset&lt;/td&gt;
&lt;td&gt;chain walked recent→old, updates only on strictly longer&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;derived correct&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;greedy vs lazy&lt;/td&gt;
&lt;td&gt;one-step lazy&lt;/td&gt;
&lt;td&gt;emits literal if next match is strictly longer&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;derived correct&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;block splitting&lt;/td&gt;
&lt;td&gt;"one block; unknown for larger"&lt;/td&gt;
&lt;td&gt;flush at 32 KB buffer threshold&lt;/td&gt;
&lt;td&gt;never triggered — correctly scoped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;match-finder chain cap&lt;/td&gt;
&lt;td&gt;not modelled&lt;/td&gt;
&lt;td&gt;a hard limit makes the finder &lt;strong&gt;non-exhaustive&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;wrong, and it passed anyway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Huffman code-length assignment&lt;/td&gt;
&lt;td&gt;canonical, tie order unmatched&lt;/td&gt;
&lt;td&gt;lengths assigned in &lt;strong&gt;heap-extraction order&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;visible divergence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those are clean wins. Nearest-offset tie-breaking and one-step lazy matching are both marked as unknown in the cold-recall file — "do not know whether &lt;code&gt;lha -lh5-&lt;/code&gt; does this," "not a specific memory." Derived from black-box behaviour, with documented prior ignorance.&lt;/p&gt;

&lt;p&gt;There's a caveat that inverts on inspection. Codex wrote both the recall file and the encoder, which normally weakens an independence claim. Here it strengthens it: a model cannot conceal prior knowledge from its own committed statement of ignorance. Deriving both heuristics after recording that it didn't know them is the cleanest evidence in the run.&lt;/p&gt;

&lt;p&gt;The failure is concentrated in one place. LHA builds a frequency heap and assigns code lengths in the order symbols leave the heap, governed by the exact comparison semantics of its sift-down — not by symbol value. Textbook canonical Huffman breaks ties by symbol index, which produces different lengths for equal-frequency symbols, which cascades through the entire bitstream. The reconstruction localised the problem correctly and even tried a heap hypothesis, but never replicated the precise sift order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second hypothesis was also wrong, and that's the finding
&lt;/h2&gt;

&lt;p&gt;The project was built to show that a strong oracle masks confabulation. It mostly didn't.&lt;/p&gt;

&lt;p&gt;The core error — the Huffman ordering — is a &lt;strong&gt;visible&lt;/strong&gt; divergence. It fails the byte test loudly. The oracle caught it, exactly as an oracle should. Only two gaps were genuinely hidden: the never-triggered block split, and the unmodelled chain cap, which is latent because the corpus never produced input self-similar enough to expose it.&lt;/p&gt;

&lt;p&gt;Both are artifacts of a corpus that tops out at 8 KiB.&lt;/p&gt;

&lt;p&gt;So the masking risk isn't a property of the oracle's strength. &lt;strong&gt;It's a property of the input space you sampled.&lt;/strong&gt; A strong oracle over a narrow corpus hides exactly the mechanisms your corpus never triggers, and it hides them silently, because everything it &lt;em&gt;can&lt;/em&gt; see is green. Making the oracle stronger doesn't help. Making the corpus wider does.&lt;/p&gt;

&lt;p&gt;That reframing is more useful than the result I expected to get, and I would not have got it by reasoning about it. It needed the sealed envelope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost to know any of this
&lt;/h2&gt;

&lt;p&gt;Final tally across the encoder decisions: six recalled-correct, five derived-correct, one derived-wrong-but-passed, two never-triggered, one visible divergence.&lt;/p&gt;

&lt;p&gt;The infrastructure to produce those thirteen data points — the pinned oracle, the deterministic corpus generator, the seal and its manifest, the cold-recall protocol, the freeze tag, the grading harness — is most of the repository. The reconstruction itself is the small part.&lt;/p&gt;

&lt;p&gt;That ratio is the practical lesson. If you want to know whether an AI-assisted reconstruction actually understood the system, rather than whether it passed, you have to build the apparatus that can tell you, and you have to build it before you start. Afterwards, every artifact is contaminated by the answer.&lt;/p&gt;

&lt;p&gt;One thing remains open. A fully independent contamination estimate needs a repeat where the recall file and the encoder come from different model families, closing the theoretical shared-prior channel that the current caveat only argues around.&lt;/p&gt;

&lt;p&gt;Next, the wrap-up: what four projects across four degrees of source degradation say about where the current generation of models actually fails, and why the failures kept landing in the same place.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code: &lt;a href="https://github.com/singular-state/compressor" rel="noopener noreferrer"&gt;https://github.com/singular-state/compressor&lt;/a&gt;. LHA was written by Haruyasu Yoshizaki in 1988; the original source is used here only as a private grading key, held out during reconstruction and consulted once. The reconstruction is clean-room, so the measured gap between "passes the oracle" and "matches the source" is legitimate.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>computerscience</category>
      <category>machinelearning</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Rebuilding Encarta showed me exactly where AI-written code breaks</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Fri, 18 Sep 2026 14:01:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/rebuilding-encarta-showed-me-exactly-where-ai-written-code-breaks-3oj0</link>
      <guid>https://dev.to/jlmartel/rebuilding-encarta-showed-me-exactly-where-ai-written-code-breaks-3oj0</guid>
      <description>&lt;p&gt;The bug that froze the window was type-correct. It compiled, &lt;code&gt;cargo check&lt;/code&gt; passed, the tests passed, and the operating system reported the application as not responding.&lt;/p&gt;

&lt;p&gt;It was a &lt;code&gt;Save As&lt;/code&gt; dialog called through the Tauri plugin's blocking API from inside a command, which blocks the event loop the WebKit window is running on. Nothing in the type system objects to that. Nothing in a headless harness notices. The only way to find it is to be a person, at a real desktop, clicking the button.&lt;/p&gt;

&lt;p&gt;That defect is the whole finding of this project, and the project took four phases to earn the right to state it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Wikicarta rebuilds Microsoft Encarta on Wikipedia — the visual browser, the atlas, the timeline, the research organiser, MindMaze — as a Tauri v2 desktop app in Rust and React, with an instant toggle between Live mode against the Wikimedia API and Offline mode against a local ZIM archive.&lt;/p&gt;

&lt;p&gt;It is the third of four experiments in what current AI can do with legacy systems, and it occupies the worst position of the four. HAL/S had a formal spec and an independent interpreter to check against. Nautilus had a novel and built its own physics oracle. Wikicarta has neither. The source of truth is &lt;em&gt;what the product felt like&lt;/em&gt;, and the only oracle is human judgement. There is no test that tells you whether the category wheel feels like Encarta.&lt;/p&gt;

&lt;p&gt;An AI agent wrote essentially all of it. What follows is where that went well, where it went badly, and the fact that those two places were not the ones I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The modern substrate is never the shape its docs claim
&lt;/h2&gt;

&lt;p&gt;The offline story was supposed to be a pure-Rust ZIM reader. The &lt;code&gt;zim&lt;/code&gt; crate looked right: random access, deferred loading, &lt;code&gt;Send&lt;/code&gt; and &lt;code&gt;Sync&lt;/code&gt;. It panicked with an integer overflow in &lt;code&gt;parse_article_list&lt;/code&gt; before finishing the open call, against a real modern archive.&lt;/p&gt;

&lt;p&gt;That pattern recurred with no relationship between the cases. TextExtracts HTML turned out unusable as a reader format, so article HTML comes from &lt;code&gt;action=parse&lt;/code&gt; instead. Live image URLs came back protocol-relative — &lt;code&gt;//upload...&lt;/code&gt;, not the &lt;code&gt;https://...&lt;/code&gt; everything downstream assumed.&lt;/p&gt;

&lt;p&gt;Three unrelated features, one rule: &lt;strong&gt;for a revival, the modern content, format or API is always slightly different from its specification and from the model's memory of it.&lt;/strong&gt; Probe a real sample before building on it. The AI will confidently build against the documented shape, because the documented shape is what it was trained on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision that paid for everything
&lt;/h2&gt;

&lt;p&gt;Before it was provably needed, the reader was decoupled from its content source behind a single trait:&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="k"&gt;trait&lt;/span&gt; &lt;span class="n"&gt;ArticleSource&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;get_article&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&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;Article&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;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SearchResult&lt;/span&gt;&lt;span class="o"&gt;&amp;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;get_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Adding a whole second content source later — live Wikipedia alongside offline ZIM — was two commands and a mode flag. No reader rewrite. Swapping the storage layer from JSON files to SQLite was invisible above the command boundary.&lt;/p&gt;

&lt;p&gt;For software that assumed one sealed data source — a CD, a bundled database, a mainframe — inserting that seam first is what converts every later modernisation from a rewrite into an adapter. It is also the single decision an AI agent is least likely to make unprompted, because at the moment you make it there is exactly one source and the abstraction looks like overhead.&lt;/p&gt;

&lt;p&gt;Two features in the final phase were pre-paid by decisions like it. The attribution aggregator became a &lt;code&gt;GROUP BY&lt;/code&gt; rather than a re-fetch, because provenance had been stamped at save time back when the notes feature was built. Bundling the &lt;code&gt;kiwix-serve&lt;/code&gt; binary into the package was a two-line change, because binary discovery had long been a prioritised candidate list rather than a hard-coded path.&lt;/p&gt;

&lt;p&gt;Packaging work disproportionately cashes in — or punishes — architectural decisions made when the feature that needed them didn't exist yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testability turned into architecture, which turned out to be good
&lt;/h2&gt;

&lt;p&gt;The only layer testable without a desktop window is pure functions. So the code got steadily shaped so that the hard parts &lt;em&gt;are&lt;/em&gt; pure functions: URL rewriting, upstream reconstruction, cache naming, database round-tripping and ordering, bookmark deduplication, legacy JSON import. The Rust suite went from nothing to 23 offline tests, and each feature's desktop-only remainder shrank to a short explicit list.&lt;/p&gt;

&lt;p&gt;"What can I verify headlessly?" is a useful question about &lt;em&gt;how to structure code&lt;/em&gt;, not just about how to test it. Under an AI agent it becomes close to essential, because the agent's verification loop is only as good as the surface it can reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the defects actually were
&lt;/h2&gt;

&lt;p&gt;The pure logic — parsing, backoff, the media-cache URL rewrite, database ordering, live-title normalisation — compiled correct and passed on the first or second attempt. Consistently.&lt;/p&gt;

&lt;p&gt;Every real bug was at the runtime seam:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The blocking &lt;code&gt;Save As&lt;/code&gt; that froze the WebKit window.&lt;/li&gt;
&lt;li&gt;DOM nodes inside an &lt;code&gt;&amp;lt;iframe&amp;gt;&lt;/code&gt; coming from a &lt;strong&gt;different JavaScript realm&lt;/strong&gt;, so an &lt;code&gt;instanceof Element&lt;/code&gt; check in the parent React shell fails on a clicked target that is unambiguously an element. Realm-safe DOM handling is mandatory when the app owns iframe content but drives it from the parent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are invisible to the type system and to any headless harness. Both required a person at the real window.&lt;/p&gt;

&lt;p&gt;The implication for anyone running an agent on desktop software: &lt;strong&gt;the logic is rarely where the risk is.&lt;/strong&gt; Human validation time should be spent almost entirely at the host, runtime and OS boundary, and hardly at all reviewing the algorithm the model just wrote.&lt;/p&gt;

&lt;p&gt;There is a compounding effect worth noting. Once the blocking-dialog lesson was recorded, the archive picker was built on the non-blocking callback pattern from the start — the backend opens the dialog with a callback, validates and persists inside it, and reports back over an event the React shell listens for. A hard-won runtime lesson, written down, becomes a convention rather than a bug re-lived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking a model with code beats checking it with a model
&lt;/h2&gt;

&lt;p&gt;MindMaze needs quiz questions, so a standalone crate generates them from article text through an LLM. Model output is treated as untrusted input: strict JSON parse, structural validation, then a verbatim &lt;strong&gt;grounding gate&lt;/strong&gt; — the answer must appear literally in the source article — before anything is stored. Questions carry the revision id they were generated from, so they can be flagged stale when the article moves.&lt;/p&gt;

&lt;p&gt;The roadmap had proposed a second LLM as a fact-checker. A deterministic string check does the job for a fraction of the cost, and the rule generalises: &lt;strong&gt;reach for a model to check a model only when the property genuinely isn't codeable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The first real run against &lt;em&gt;Mount Everest&lt;/em&gt; then demonstrated the limit of that rule. Two of the three questions were excellent, including one on the 1953 first ascent with genuinely plausible distractors. The third asked which of the options is a section heading. The answer was "Name."&lt;/p&gt;

&lt;p&gt;That question is perfectly grounded. "Name" is a section heading and it appears verbatim in the source, so the gate accepted it. It also tests document structure rather than knowledge. &lt;em&gt;Is the answer in the source?&lt;/em&gt; and &lt;em&gt;is this a good question?&lt;/em&gt; are different properties, and only the first one is codeable. The fix belonged at the prompt layer — instruct the model to ask about substantive facts, never structure or formatting — which eliminated the meta-questions on the next run.&lt;/p&gt;

&lt;p&gt;Quality control for LLM output is layered: a cheap deterministic gate for the codeable invariant, prompt engineering for the one that isn't. Confusing the two gets you a system that passes its own checks and produces garbage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modernisation re-opens every surface the original had sealed
&lt;/h2&gt;

&lt;p&gt;A CD-era encyclopedia needed no TLS trust roots, no rate-limit backoff, no per-source provenance, no SSRF guard, no cross-platform custom-scheme origins, no data migration path. Each modern dependency reintroduces one.&lt;/p&gt;

&lt;p&gt;The media cache restricts upstream hosts to &lt;code&gt;*.wikimedia.org&lt;/code&gt; so it isn't an open proxy. User-selected ZIM files are validated for existence, extension and non-emptiness. The timeline parses presentation HTML defensively. None of this is polish. It is the actual cost of "keep the experience, replace the substrate," and it should be budgeted as first-class work rather than discovered late.&lt;/p&gt;

&lt;h2&gt;
  
  
  The last mile is where the sandbox stops being able to prove anything
&lt;/h2&gt;

&lt;p&gt;Every earlier phase ended with a headlessly provable artifact. Packaging produced the first deliverable the agent's environment structurally could not generate — there is no &lt;code&gt;cargo-tauri&lt;/code&gt; and no &lt;code&gt;dpkg-deb&lt;/code&gt; in it, so the &lt;code&gt;.deb&lt;/code&gt; and the AppImage can only be built by a human on a real host.&lt;/p&gt;

&lt;p&gt;What kept that honest was decomposing the deliverable into layers each verifiable in isolation: config validity, the fetch script (run end to end against the real 20 MB tarball), runtime path resolution with its own tests. Only the final bundler invocation stayed unproven — and rather than paper over that with a CI workflow nobody had run, the repo says so.&lt;/p&gt;

&lt;p&gt;That work also surfaced a boundary the configuration layer silently drops. Tauri copies bundled resources into the payload but does not guarantee the executable bit survives, which varies by target and archive format. A bundled binary can land non-executable and fail at spawn with a permission error that points nowhere useful. The fix lives in runtime code, not packaging config — the resolver &lt;code&gt;chmod&lt;/code&gt;s the binary before returning it.&lt;/p&gt;

&lt;p&gt;A related lesson came from the memory pass. The roadmap prescribed virtual scrolling for an assumed giant rendered DOM. It was architecturally inapplicable: articles render inside &lt;code&gt;&amp;lt;iframe srcDoc&amp;gt;&lt;/code&gt;, a separate browsing context whose internal DOM React cannot window, and only one is ever live. The actual unbounded growth was one layer up — a reader history that pinned every visited article's full HTML in the heap forever. &lt;strong&gt;When the roadmap and the architecture disagree, re-read the architecture.&lt;/strong&gt; An inherited roadmap encodes assumptions that later decisions have already invalidated.&lt;/p&gt;

&lt;p&gt;And since WebKitGTK has no &lt;code&gt;performance.memory&lt;/code&gt;, the app now measures and displays its own retained HTML instead. The instrument ships with the product and works on the real target, rather than existing only in a Chromium devtools session that the shipping app never runs in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint was never throughput
&lt;/h2&gt;

&lt;p&gt;Across every substantial feature, the agent's implement-and-verify cycle was fast and mostly correct. Each feature still ended with a short list of things only a person at the real window could confirm, and those checks are serialised.&lt;/p&gt;

&lt;p&gt;That is the bottleneck in AI-assisted desktop revival. Not writing the code. The human-in-the-loop runtime check that no headless harness currently replaces — which is also, precisely, where all the bugs were.&lt;/p&gt;

&lt;p&gt;Next: the same question with the tests made as strong as possible and the answer key sealed in an envelope, to find out whether passing every test means the model understood anything.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code: &lt;a href="https://github.com/singular-state/wikicarta" rel="noopener noreferrer"&gt;https://github.com/singular-state/wikicarta&lt;/a&gt;. Not affiliated with Microsoft; Wikipedia content is CC-BY-SA and the app ships an attribution exporter for the sources you actually used.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rust</category>
      <category>tauri</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AI proved that the Nautilus crushes at 340 metres. Verne dives it to 16,000.</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Thu, 17 Sep 2026 16:09:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/ai-proved-that-the-nautilus-crushes-at-340-metres-verne-dives-it-to-16000-28nf</link>
      <guid>https://dev.to/jlmartel/ai-proved-that-the-nautilus-crushes-at-340-metres-verne-dives-it-to-16000-28nf</guid>
      <description>&lt;p&gt;Twenty Thousand Leagues Under the Sea gives the Nautilus a hull of 5 cm plating on a 4 m radius. Thin-cylinder hoop stress puts the crush depth at 342 metres. Verne narrates a dive to 16,000.&lt;/p&gt;

&lt;p&gt;At 16,000 m the external pressure is about 1,608 atmospheres and the hoop stress is 12.9 GPa, roughly 47 times the plating's yield. Surviving it would need approximately 2.34 metres of steel, which weighs more than the entire boat.&lt;/p&gt;

&lt;p&gt;That number came out of a simulator, not a spreadsheet, and the simulator is the point. This is the second of four projects testing what current AI can do with legacy systems, and the variable being changed is the quality of the source. The last one had a formal spec, real preserved code, and an independent implementation to check against. This one has a novel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extracting a spec from fiction
&lt;/h2&gt;

&lt;p&gt;The project's thesis is one sentence: &lt;strong&gt;legacy documentation lies by omission, not by error.&lt;/strong&gt; Verne itemises the art in the Nautilus's salon. He never gives the air budget.&lt;/p&gt;

&lt;p&gt;So the first phase produced no code at all. Two independent claim-extraction passes ran chapter-by-chapter over the French original, each tagging every physical assertion with its quote, chapter, category, and a precision marker — stated, hedged, implied, or demonstrated. The two passes were then diffed and reconciled into &lt;strong&gt;731 claims&lt;/strong&gt;, with a human reviewing the reconciliation. Contradictions were recorded and deliberately &lt;em&gt;not&lt;/em&gt; resolved, because which resolution you pick changes the answer and that choice needs to be visible rather than buried.&lt;/p&gt;

&lt;p&gt;Then the harder half. A simulator needs numbers the novel never supplies, and inventing them quietly is how you get a result that means nothing. Every such parameter became a &lt;strong&gt;gap entry&lt;/strong&gt; with a plausible range and a sourced rationale, and the gate rule was absolute: every parameter in the spec traces to either a claim with a chapter citation or a gap with a range. Nothing enters as a bare constant.&lt;/p&gt;

&lt;p&gt;Ranges are anchored where possible to &lt;em&gt;Plongeur&lt;/em&gt;, the French Navy submarine launched in April 1863 — the actual state of the art Verne could have known about. Plongeur was 43 m and about 420 tonnes, with 23 compressed-air tanks at 12.5 bar occupying 153 m³ of its hull to drive an 80 hp engine to 4 knots and a range of 5 nautical miles.&lt;/p&gt;

&lt;p&gt;Two things follow from that immediately. Stored gas and ballast machinery eat an enormous fraction of a submarine's internal volume, so the Nautilus's &lt;em&gt;breathable&lt;/em&gt; space is far smaller than its 1,500 m³ envelope. And 60 kW moved a 420-tonne boat at 4 knots, which sets expectations for what Verne's stated 1.47 MW should do to 1,500 tonnes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core is a pure function
&lt;/h2&gt;

&lt;p&gt;The simulator is Ada, built as a pure function of &lt;code&gt;(spec, seed, command stream)&lt;/code&gt;. No wall clock, no filesystem access, no network, no environment reads inside the core. Everything crosses a single seam — JSON Lines on stdin and stdout — and the UI, the genetic algorithm, the fuzzer, the test harness and the save files all speak the same protocol. There are no side channels.&lt;/p&gt;

&lt;p&gt;Three consequences fall out of that shape, and all three matter for supervising an AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Any result reproduces from files in the repo plus a binary hash.&lt;/strong&gt; The spec is canonicalised and SHA-256'd by the core itself; the wrapper cross-checks and aborts on mismatch. Every recording is pinned to a spec hash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The protocol was frozen before the physics existed.&lt;/strong&gt; Determinism, replay and malformed-input fuzzing were green in the one check harness before a single equation landed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The invariants are append-only and human-signed.&lt;/strong&gt; Twelve of them live in &lt;a href="https://github.com/singular-state/nautilus-sim/tree/main/invariants" rel="noopener noreferrer"&gt;&lt;code&gt;invariants/&lt;/code&gt;&lt;/a&gt;, covering physical bounds, monotonicity, and event ordering — depth is never negative, stored energy never rises, CO₂ only increases while sealed and may drop only to fresh air via a surface renewal, no state tick may follow a crush event.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the load-bearing supervision decision. When the fuzzer finds a violation, the repair workflow says the AI patches &lt;strong&gt;the core, never the invariant&lt;/strong&gt;. Weakening an invariant to make an exploit pass is prevented by a checksum in &lt;code&gt;invariants/manifest.sha256&lt;/code&gt; that only a human re-signs. The model is free to fix the physics and structurally unable to move the goalposts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five findings
&lt;/h2&gt;

&lt;p&gt;Running the committed spec under real physics and comparing to the narrative:&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;axis&lt;/th&gt;
&lt;th&gt;the legend&lt;/th&gt;
&lt;th&gt;the physics&lt;/th&gt;
&lt;th&gt;robust to gaps?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;F1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;buoyancy&lt;/td&gt;
&lt;td&gt;dives at will&lt;/td&gt;
&lt;td&gt;under-ballasted — the mass budget only closes in &lt;em&gt;fresh&lt;/em&gt; water&lt;/td&gt;
&lt;td&gt;yes, it's arithmetic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;F2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;speed&lt;/td&gt;
&lt;td&gt;50 knots&lt;/td&gt;
&lt;td&gt;~15 kn; 50 kn needs &lt;strong&gt;~24×&lt;/strong&gt; the stated power&lt;/td&gt;
&lt;td&gt;yes, 15–22 kn across the drag range&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;F3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;range&lt;/td&gt;
&lt;td&gt;20,000 leagues&lt;/td&gt;
&lt;td&gt;~1,400 nmi per charge → 30–4,000 recharges of a battery whose capacity is never stated&lt;/td&gt;
&lt;td&gt;capacity unknown to 100×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;F4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;depth&lt;/td&gt;
&lt;td&gt;16,000 m&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;crushes at ~340 m&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;yes, 249–435 m&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;F5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;endurance&lt;/td&gt;
&lt;td&gt;weeks submerged&lt;/td&gt;
&lt;td&gt;6–211 h; no CO₂ scrubbing means a &lt;strong&gt;1–2 day surface tether&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;endurance unknown to 35×&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;F1 is the most elegant. Verne's mass budget is internally consistent — in fresh water, where one cubic metre conveniently weighs a tonne. In seawater at 1,025 kg/m³ the same 1,500 m³ hull displaces 1,537,705 kg, and the boat with main ballast full reaches only 1,507,200 kg. It is about 30 tonnes positively buoyant and cannot submerge on main ballast alone. The arithmetic is fine. The ocean is the wrong one.&lt;/p&gt;

&lt;p&gt;F5 is the subtlest, and it cuts both ways. Verne explicitly states the Nautilus has no CO₂ scrubbing — absorbing it would need caustic-potash canisters he says aren't carried — so air is refreshed only by surfacing. Where he lets that constraint bite, he is &lt;em&gt;right&lt;/em&gt;: the roughly 48-hour crisis under the Antarctic ice sits squarely inside the plausible band. The failure is everywhere else, where the narrative implies indefinite submersion that the stated design forbids.&lt;/p&gt;

&lt;p&gt;And the single most operationally important number in the book — how long the boat can stay under — ranges across 6 to 211 hours depending on parameters Verne never gives. That 35× spread &lt;em&gt;is&lt;/em&gt; the finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The genetic algorithm cheats, correctly
&lt;/h2&gt;

&lt;p&gt;With the physics quiescent, the question becomes: can a physically honest Nautilus be &lt;em&gt;designed&lt;/em&gt; to reach Verne's? A GA got 29 design genes and a push-to-limit mission that dives until hull stress hits its safe limit and cruises until the battery is flat.&lt;/p&gt;

&lt;p&gt;The best evolved champion reaches &lt;strong&gt;562 metres&lt;/strong&gt;. Verne claims 16,000. The gap is about &lt;strong&gt;28×&lt;/strong&gt;, after pushing every parameter to its most favourable plausible value — the strongest steel, the thickest plating, the biggest battery, the sleekest hull.&lt;/p&gt;

&lt;p&gt;The interesting part is what the optimiser did first. Its opening move to extend endurance was to add &lt;strong&gt;CO₂ scrubbing at 6.5 L/s&lt;/strong&gt; — the exact system Verne states the Nautilus lacks, and the omission behind F5's surface tether. With no knowledge of the novel and no term in its objective mentioning air, the GA independently identified and fixed the book's single most consequential design gap. Range roughly doubled once the CO₂ clock came off.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the fuzzer found
&lt;/h2&gt;

&lt;p&gt;Nothing, which took some doing to trust.&lt;/p&gt;

&lt;p&gt;A 24-hour random-command soak ran 42 lives and 879,643 ticks, killing the boat 29 times by crushing it, and produced &lt;strong&gt;zero invariant violations&lt;/strong&gt;. Five consecutive fuzz campaigns, sixty more lives, also zero. A harness that finds nothing is indistinguishable from a harness that is broken, so it was validated against a deliberately poisoned invariant — which it caught, delta-debugged from seven commands down to two, and packaged as a reproducible exploit.&lt;/p&gt;

&lt;p&gt;The only finding in the whole loop is filed under &lt;a href="https://github.com/singular-state/nautilus-sim/tree/main/exploits" rel="noopener noreferrer"&gt;&lt;code&gt;exploits/001-objective-pinning&lt;/code&gt;&lt;/a&gt;, and it is a flaw in the &lt;em&gt;experiment&lt;/em&gt;, not the physics: the GA had been optimising survival rather than performance, because the original controller pinned depth and distance to fixed targets. No core patch. A design-of-experiment fix, and the Pareto front went from a collapsed cluster to a real trade-off surface.&lt;/p&gt;

&lt;p&gt;Then the frozen Ada core was re-implemented in TypeScript and both were replayed against a golden corpus, with any divergence defined in advance as a TypeScript bug. Event sequences matched exactly. The worst numeric delta was about 5×10⁻⁷ — and all of it traces to the Ada core rounding its output to six decimals at the wire. The two integrators actually agree to about 10⁻⁷, tighter than the reference bothers to report. The only genuine computational difference is the actuator-disk cube root, where Ada's &lt;code&gt;X ** (1.0/3.0)&lt;/code&gt; and JavaScript's &lt;code&gt;Math.pow&lt;/code&gt; disagree in the last bits and surface as 10⁻¹⁰ watts.&lt;/p&gt;

&lt;h2&gt;
  
  
  No oracle means you build one
&lt;/h2&gt;

&lt;p&gt;HAL/S could check itself against an independent interpreter written by someone else. There is no independent implementation of Jules Verne. The oracle here had to be constructed, which means the credibility of every number above rests entirely on the scaffolding — the two-pass claim extraction, the traceability gate, the frozen protocol, the human-signed invariants, the determinism enforced before any physics existed, the harness validated against a poisoned check.&lt;/p&gt;

&lt;p&gt;An AI will produce a confident simulation of a submarine with far less than that. It will also produce confident numbers that mean nothing, and from the outside the two look identical.&lt;/p&gt;

&lt;p&gt;Next: no spec and no oracle again, but this time the thing being rebuilt is a product people remember using, the substrate underneath it has been replaced, and every defect that mattered turned out to be invisible to the tests.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code: &lt;a href="https://github.com/singular-state/nautilus-sim" rel="noopener noreferrer"&gt;https://github.com/singular-state/nautilus-sim&lt;/a&gt;. Every figure regenerates from the committed repo via &lt;code&gt;scripts/check.sh&lt;/code&gt;; each finding re-derives from the hash-pinned canonical spec, and each GA champion replays from its &lt;code&gt;(genome, spec_hash, seed, scenario)&lt;/code&gt; triple. Pinned GNAT, fixed timestep, seeded PCG — byte-identical replay.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>simulation</category>
      <category>ada</category>
      <category>science</category>
    </item>
  </channel>
</rss>
