<?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: Ayoub Hammoudi</title>
    <description>The latest articles on DEV Community by Ayoub Hammoudi (@ayoub_hammoudi).</description>
    <link>https://dev.to/ayoub_hammoudi</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%2F4180380%2F7da93f43-0d81-4b44-86c5-00032577d099.png</url>
      <title>DEV Community: Ayoub Hammoudi</title>
      <link>https://dev.to/ayoub_hammoudi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ayoub_hammoudi"/>
    <language>en</language>
    <item>
      <title>257 clones in two days. 1 star. 0 issues.</title>
      <dc:creator>Ayoub Hammoudi</dc:creator>
      <pubDate>Thu, 05 Nov 2026 20:33:32 +0000</pubDate>
      <link>https://dev.to/ayoub_hammoudi/257-clones-in-two-days-1-star-0-issues-24dl</link>
      <guid>https://dev.to/ayoub_hammoudi/257-clones-in-two-days-1-star-0-issues-24dl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcm021yrvxql2t20pnvi7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcm021yrvxql2t20pnvi7.png" alt="257 clones, 66 unique cloners, 1 star, 0 issues" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;257 clones in two days.&lt;/p&gt;

&lt;p&gt;1 star. 0 issues. Not a single "hello".&lt;/p&gt;

&lt;p&gt;We've all been on the other side of that silence. You find a tool, clone it, try it late at night. It almost works. You close the tab, because asking feels like bothering a stranger.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9aydqwwlts0wx2fuewde.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9aydqwwlts0wx2fuewde.png" alt="12 days of nothing, then 166 clones in one day" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I'm not reading those numbers as a win yet. I'm reading them as 66 people standing at a door that looked closed.&lt;/p&gt;

&lt;p&gt;Here it is, wide open:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Half-formed idea, "is this a stupid question?"&lt;/strong&gt; Discussions. It isn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Found a bug?&lt;/strong&gt; A two-minute issue template.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Want to send code?&lt;/strong&gt; CI, CodeQL and security checks run on every PR. Strict on purpose: when it's green, you can be bold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb93nrvde1oke2xc5s4s5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb93nrvde1oke2xc5s4s5.png" alt="The door is open" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One rule, and it applies to me first: a change to the routing comes with a measurement. Opinions don't change the table. Not even mine.&lt;/p&gt;

&lt;p&gt;To the 66: thank you for trying it. Now tell me where it broke.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/spar-x-fr/spar-x-skills" rel="noopener noreferrer"&gt;github.com/spar-x-fr/spar-x-skills&lt;/a&gt;&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>opensource</category>
      <category>github</category>
      <category>ai</category>
    </item>
    <item>
      <title>"Mandatory user verification" on DEV is a scam. Here's how to spot it in 30 seconds.</title>
      <dc:creator>Ayoub Hammoudi</dc:creator>
      <pubDate>Wed, 04 Nov 2026 19:09:50 +0000</pubDate>
      <link>https://dev.to/ayoub_hammoudi/mandatory-user-verification-on-dev-is-a-scam-heres-how-to-spot-it-in-30-seconds-1594</link>
      <guid>https://dev.to/ayoub_hammoudi/mandatory-user-verification-on-dev-is-a-scam-heres-how-to-spot-it-in-30-seconds-1594</guid>
      <description>&lt;p&gt;If you got tagged by "Support Bot" on DEV, don't click.&lt;/p&gt;

&lt;p&gt;It took one look at the profile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The name isn't "Support". It's Cyrillic letters dressed as Latin ones: "Suрроrt Воt".&lt;/li&gt;
&lt;li&gt;The account was created on Nov 4: 0 posts, 41 comments when I looked.&lt;/li&gt;
&lt;li&gt;It tags 10 people per comment, in alphabetical order, ten at a time. Nobody at DEV works through the alphabet by hand.&lt;/li&gt;
&lt;li&gt;The username has already changed: my notification says salemsalem123, the profile now says supportbot123.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No real platform verifies you through a stranger's comment, and none needs your card for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clicked but typed nothing?&lt;/strong&gt; Close the tab, clear that site's data, check your downloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Authorized something or typed a password?&lt;/strong&gt; Revoke the app and change the password now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entered your card?&lt;/strong&gt; Call your bank.&lt;/p&gt;

&lt;p&gt;Then flag the account: profile, then "Flag" or "Mark as Spam".&lt;/p&gt;

&lt;p&gt;Check the profile before the link. It takes ten seconds, and it would have stopped this one cold.&lt;/p&gt;

</description>
      <category>security</category>
      <category>discuss</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>I burned a week of Claude quota in 3 days. Then I found the setting eating it.</title>
      <dc:creator>Ayoub Hammoudi</dc:creator>
      <pubDate>Tue, 03 Nov 2026 19:58:38 +0000</pubDate>
      <link>https://dev.to/ayoub_hammoudi/i-burned-a-week-of-claude-quota-in-3-days-then-i-found-the-setting-eating-it-2099</link>
      <guid>https://dev.to/ayoub_hammoudi/i-burned-a-week-of-claude-quota-in-3-days-then-i-found-the-setting-eating-it-2099</guid>
      <description>&lt;p&gt;I burned a full week of my Claude Ultra quota in 3 days.&lt;/p&gt;

&lt;p&gt;My first instinct was to blame the model. So I did what any normal person would do: I benchmarked my own AI, and I let a script grade every answer so I couldn't lie to myself.&lt;/p&gt;

&lt;p&gt;100 runs later, I had a number I didn't believe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same problem. Same model. Ten times the price.
&lt;/h2&gt;

&lt;p&gt;A routing algorithm with mandatory stops and a toll quota. Opus solved it correctly both ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;at the lowest effort: &lt;strong&gt;$0.19&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;at the highest effort: &lt;strong&gt;$2.03&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I reran the logic in my head looking for what the $1.84 had bought. Nothing. Same verdict from the grader, and a much longer wait.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzcp147pxa976qzmhjo1a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzcp147pxa976qzmhjo1a.png" alt="Cost by effort level, relative to high" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Past &lt;code&gt;medium&lt;/code&gt;, the bars only do one thing: get longer. &lt;code&gt;max&lt;/code&gt; cost up to 9.3 times &lt;code&gt;high&lt;/code&gt;, for the same result, and took 4 to 7 times as long.&lt;/p&gt;

&lt;p&gt;That should have been the headline. It wasn't, because of what the cheapest model did next.&lt;/p&gt;

&lt;h2&gt;
  
  
  2 cents
&lt;/h2&gt;

&lt;p&gt;I gave Haiku, on its lowest effort, a task I expected it to fail: write a regex engine from scratch, without &lt;code&gt;re&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then I compared it with Python's &lt;code&gt;re&lt;/code&gt; on 3,000 random patterns.&lt;/p&gt;

&lt;p&gt;It matched all of them. Three runs out of three. For &lt;strong&gt;$0.02&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And I had been paying Opus to find values in files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I checked my everyday work
&lt;/h2&gt;

&lt;p&gt;Finding a value in a repo. Renaming across files. Fixing a parser. The boring work that fills a week, and that I was running on the most expensive brain I had.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0czg00q68io6xaiqtfbe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0czg00q68io6xaiqtfbe.png" alt="Same success, cost divided by up to 50" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;$0.352 became $0.041. 88% less, and the same 6 passes out of 6.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you turn everything down
&lt;/h2&gt;

&lt;p&gt;Don't. This is the part most cost posts skip.&lt;/p&gt;

&lt;p&gt;These tasks were well specified, and something could check the answer. That is exactly where low effort is safe. When nothing can check the work, like design or review, the numbers say nothing, and my samples are small. My credit also ran out mid-benchmark, so 21 runs were excluded and Haiku is only partly measured on the hard tasks.&lt;/p&gt;

&lt;p&gt;So the lesson was never "use the cheap setting". It was this: &lt;strong&gt;when a check proves the result, the check guarantees the quality, not the effort.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So we built rightsize
&lt;/h2&gt;

&lt;p&gt;It's that sentence turned into a Claude Code skill. Free and open source. It picks the model and effort for every step, and saves the heavy thinking for the work that truly needs it.&lt;/p&gt;

&lt;p&gt;It also runs only the tests a change touches (0.7 s instead of 11.4 s on a 40-module repo), and ships an audit that reads your local transcripts and shows where your tokens went. Nothing leaves your machine. I recommend it, unless you prefer not knowing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/plugin marketplace add spar-x-fr/spar-x-skills
/plugin &lt;span class="nb"&gt;install &lt;/span&gt;spar-x-skills@spar-x-skills
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/spar-x-fr/spar-x-skills/tree/main/skills/rightsize" rel="noopener noreferrer"&gt;github.com/spar-x-fr/spar-x-skills&lt;/a&gt;, maintained with Zakaria Khchiche.&lt;/p&gt;

&lt;p&gt;It costs nothing. But you can pay us in GitHub stars, the only currency Claude hasn't found a way to burn yet.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
    <item>
      <title>PHP forgot to forget. Node.js remembered the random.</title>
      <dc:creator>Ayoub Hammoudi</dc:creator>
      <pubDate>Fri, 23 Oct 2026 10:58:15 +0000</pubDate>
      <link>https://dev.to/ayoub_hammoudi/php-forgot-to-forget-nodejs-remembered-the-random-ice</link>
      <guid>https://dev.to/ayoub_hammoudi/php-forgot-to-forget-nodejs-remembered-the-random-ice</guid>
      <description>&lt;p&gt;On 21 October, PHP and Node.js published security advisories on the same day. Two unrelated bugs in two runtimes that run a large part of the web. Read side by side, they are the same bug turned inside out: one runtime kept something it had promised to throw away, the other froze something it had promised would never repeat.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHP forgot to forget
&lt;/h2&gt;

&lt;p&gt;PHP's oldest promise is that every request starts clean. Nothing survives from one request to the next. It is why most of us stopped thinking about memory the day we wrote our first &lt;code&gt;&amp;lt;?php&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;CVE-2026-109619, "RCE via stale global state" (CVSS 9.2), breaks that promise. PHP keeps global state for errors, sessions, objects and extensions, and some of it can point into memory owned by the current request. That state has to be reset before the request memory is released.&lt;/p&gt;

&lt;p&gt;A fatal error can skip the reset. So can running out of memory, which ends the request the same way. The request heap is freed, the worker's global still points at it, and a later request that lands on the same worker can read a pointer into memory that no longer belongs to anyone.&lt;/p&gt;

&lt;p&gt;Its sibling, CVE-2026-109618, "RCE via OOM corruption" (also 9.2), sits on the same exit path. PHP leaves an out-of-memory error with &lt;code&gt;longjmp()&lt;/code&gt;, and some code was never written to be abandoned halfway. The fix says a lot: PHP no longer jumps out on the spot. It raises a soft out-of-memory error at the next safe point in the VM, and keeps an immediate hard &lt;code&gt;_exit()&lt;/code&gt; for a higher threshold.&lt;/p&gt;

&lt;p&gt;The fault was never in the happy path. It was in the moment everyone treats as over: the crash.&lt;/p&gt;

&lt;h2&gt;
  
  
  Node.js remembered the random
&lt;/h2&gt;

&lt;p&gt;Node's bug runs the other way. A startup snapshot freezes the heap of a warmed-up process so the next start can skip the warm-up. Single executable applications can ship one too.&lt;/p&gt;

&lt;p&gt;CVE-2026-106074 describes what happens when that heap already holds random data. &lt;code&gt;crypto.randomUUID()&lt;/code&gt; and &lt;code&gt;randomInt()&lt;/code&gt; keep a cache of pre-generated entropy, the snapshot saves it, and every process started from it repeats the same values.&lt;/p&gt;

&lt;p&gt;I wanted to see it, so I tried it on the Node 22.16.0 I had installed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// entry.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:v8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;startupSnapshot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDeserializeMainFunction&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ node --snapshot-blob snap.blob --build-snapshot entry.js
$ for i in 1 2 3; do node --snapshot-blob snap.blob; done
ef9885a2-6c01-4412-abfd-85d0ea1888aa
ef9885a2-6c01-4412-abfd-85d0ea1888aa
ef9885a2-6c01-4412-abfd-85d0ea1888aa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is something quietly insulting about reading the same "random" UUID three times.&lt;/p&gt;

&lt;p&gt;Then I counted. Across two processes started from the same snapshot, the first 127 UUIDs were identical, and so were the first 1023 values from &lt;code&gt;randomInt()&lt;/code&gt;. The 128th UUID was different.&lt;/p&gt;

&lt;p&gt;Those numbers are not an accident. &lt;code&gt;randomUUID()&lt;/code&gt; fills a buffer for 128 UUIDs at a time (&lt;code&gt;kBatchSize = 128&lt;/code&gt;). One was used before the snapshot, so 127 were frozen with the heap. &lt;code&gt;randomInt()&lt;/code&gt; reads 6 bytes per attempt from a 6 KiB cache: 1024 slots, one used, 1023 frozen.&lt;/p&gt;

&lt;p&gt;The fix is short. While Node builds a snapshot, it registers a callback that zero-fills both caches and resets their offsets after user serialization, and again before user deserialization, because microtasks and exit handlers can refill them in between.&lt;/p&gt;

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

&lt;p&gt;Neither bug lives in code anybody reads twice. "Every request starts clean" and "random is different every time" are not features. They are assumptions so old that nobody writes them down, and you can't test what you never wrote down.&lt;/p&gt;

&lt;p&gt;Both become cheap to test once you say them out loud. Build the snapshot, start it twice, assert the UUIDs differ. Kill request N with a fatal error and look at what request N+1 sees.&lt;/p&gt;

&lt;p&gt;Mostly it reminded me where to look. The expensive bugs rarely sit in the path we watch. They wait in the moments we consider finished: the crash, the shutdown, the cleanup, the boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PHP: 8.2.35, 8.3.36, 8.4.27, 8.5.12&lt;/li&gt;
&lt;li&gt;Node.js: 22.23.4, 24.22.1, 26.12.1&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you ship single executable applications, rebuild them: an SEA carries its own copy of Node, so upgrading the one on the machine does not patch it. Snapshot blobs have to be rebuilt anyway, since Node refuses to load a blob made by a different binary.&lt;/p&gt;

&lt;p&gt;Sources: the &lt;a href="https://nodejs.org/en/blog/vulnerability/october-2026-security-releases" rel="noopener noreferrer"&gt;Node.js October 2026 security releases&lt;/a&gt;, and the php-src advisories &lt;a href="https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp" rel="noopener noreferrer"&gt;GHSA-6wmf-4rv5-p3rp&lt;/a&gt; and &lt;a href="https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w" rel="noopener noreferrer"&gt;GHSA-w8hw-w9fx-j79w&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I am Ayoub Hammoudi, a backend and AI platforms engineer in Paris. More of my work is at &lt;a href="https://github.com/Ayoubhm07" rel="noopener noreferrer"&gt;github.com/Ayoubhm07&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>php</category>
      <category>node</category>
      <category>security</category>
      <category>javascript</category>
    </item>
    <item>
      <title>My first Apache Kafka PR deleted three lines. I almost deleted the wrong one.</title>
      <dc:creator>Ayoub Hammoudi</dc:creator>
      <pubDate>Tue, 20 Oct 2026 19:43:07 +0000</pubDate>
      <link>https://dev.to/ayoub_hammoudi/my-first-apache-kafka-pr-deleted-three-lines-i-almost-deleted-the-wrong-one-1f9b</link>
      <guid>https://dev.to/ayoub_hammoudi/my-first-apache-kafka-pr-deleted-three-lines-i-almost-deleted-the-wrong-one-1f9b</guid>
      <description>&lt;p&gt;Deleting a duplicate line is the safest change in software. Nothing can break, because nothing changes. I believed that right up until I measured it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The door
&lt;/h2&gt;

&lt;p&gt;Apache Kafka does not run its CI for a first-time contributor. A pull request from someone the project has never merged gets a labeler and a triage bot, and the real build waits until a committer approves it. When I looked, one first-timer's pull request had been sitting for five days with two check runs, no comment and no review.&lt;/p&gt;

&lt;p&gt;So a first pull request there is not a chance to impress anyone. It is a chance to be trusted once.&lt;/p&gt;

&lt;p&gt;Before I found anything, I chased three families of defects and came back empty. A scanner for malformed SLF4J placeholders across 3,911 production files found zero. Leftover JUnit 4 imports: zero files. And a family of &lt;code&gt;ByteBuffer.array()&lt;/code&gt; fixes, where five pull requests from three contributors were stuck on one question from a committer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did we verify if this is a real-world scenario before drafting this PR?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That question stayed with me for everything that followed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The find
&lt;/h2&gt;

&lt;p&gt;What survived was small. In &lt;code&gt;build.gradle&lt;/code&gt;, three test dependencies were declared twice inside the same block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;core              libs.apachedsCoreApi        lines 1221 and 1230
connect:runtime   project(':server-common')   lines 3842 and 3847
connect:runtime   project(':server')          lines 3840 and 3848
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gradle resolves a configuration as a set, so the second declaration does nothing. Delete it and move on.&lt;/p&gt;

&lt;p&gt;Except that every pair has two lines, and you have to pick one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wrong one
&lt;/h2&gt;

&lt;p&gt;For &lt;code&gt;core&lt;/code&gt; I picked line 1221. It looked like the stray one, alone between two unrelated dependencies, while 1230 sat neatly at the head of the &lt;code&gt;apacheds*&lt;/code&gt; group. Keeping 1230 made the file look tidier.&lt;/p&gt;

&lt;p&gt;Then I did what that committer's question asks for. I generated the dependency report for both test configurations of both projects, before and after, and compared them.&lt;/p&gt;

&lt;p&gt;The resolved sets were identical: 73, 104, 125 and 171 modules, not one difference.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;:core&lt;/code&gt; report was not. Fifteen lines had moved. &lt;code&gt;org.apache.directory.api:api-all&lt;/code&gt; jumped from position 150 to 87, ahead of &lt;code&gt;apacheds-core-api&lt;/code&gt;. Removing line 1221 had pushed &lt;code&gt;apachedsCoreApi&lt;/code&gt; behind another declaration, and the report follows declaration order.&lt;/p&gt;

&lt;p&gt;On the JVM, order is not cosmetic. When two jars on a classpath ship the same class, the first one wins. These two artifacts both come from Apache Directory and probably share no classes.&lt;/p&gt;

&lt;p&gt;Probably is not a word I wanted to put in front of a reviewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The right one
&lt;/h2&gt;

&lt;p&gt;So I removed the later line of each pair instead: 1230, 3847 and 3848. Every declaration that remains keeps its position, and the classpath order stays exactly as it was.&lt;/p&gt;

&lt;p&gt;The four reports came back byte-identical, at 241, 418, 498 and 760 lines. Both modules compiled their tests, &lt;code&gt;check&lt;/code&gt; passed without the test task, &lt;code&gt;rat&lt;/code&gt; passed at the root, and &lt;code&gt;WorkerTest&lt;/code&gt; ran 140 tests with no failures as a spot check on the connect side.&lt;/p&gt;

&lt;p&gt;The description ended up far longer than the diff. One paragraph in it explains why the later line and not the earlier one. It answers the question before anyone has to ask it.&lt;/p&gt;

&lt;p&gt;A committer approved it the next morning with two words, "Thanks, LGTM", and merged it a moment later.&lt;/p&gt;

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

&lt;p&gt;A deletion is not a non-event. It has a position, and in a build file position is behaviour. "It resolves to the same set" and "it builds the same classpath" are two different claims, and only the first one was true of my first choice.&lt;/p&gt;

&lt;p&gt;The other lesson is about proportion. In a project you have never touched, the size of the change and the size of the proof have nothing to do with each other. A three line diff with a boring, byte-identical proof is easy to merge. The same diff with "should be a no-op" is a question a busy committer has no time to answer.&lt;/p&gt;

&lt;p&gt;The rule I keep now is simple. When you remove a duplicate declaration, remove the later one.&lt;/p&gt;

&lt;p&gt;The pull request is &lt;a href="https://github.com/apache/kafka/pull/23786" rel="noopener noreferrer"&gt;apache/kafka#23786&lt;/a&gt;. How I look for these defects in the first place is in &lt;a href="https://dev.to/ayoub_hammoudi/how-i-got-five-fixes-merged-into-prefect-sqlglot-apache-beam-and-microsoft-pyrit-by-asking-one-3m5p"&gt;an earlier post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I am Ayoub Hammoudi, a backend and AI platforms engineer in Paris. More of my work is at &lt;a href="https://github.com/Ayoubhm07" rel="noopener noreferrer"&gt;github.com/Ayoubhm07&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>gradle</category>
      <category>opensource</category>
      <category>kafka</category>
    </item>
    <item>
      <title>Playwright recorded my 25 fps animation at about 9. The fix was to stop recording.</title>
      <dc:creator>Ayoub Hammoudi</dc:creator>
      <pubDate>Mon, 19 Oct 2026 18:36:28 +0000</pubDate>
      <link>https://dev.to/ayoub_hammoudi/playwright-recorded-my-25-fps-animation-at-about-9-the-fix-was-to-stop-recording-5ac7</link>
      <guid>https://dev.to/ayoub_hammoudi/playwright-recorded-my-25-fps-animation-at-about-9-the-fix-was-to-stop-recording-5ac7</guid>
      <description>&lt;p&gt;I needed a short video of my portfolio for LinkedIn: the opening animation, a swordsman who cuts the screen in two, then a fast scroll through every section. Playwright records video in one option, so I started there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first video looked fine in stills
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;recordVideo&lt;/code&gt; on a browser context gave me a 25 fps WebM. Every frame I pulled out of it looked right. I had only checked stills. Played back, it stuttered, and the first person who watched it saw it at once.&lt;/p&gt;

&lt;p&gt;Measuring it frame by frame told the real story:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the opening: 65% of frames identical to the one before&lt;/li&gt;
&lt;li&gt;the swordsman: 63%&lt;/li&gt;
&lt;li&gt;the scroll: 79 of 200 transitions were a freeze followed by a jump&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is about 9 real frames per second inside a 25 fps file. The recorder is fed by the browser's screencast, which hands over a frame only when the compositor has one ready. On a heavy page it has fewer, and the gaps are filled with repeats. Nothing fails. The video is just wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop recording, start stepping
&lt;/h2&gt;

&lt;p&gt;If the browser cannot keep up in real time, take real time out of the equation. Every CSS animation and transition on a page is a Web Animation, reachable through &lt;code&gt;document.getAnimations()&lt;/code&gt;, and a paused animation can be placed at any instant by setting its &lt;code&gt;currentTime&lt;/code&gt;. So the capture becomes a loop: advance every animation by exactly 40 ms, take a screenshot, repeat. 25 frames per second by construction, however long each screenshot takes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// injected with context.addInitScript, before the page loads&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WeakMap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__step&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAnimations&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeline&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeline&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="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="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Animations that start later, like the swordsman when his IntersectionObserver fires, are picked up on the next step and join the same clock.&lt;/p&gt;

&lt;p&gt;Scrolling gets the same treatment: one &lt;code&gt;scrollTo&lt;/code&gt; per frame along an eased curve, instead of a smooth scroll that the recorder samples whenever it can.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;easeInOut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scrollTo&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;instant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;__step&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screenshot&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`frames/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;.jpg`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frames go to ffmpeg as an image sequence. The new video has zero frozen frames in the opening, the descent and the scroll.&lt;/p&gt;

&lt;h2&gt;
  
  
  The traps
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A timer that ignores your clock.&lt;/strong&gt; The opening has a 7-second safety timeout that removes it if its end event never arrives. Each captured frame costs far more than 40 ms of real time, so that timer would have fired long before the virtual clock got there. I neutralised that one timeout in the capture script. Anything driven by &lt;code&gt;setTimeout&lt;/code&gt; or &lt;code&gt;requestAnimationFrame&lt;/code&gt; runs on wall time, not on yours: the text-scramble effect on my headings plays faster in the video than on the site, and I left it that way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scroll-driven animations already have a clock.&lt;/strong&gt; Animations bound to &lt;code&gt;animation-timeline: view()&lt;/code&gt; follow the scroll position, so setting their &lt;code&gt;currentTime&lt;/code&gt; makes no sense. The loop skips anything whose timeline is not &lt;code&gt;document.timeline&lt;/code&gt;. Driving the scroll drives them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One frame can lie.&lt;/strong&gt; A section flashed white for a single frame. In real time nobody would see it. In a 25 fps video it reads as a glitch. A frame-difference pass over the whole video found it, and I cut it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The upload has opinions too.&lt;/strong&gt; LinkedIn's composer previewed my WebM, then dropped it as soon as I clicked Next. The same frames encoded as H.264 MP4, in &lt;code&gt;yuv420p&lt;/code&gt; with standard colour range, stayed.&lt;/p&gt;

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

&lt;p&gt;A screen recorder answers "what did the screen show?", and on a busy page the honest answer is "less than you asked for". For a demo, the better question is "what should the screen show at 40 ms, at 80, at 120?". The Web Animations API lets you ask it directly.&lt;/p&gt;

&lt;p&gt;And watch the playback, not the stills. Every still I checked was perfect.&lt;/p&gt;

&lt;p&gt;The 19-second result is in &lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7518005605320687618/" rel="noopener noreferrer"&gt;my LinkedIn post&lt;/a&gt;, and the site itself is at &lt;a href="https://ayoubhammoudi.com" rel="noopener noreferrer"&gt;ayoubhammoudi.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I am Ayoub Hammoudi, a full-stack engineer in Paris working on data and AI platforms. More of my work is at &lt;a href="https://github.com/Ayoubhm07" rel="noopener noreferrer"&gt;github.com/Ayoubhm07&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>playwright</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>css</category>
    </item>
    <item>
      <title>My fix was one line. Code review found two holes in it.</title>
      <dc:creator>Ayoub Hammoudi</dc:creator>
      <pubDate>Fri, 16 Oct 2026 16:48:20 +0000</pubDate>
      <link>https://dev.to/ayoub_hammoudi/my-fix-was-one-line-code-review-found-two-holes-in-it-k0m</link>
      <guid>https://dev.to/ayoub_hammoudi/my-fix-was-one-line-code-review-found-two-holes-in-it-k0m</guid>
      <description>&lt;p&gt;The bug I fixed in Microsoft Agent Framework did not crash anything. That is what made it worth fixing.&lt;/p&gt;

&lt;p&gt;When the framework called Gemini without streaming, the response knew which model had answered. When it made the same call with streaming, the model name could disappear on the way. The framework records its metrics through OpenTelemetry, and the model name is one of the dimensions those metrics are split by. On streamed calls, the token and duration histograms simply lost that dimension. Every request still succeeded. The per-model view of cost and latency just had a hole in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it happened
&lt;/h2&gt;

&lt;p&gt;Each streamed chunk from Gemini may or may not carry a &lt;code&gt;model_version&lt;/code&gt;. The streaming parser copied that field straight into the update, so a chunk without it produced an update without a model. The aggregator keeps the last non-empty value it sees, and there was nothing to catch up afterwards, so a stream whose chunks carried no version ended with no model at all.&lt;/p&gt;

&lt;p&gt;The non-streaming parser, thirty-odd lines above, already handled this: it fell back to the configured model when the response did not name one. Two paths, same input type, different answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line fix
&lt;/h2&gt;

&lt;p&gt;So I proposed the obvious thing: apply the same fallback on the streaming path. One line, two tests, parity with the sibling.&lt;/p&gt;

&lt;p&gt;It was wrong twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hole one
&lt;/h2&gt;

&lt;p&gt;I had raised this one myself, as an open question in the PR description: was the client's default model really the right fallback? The automated reviewer answered it. The fallback used the client's default model, not the model actually requested. A client configured for &lt;code&gt;gemini-2.5-flash&lt;/code&gt; and called with &lt;code&gt;gemini-2.5-pro&lt;/code&gt; for one request would label that request as Flash, metrics included. The model resolved for the request was already in scope where the stream was consumed, so the parser now takes it as an argument and falls back to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hole two
&lt;/h2&gt;

&lt;p&gt;Then the maintainer found the subtler one. Applying the fallback chunk by chunk could overwrite a version the server had already reported. If an early chunk said &lt;code&gt;gemini-2.5-pro-002&lt;/code&gt; and the final usage chunk carried no version, the aggregator, which keeps the last non-empty value, would replace the precise version with the requested alias &lt;code&gt;gemini-2.5-pro&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix had to remember what the server said for the whole stream, not chunk by chunk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;fallback_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response_stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;fallback_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_version&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_process_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fallback_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and in the parser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_version&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What merged
&lt;/h2&gt;

&lt;p&gt;Three iterations, four tests. One checks the version from a chunk is kept, one checks the fallback to the configured model, one the fallback to the requested model, and one replays the maintainer's exact case: an observed server version must survive a later chunk that omits it. The last one fails on the previous commit, which is the point of it.&lt;/p&gt;

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

&lt;p&gt;My first version was not careless. It matched the sibling path exactly, it had tests, and it fixed the reported symptom. It was still wrong, twice. The first hole I had seen and left as a question. The second came from the maintainer.&lt;/p&gt;

&lt;p&gt;That is the case for review in one PR. Not catching typos, but catching the second-order behaviour of a change: which value is in scope, what the aggregator does with it next, what a later chunk can undo. Observability bugs make this harder, because nothing fails. The only signal is a number that is quietly attached to the wrong thing, or to nothing.&lt;/p&gt;

&lt;p&gt;If you run LLM calls behind OpenTelemetry, it is worth one test that compares a streamed call with a non-streamed one and checks every attribute, not just the status code.&lt;/p&gt;

&lt;p&gt;The PR, with the full review thread: &lt;a href="https://github.com/microsoft/agent-framework/pull/9139" rel="noopener noreferrer"&gt;microsoft/agent-framework#9139&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am Ayoub Hammoudi, a backend and AI platforms engineer in Paris. More of my work is at &lt;a href="https://github.com/Ayoubhm07" rel="noopener noreferrer"&gt;github.com/Ayoubhm07&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>observability</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI did not erase deep understanding. It made it rare.</title>
      <dc:creator>Ayoub Hammoudi</dc:creator>
      <pubDate>Thu, 15 Oct 2026 12:56:49 +0000</pubDate>
      <link>https://dev.to/ayoub_hammoudi/ai-did-not-erase-deep-understanding-it-made-it-rare-3npd</link>
      <guid>https://dev.to/ayoub_hammoudi/ai-did-not-erase-deep-understanding-it-made-it-rare-3npd</guid>
      <description>&lt;p&gt;I hated AI.&lt;/p&gt;

&lt;p&gt;Not on principle. Because I had spent years digging. Stack Overflow at two in the morning, GitHub issues, reading a file until I understood why it actually broke rather than why it appeared to. Effort was the filter. It was the thing that separated the people who could from the people who could not, and I had paid for my side of that line.&lt;/p&gt;

&lt;p&gt;Then a kid with two months of experience started shipping a working project in five minutes.&lt;/p&gt;

&lt;p&gt;I want to be precise about what I felt, because I think a lot of people feel it and do not say it. It was not fear of being replaced. It was something smaller and uglier: the suspicion that the thing I had bought with my nights had just been handed out for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sentence that moved me
&lt;/h2&gt;

&lt;p&gt;Two people changed my mind. One was a teacher of mine. The other was a friend of his. Separately, weeks apart, they said the same thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You are used to a manual gearbox. Somebody just handed you an automatic. Your advantage is not that you can drive. It is that you know what is happening under the hood.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I nodded politely and did not understand it for months.&lt;/p&gt;

&lt;p&gt;What finally landed was the inversion hidden inside it. I had been reading the situation as a loss, as though a scarce skill had been devalued by mass production. The opposite was true. The skill had not been devalued. It had been made optional, which is a very different thing, and optional skills do not disappear. They become uncommon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scarcity, not erasure
&lt;/h2&gt;

&lt;p&gt;Before, everyone had to dig. There was no other route from a stack trace to a fix. Because the effort was compulsory, it was also ordinary. Everybody around you had paid the same price, so the price bought you nothing in particular.&lt;/p&gt;

&lt;p&gt;Today nobody is forced to. You can ship a feature without ever asking why the library behaves that way, and most of the time it works, which is exactly what makes it dangerous. The understanding is still there to be had. Almost nobody acquires it, because nothing in the daily loop requires it anymore.&lt;/p&gt;

&lt;p&gt;That is the whole shift, and it runs the other way from how it is usually told. The supply of people who understand deeply is shrinking. The demand for them has not moved. Anyone who says this makes expertise worthless has the arrow backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually looks like
&lt;/h2&gt;

&lt;p&gt;This is the part that sounds abstract until you have done it, so here is the concrete shape of it.&lt;/p&gt;

&lt;p&gt;A model will hand you code that is syntactically perfect, idiomatic, well named, and wrong in a way that will not show up for six months. It will confidently call a method that behaves differently than its name suggests. It will reproduce a pattern from the project's most-copied file without noticing that the file is the one everybody complains about. It will give you the answer that is most common on the internet, which is not the same as the answer that is correct here.&lt;/p&gt;

&lt;p&gt;None of that is caught by reading the output carefully. It is caught by already knowing the terrain. By having read the library, by recognising that two siblings in the same class disagree with each other, by having a sense of what this codebase does when it is healthy.&lt;/p&gt;

&lt;p&gt;That sense cannot be prompted into existence. It is the residue of having dug, and it is the only part of the job that is getting scarcer rather than cheaper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where that leaves me
&lt;/h2&gt;

&lt;p&gt;I use AI every day now. I stopped resisting it a while ago, and resisting it was never the interesting position anyway.&lt;/p&gt;

&lt;p&gt;But I am not ahead because I use it. Plenty of people use it. I am ahead, on the days when I am ahead, because I can tell when it is wrong, and because telling when it is wrong is a thing I can only do about the parts of the stack I actually took the time to learn.&lt;/p&gt;

&lt;p&gt;So the advice I would give my past self, the one who was quietly resentful watching a beginner ship in five minutes, is not "learn the tool". Everyone learns the tool. It is: keep digging in the places nobody is forced to dig anymore. That is where the margin moved.&lt;/p&gt;

&lt;p&gt;I am Ayoub Hammoudi, a backend and AI platforms engineer in Paris. More of my work is at &lt;a href="https://github.com/Ayoubhm07" rel="noopener noreferrer"&gt;github.com/Ayoubhm07&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>softwareengineering</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How I got seven fixes merged into Prefect, SQLGlot, Apache Beam, Kafka and Microsoft by asking one question</title>
      <dc:creator>Ayoub Hammoudi</dc:creator>
      <pubDate>Wed, 14 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/ayoub_hammoudi/how-i-got-five-fixes-merged-into-prefect-sqlglot-apache-beam-and-microsoft-pyrit-by-asking-one-3m5p</link>
      <guid>https://dev.to/ayoub_hammoudi/how-i-got-five-fixes-merged-into-prefect-sqlglot-apache-beam-and-microsoft-pyrit-by-asking-one-3m5p</guid>
      <description>&lt;p&gt;I had seven pull requests merged upstream: one in Prefect, one in SQLGlot, one in Apache Beam, two in Microsoft PyRIT, one in Microsoft Agent Framework and one in Apache Kafka. The largest was 78 added lines. Two of them added nothing at all and only deleted. The fastest went from issue to merge in 34 minutes.&lt;/p&gt;

&lt;p&gt;None of that came from reading code hoping something would look wrong. It came from one question, asked over and over:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does this project do the same thing more than once, and which one disagrees with the others?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Mature codebases are full of families. Dialect generators. Notification blocks. Scorers. Serializers. Every family is a contract implemented N times, and a contract implemented N times is a contract that drifts. When one sibling drifts, the other siblings become your oracle. You are no longer arguing about what the code should do. The project already answered that, N minus one times.&lt;/p&gt;

&lt;h2&gt;
  
  
  The clearest case: a seed that never reached the query
&lt;/h2&gt;

&lt;p&gt;SQLGlot transpiles SQL between dialects. Databricks has a &lt;code&gt;UNIFORM(min, max, seed)&lt;/code&gt; function. So does Snowflake. Both are generated by sibling classes.&lt;/p&gt;

&lt;p&gt;The Databricks parser stores that third argument in the &lt;code&gt;seed&lt;/code&gt; slot of &lt;code&gt;exp.Uniform&lt;/code&gt;. The Snowflake generator reads both &lt;code&gt;gen&lt;/code&gt; and &lt;code&gt;seed&lt;/code&gt;. The DuckDB generator reads only &lt;code&gt;gen&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So a query transpiled to DuckDB silently lost its seed. &lt;code&gt;ABS(HASH())&lt;/code&gt; hashed nothing. Seed 5, seed 99 and no seed at all produced identical SQL. No exception, no warning, and a result that looks completely plausible until you need reproducibility.&lt;/p&gt;

&lt;p&gt;Here is the part that matters for the method. I did not have to decide what the correct output was. I wrote in the pull request:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The two expected strings are not mine: &lt;code&gt;tests/dialects/test_snowflake.py:3312-3334&lt;/code&gt; already asserts them for the equivalent Snowflake input.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The project had already written down the right answer for the sibling. My test asserted the same strings for the dialect that got it wrong. A reviewer does not have to trust my judgement about SQL semantics. They only have to agree that two dialects reading the same parsed expression should not disagree.&lt;/p&gt;

&lt;p&gt;Issue opened at 15:46. Merged at 16:20. Thirty four minutes, for a three line change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same shape, three more times
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prefect.&lt;/strong&gt; Notification blocks let you set a bot name. One of them, &lt;code&gt;NotifyMattermost&lt;/code&gt;, accepted the setting and never passed it to the request. Its siblings all did. The fix was sixteen lines with a test, and the expected behaviour came from the other blocks, not from me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Microsoft PyRIT.&lt;/strong&gt; &lt;code&gt;SubStringScorer&lt;/code&gt; includes its text matcher's configuration in the scorer identity. &lt;code&gt;DecodingScorer&lt;/code&gt; holds the same kind of matcher, at the same point of the same method, and did not. The consequence is not cosmetic: two scorers that return opposite verdicts for the same input shared one identity hash, so cached evaluation metrics could be reused across different behaviour. The sibling had already been fixed in a pull request that was merged. I copied the two lines it added into the class that had been missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apache Beam.&lt;/strong&gt; This one breaks the pattern, and it is worth including for that reason. A debug log reading &lt;code&gt;### Labels&lt;/code&gt; arrived in the Python BigQuery connector with the commit that added BigQuery Read API support, merged on 6 October 2021. My one line removal merged on 6 October 2026. Five years to the day, in a release path, printing a dataset label dictionary at warning level on every temporary dataset setup. There was no sibling to compare against here. There was just a line that no release should have contained. Zero additions, one deletion, one approval, no requested changes.&lt;/p&gt;

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

&lt;p&gt;It is not scanning for theoretical defects.&lt;/p&gt;

&lt;p&gt;I learned that bluntly, and at my own expense. On another project I prepared a clean, exhaustive change that removed a parameter no code reads across seven model families. Every call site checked, every live use preserved, the project's own type checker measured at the version they pin, the full suite run. Only after all of that did I sit down and read their contributing guide properly. It says, in writing, that they will close changes like mine:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code agents often zero in on "theoretical bugs" found by code analysis that rarely if ever cause problems in practice. These are generally not worth fixing unless they can be exploited by an attacker.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had spent a twenty three hour test run before reading the one page that decides whether the work was admissible at all. I opened it anyway. A maintainer closed it two minutes after a bot flagged it for review, without a word, which is exactly what that page says they will do. The CI on it was entirely green: sixteen jobs, 199,469 tests, zero failures.&lt;/p&gt;

&lt;p&gt;That is a fair outcome and I would have done the same in their position. The lesson is not that the policy is wrong. It is that reading it costs ten minutes and belongs at minute zero, not after the suite.&lt;/p&gt;

&lt;p&gt;A maintainer of a different project put the same bar as a question on the issue tracker: &lt;em&gt;Did we verify if this is a real-world scenario before drafting this PR?&lt;/em&gt; He was reviewing one of five pull requests, from three contributors, all proposing the same category of change to the same family of classes. None of the five has been merged, and four are still open. Correctness of an API contract is not enough. Something has to actually go wrong.&lt;/p&gt;

&lt;p&gt;The sibling question clears that bar by construction. If one implementation out of five behaves differently with the same data in hand, the breakage is reachable by definition. Someone is on that path right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five questions, in the order I ask them
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where is the family?&lt;/strong&gt; The interface, the base class, the registry, the dialect map. Anything implemented more than three times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the project already state the right answer?&lt;/strong&gt; An existing test asserting it for the sibling, a docstring that promises it, a schema that declares it. If nothing in the repository states it, you are about to argue about taste with someone who has merge rights, and you will lose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are the siblings actually in the same situation?&lt;/strong&gt; This is where most candidates die, and it is the question people skip. If the sibling that behaves correctly never receives the data in question, it is not a witness, and you have no case at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does it fail before and pass after?&lt;/strong&gt; Write the test first, run it, keep the exact failure message. It costs one command and it is the most persuasive thing you can put in front of a reviewer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Did you run what they run, at the versions they pin?&lt;/strong&gt; Their linter version lives in &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt;, &lt;code&gt;setup.py&lt;/code&gt; or the CI workflow, not in your head. One number produced with a newer tool invites a reviewer to dismiss the entire report.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The part nobody writes about
&lt;/h2&gt;

&lt;p&gt;Everything above is the visible half. The other half is the work that produced nothing, and it is most of it.&lt;/p&gt;

&lt;p&gt;That repository was Kafka, and I chased three separate defect families there before finding anything worth proposing. I wrote a scanner for malformed log placeholders and ran it across 3,911 production files: zero cases. I grepped the whole tree for leftover JUnit 4 imports: zero files. The third was the family of five pull requests above, already dead on a bar a maintainer had set.&lt;/p&gt;

&lt;p&gt;What survived those three dead ends was three test dependencies declared twice in the same Gradle block. Zero lines added, three removed.&lt;/p&gt;

&lt;p&gt;That is the method working, not failing. Seven merges did not come from being prolific. They came from killing candidates early, before writing anything, so the few that reached a maintainer were ones they could not reasonably refuse. Five of the seven were under forty lines. The effort was never in the fixing.&lt;/p&gt;

&lt;p&gt;So if you take one thing from this: the skill being tested is not finding bugs. It is deciding, quickly and honestly, which of the bugs you found are worth another person's attention.&lt;/p&gt;

&lt;p&gt;Everything I shipped is public and linked below. Every claim in this post can be checked against it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/PrefectHQ/prefect/pull/23281" rel="noopener noreferrer"&gt;PrefectHQ/prefect#23281&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/tobymao/sqlglot/pull/8519" rel="noopener noreferrer"&gt;tobymao/sqlglot#8519&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/beam/pull/40430" rel="noopener noreferrer"&gt;apache/beam#40430&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/PyRIT/pull/3014" rel="noopener noreferrer"&gt;microsoft/PyRIT#3014&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/PyRIT/pull/3075" rel="noopener noreferrer"&gt;microsoft/PyRIT#3075&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/agent-framework/pull/9139" rel="noopener noreferrer"&gt;microsoft/agent-framework#9139&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apache/kafka/pull/23786" rel="noopener noreferrer"&gt;apache/kafka#23786&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am Ayoub Hammoudi, a backend and AI platforms engineer in Paris. More of this work is at &lt;a href="https://github.com/Ayoubhm07" rel="noopener noreferrer"&gt;github.com/Ayoubhm07&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>testing</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
