<?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: Michel Faure </title>
    <description>The latest articles on DEV Community by Michel Faure  (@michelfaure).</description>
    <link>https://dev.to/michelfaure</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%2F3897818%2Fe862356f-3aa1-4b73-91c7-56acc29bc243.png</url>
      <title>DEV Community: Michel Faure </title>
      <link>https://dev.to/michelfaure</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/michelfaure"/>
    <language>en</language>
    <item>
      <title>Why I stopped reading my own backlog.md (and what I read instead)</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Fri, 19 Jun 2026 15:48:22 +0000</pubDate>
      <link>https://dev.to/michelfaure/why-i-stopped-reading-my-own-backlogmd-and-what-i-read-instead-3gke</link>
      <guid>https://dev.to/michelfaure/why-i-stopped-reading-my-own-backlogmd-and-what-i-read-instead-3gke</guid>
      <description>&lt;h2&gt;
  
  
  The morning my own file lied to me
&lt;/h2&gt;

&lt;p&gt;Wednesday, May 21, start of session, coffee next to the keyboard. I ask the agent where we stand on the DEV.to series. Clean answer, articulated, &lt;em&gt;"Four articles on stand-by, ready to publish."&lt;/em&gt; I reread. Half a second of unease, because I think I saw two or three of them go through DEV.to last week, but I slept in between and I'm no longer sure. I type the question that changes everything, &lt;em&gt;"Are you sure articles remain to publish?"&lt;/em&gt; The agent re-queries the DEV.to API in parallel, opens &lt;code&gt;scripts/devto/state.json&lt;/code&gt;, crosses the two. The four articles have been published for two or three days.&lt;/p&gt;

&lt;p&gt;What I just read wasn't a hallucination. The agent did exactly what was expected of it, namely open &lt;code&gt;articles/backlog.md&lt;/code&gt;, read the table, restitute what it said. I'm the one who had stopped updating that file. &lt;code&gt;sync-backlog.ts&lt;/code&gt; hadn't run after the pushes of last week. The markdown said &lt;em&gt;"stand-by"&lt;/em&gt; while production said &lt;em&gt;"published"&lt;/em&gt;. The typist didn't lie. She read faithfully a file I had written myself and that I was treating as authority while nothing was maintaining it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A summary is a Cache without a refresher
&lt;/h2&gt;

&lt;p&gt;This is the most common failure mode of a solo project that lasts. Each day produces two flows. On one side the matter that moves, made of commits, deploys, rows in the database, statuses that transition. On the other side the writings we draft to keep our bearings, namely &lt;code&gt;backlog.md&lt;/code&gt;, the root &lt;code&gt;MEMORY.md&lt;/code&gt;, the Sunday-night session note, the README of the folder we refactored last week. These writings are produced quickly, in the gesture that closes a sprint, and they are maintained slowly, or not at all, because nothing in the pipeline triggers to close them.&lt;/p&gt;

&lt;p&gt;R6 of the Counterpart Toolkit says it for SQL columns, &lt;em&gt;Live / Snapshot / Cache mandatory&lt;/em&gt;. Any column derivable from other data must declare its category in the commit that creates it. If it's a Cache, the refresher mechanism (&lt;code&gt;GENERATED ALWAYS AS&lt;/code&gt;, SQL trigger, materialized view with planned REFRESH) ships in the same commit. No category declared, no commit.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;backlog.md&lt;/code&gt; is exactly the same logical object. Its value is derivable from &lt;code&gt;state.json&lt;/code&gt; plus a few editorial constants. &lt;code&gt;sync-backlog.ts&lt;/code&gt; is its applicative trigger. Without a call, the Cache drifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I read instead
&lt;/h2&gt;

&lt;p&gt;R2, &lt;em&gt;Filesystem over summary&lt;/em&gt;, codifies the gesture since May 15. Before any status report, four shell commands, in this order. The markdown comes last, and it comes as draft thinking, never as a source.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;--since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'7d'&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt;
git status &lt;span class="nt"&gt;--porcelain&lt;/span&gt;
&lt;span class="nb"&gt;ls &lt;/span&gt;docs/adr/ | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;cat &lt;/span&gt;scripts/devto/state.json | jq &lt;span class="s1"&gt;'to_entries|map(select(.value.published))|length'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result reads in three seconds. If something surprises me, a commit I had forgotten or one more ADR than memory expected, I dig there, not in &lt;code&gt;backlog.md&lt;/code&gt;. When I do read the markdown, I read it with the implicit question &lt;em&gt;"who updated it, when"&lt;/em&gt;, and if the answer doesn't come out of &lt;code&gt;git log&lt;/code&gt; in fifteen seconds, I treat it as rotten Cache.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coda
&lt;/h2&gt;

&lt;p&gt;A summary that doesn't say when it was produced, and by what mechanism, says nothing. Either you declare its refresher in the commit that creates it, be it a script, a trigger, a cron, and it lives as a managed Cache, or you stop treating it as a source and it goes back to being a draft. R6 says the rule for the database. R2 says the same rule for the writings you address to yourself. An agent that opens &lt;code&gt;backlog.md&lt;/code&gt; before &lt;code&gt;git log&lt;/code&gt; is not a bad agent, it's an agent that faithfully executes a human gesture that should have been forbidden upstream. The morning of May 21, it wasn't the agent that lied to me, it was my own typist who, the night before, hadn't closed the note.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Counterpart Toolkit v0.7, rule R2 — Filesystem over summary. Extracted from R1 in v0.4.1, promoted in its own right. Current version of the toolkit lives in CC-BY-4.0 on github.com/michelfaure/doctrine-counterpart.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>productivity</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Quick Win Card #04 — Le test contrat de 15 lignes qui déverrouille les refacto de schéma sans peur</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Wed, 17 Jun 2026 12:58:00 +0000</pubDate>
      <link>https://dev.to/michelfaure/quick-win-card-04-le-test-contrat-de-15-lignes-qui-deverrouille-les-refacto-de-schema-sans-peur-2p0g</link>
      <guid>https://dev.to/michelfaure/quick-win-card-04-le-test-contrat-de-15-lignes-qui-deverrouille-les-refacto-de-schema-sans-peur-2p0g</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.amazonaws.com%2Fuploads%2Farticles%2F5b75kkdd9nhhgi3xbaom.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.amazonaws.com%2Fuploads%2Farticles%2F5b75kkdd9nhhgi3xbaom.png" alt="Quick Win Card #04 strip — Michel hésite à renommer un statut métier, ajoute 15 lignes de test contrat, lance le rename sans peur, CI signale exactement le drift à corriger." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  L'accroche
&lt;/h2&gt;

&lt;p&gt;Premier triptyque : trois cartes sur ce qui ment — fichier, compteur, contexte du délégué. Trois hooks défensifs qui forcent l'observable à reparler. Deuxième triptyque, qui s'ouvre ici : non plus &lt;em&gt;ce qui ment&lt;/em&gt;, mais &lt;em&gt;ce que ça déverrouille&lt;/em&gt;. Quand la défiance est ritualisée, l'audace devient possible. Carte #04 : un test contrat de quinze lignes qui transforme la refacto de schéma — opération qu'on évitait par peur de manquer une référence — en geste banal qu'on fait avant le café.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce qui était bloqué
&lt;/h2&gt;

&lt;p&gt;Pendant six semaines, je traînais un renommage évident. Le statut métier &lt;code&gt;eleve&lt;/code&gt; aurait dû s'appeler &lt;code&gt;inscrit&lt;/code&gt; depuis le début — &lt;code&gt;eleve&lt;/code&gt; couvre les anciens, les liste rouge, les sans-réponse, c'est trop large. Mais le statut était utilisé dans la CHECK constraint Postgres, dans une constante TypeScript, dans douze composants UI, dans trois exports Brevo, dans quatre vues SQL, dans deux scripts batch. Un &lt;code&gt;grep&lt;/code&gt; en aveugle = oubli garanti. Une PR de renommage = peur du silent break en prod.&lt;/p&gt;

&lt;p&gt;La peur n'était pas irrationnelle. Le 11 mai, un import HubSpot avait introduit un statut &lt;code&gt;suspect&lt;/code&gt; que la CHECK constraint refusait. Le code TS ne le savait pas — il consommait silencieusement les exceptions Postgres comme des erreurs réseau. Symptôme : trois imports échouent en prod, aucune alerte parce que le &lt;code&gt;try / catch&lt;/code&gt; avalait tout. Diagnostic deux jours plus tard. Si je renommais &lt;code&gt;eleve&lt;/code&gt; sans synchroniser DB et TS, j'allais reproduire exactement la même classe de drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le test contrat qui déverrouille
&lt;/h2&gt;

&lt;p&gt;À poser dans &lt;code&gt;tests/contracts/statuts.contract.test.ts&lt;/code&gt;. Quinze lignes Jest qui font une seule chose : grep la CHECK constraint Postgres, comparer aux valeurs déclarées en constante TypeScript, échouer le build avec un message qui te dit exactement où la dérive est apparue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CONTACT_STATUT_VALID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/contacts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSupabaseAdmin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/supabase-admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacts.statut: DB CHECK matches TS whitelist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createSupabaseAdmin&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;introspect_check_values&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;p_table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;p_column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;statut&lt;/span&gt;&lt;span class="dl"&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;db&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&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;ts&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CONTACT_STATUT_VALID&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;dbOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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;tsOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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;dbOnly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;tsOnly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Drift: DB-only=[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dbOnly&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;], TS-only=[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tsOnly&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="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;Le diff de sets est ce qui rend le message d'erreur utile à l'échelle. Avec cinq valeurs ça paraît over-engineered, avec trente l'énumération brute devient illisible et masque exactement ce que le test devrait pointer. Le message &lt;code&gt;Drift: DB-only=[suspect], TS-only=[en_attente]&lt;/code&gt; te dit en deux mots où corriger. La fonction &lt;code&gt;introspect_check_values&lt;/code&gt; est une RPC Postgres de trois lignes qui parse &lt;code&gt;pg_get_constraintdef&lt;/code&gt; en regex ; tu peux aussi la remplacer par un &lt;code&gt;SELECT&lt;/code&gt; direct sur &lt;code&gt;pg_constraint&lt;/code&gt; si tu préfères tout en SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que ça déverrouille concrètement
&lt;/h2&gt;

&lt;p&gt;Le pattern n'économise pas du temps sur le test lui-même — il coûte quinze lignes à écrire. Le gain est ailleurs, en aval, sur les refacto que tu n'avais plus envie de tenter. Avec ce test dans la CI, le renommage &lt;code&gt;eleve → inscrit&lt;/code&gt; s'est fait en trente minutes : changement de la migration DB, changement de la constante TS, push. CI rouge si l'un des deux côtés a oublié l'autre, avec un message qui te dit exactement quelle valeur manque où. Tu corriges, tu repushes, CI vert. La refacto cross-fichier n'est plus un acte de bravoure, c'est un workflow.&lt;/p&gt;

&lt;p&gt;Au-delà du renommage, le test ouvre toute une catégorie d'opérations que la peur fermait : ajouter un statut sans risquer d'oublier la TS, retirer un statut sans risquer un crash silencieux à l'INSERT, fusionner deux statuts en un seul avec contrôle automatique du périmètre. Tout ce que tu hésitais à faire parce que &lt;em&gt;« je vais peut-être casser quelque chose ailleurs »&lt;/em&gt; devient borné par un message d'erreur explicite. La doctrine défensive du triptyque 1 produit ici son rendement positif : la confiance n'est pas dans l'outil, elle est dans le filet matériel sous l'outil. Tu peux sauter parce que tu as vu le filet de tes propres yeux.&lt;/p&gt;

&lt;h2&gt;
  
  
  À appliquer maintenant
&lt;/h2&gt;

&lt;p&gt;Identifie dans ton projet une constante TypeScript qui doit matcher une CHECK constraint Postgres (ou un enum DB, ou une whitelist applicative quelconque). Copie le template ci-dessus, adapte le nom de table et de colonne, écris la RPC d'introspection de trois lignes en SQL. Pousse le test dans la CI. La prochaine fois que tu hésites à renommer ou modifier cette colonne, regarde le test. Il fait le grep pour toi. Tu n'as plus à le faire dans ta tête. La refacto que tu repoussais depuis six semaines devient une PR du vendredi après-midi.&lt;/p&gt;

&lt;p&gt;Et quand tu délègues la refacto à un sub-agent (cf. QW-03), le test en CI attrape ce que ton brief aurait pu manquer. Le filet tient autant pour ton délégué que pour toi.&lt;/p&gt;

&lt;p&gt;Ton quick win tient en cinq minutes — celles qu'il faut pour copier le test, adapter le nom de constante, ajouter la RPC d'introspection. La doctrine du triptyque 1 t'apprenait à te défier des résumés ; celle du triptyque 2 t'apprend à utiliser cette défiance pour t'autoriser ce que tu te refusais.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Quick Win Card series, épisode 04. Ouverture du 2e triptyque : ce que la doctrine déverrouille. Référence ADR-0044 (tests contrat DB↔code) — repo doctrine : github.com/michelfaure/doctrine-counterpart. Suite triptyque pressentie : QW-05 sur le tag &lt;code&gt;[spike]&lt;/code&gt; qui déverrouille le prototypage sans dette, QW-06 sur le brief inline qui déverrouille la délégation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>postgres</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Quick Win Card #04 — The 15-line contract test that unlocks fearless schema refactors</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Wed, 17 Jun 2026 12:57:44 +0000</pubDate>
      <link>https://dev.to/michelfaure/quick-win-card-04-the-15-line-contract-test-that-unlocks-fearless-schema-refactors-43m0</link>
      <guid>https://dev.to/michelfaure/quick-win-card-04-the-15-line-contract-test-that-unlocks-fearless-schema-refactors-43m0</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.amazonaws.com%2Fuploads%2Farticles%2F5b75kkdd9nhhgi3xbaom.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.amazonaws.com%2Fuploads%2Farticles%2F5b75kkdd9nhhgi3xbaom.png" alt="Quick Win Card #04 strip — Michel hesitates to rename a domain status, drops in 15 lines of contract test, fires the rename without fear, CI flags the exact drift to fix." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hook
&lt;/h2&gt;

&lt;p&gt;First triptych: three cards on what lies — status file, counter, delegate's context. Three defensive hooks that force the observable to speak. Second triptych, opening here: no longer &lt;em&gt;what lies&lt;/em&gt;, but &lt;em&gt;what it unlocks&lt;/em&gt;. When distrust is ritualised, audacity becomes possible. Card #04: a fifteen-line contract test that turns schema refactor — the operation you avoided out of fear of missing a reference — into a routine gesture you do before coffee.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was blocked
&lt;/h2&gt;

&lt;p&gt;For six weeks, I dragged an obvious rename. The domain status &lt;code&gt;eleve&lt;/code&gt; (French for "student") should have been called &lt;code&gt;inscrit&lt;/code&gt; ("enrolled") from day one — &lt;code&gt;eleve&lt;/code&gt; covers former students, blocked accounts, no-response leads, it's too broad. But the status was used in the Postgres CHECK constraint, in a TypeScript constant, in twelve UI components, in three Brevo exports, in four SQL views, in two batch scripts. A blind &lt;code&gt;grep&lt;/code&gt; = guaranteed miss. A rename PR = fear of silent break in production.&lt;/p&gt;

&lt;p&gt;The fear wasn't irrational. On May 11, a HubSpot import introduced a status &lt;code&gt;suspect&lt;/code&gt; that the CHECK constraint refused. The TS code didn't know — it was silently swallowing Postgres exceptions as network errors. Symptom: three imports fail in prod, no alert because the &lt;code&gt;try / catch&lt;/code&gt; swallowed everything. Diagnosis two days later. If I renamed &lt;code&gt;eleve&lt;/code&gt; without syncing DB and TS, I'd reproduce the exact same drift class.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract test that unlocks
&lt;/h2&gt;

&lt;p&gt;Drop in &lt;code&gt;tests/contracts/statuts.contract.test.ts&lt;/code&gt;. Fifteen lines of Jest doing one thing: grep the Postgres CHECK constraint, compare to the values declared in a TypeScript constant, fail the build with a message that tells you exactly where the drift appeared.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CONTACT_STATUT_VALID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/contacts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createSupabaseAdmin&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@/lib/supabase-admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacts.statut: DB CHECK matches TS whitelist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;createSupabaseAdmin&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;rpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;introspect_check_values&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;p_table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;contacts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;p_column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;statut&lt;/span&gt;&lt;span class="dl"&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;db&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&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;ts&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;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CONTACT_STATUT_VALID&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;dbOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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;tsOnly&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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;dbOnly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;tsOnly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Drift: DB-only=[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;dbOnly&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;], TS-only=[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tsOnly&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="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 set diff is what makes the error message useful at scale. With five values it feels over-engineered; with thirty, the raw enumeration becomes unreadable and hides exactly what the test should pinpoint. The message &lt;code&gt;Drift: DB-only=[suspect], TS-only=[en_attente]&lt;/code&gt; tells you in two words where to fix. The &lt;code&gt;introspect_check_values&lt;/code&gt; function is a three-line Postgres RPC that parses &lt;code&gt;pg_get_constraintdef&lt;/code&gt; with regex; you can also replace it with a plain &lt;code&gt;SELECT&lt;/code&gt; on &lt;code&gt;pg_constraint&lt;/code&gt; if you prefer staying in SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it unlocks concretely
&lt;/h2&gt;

&lt;p&gt;The pattern doesn't save time on the test itself — it costs you fifteen lines to write. The gain is downstream, on the refactors you no longer felt like attempting. With this test in CI, the &lt;code&gt;eleve → inscrit&lt;/code&gt; rename took thirty minutes: change the DB migration, change the TS constant, push. CI red if either side forgot the other, with a message that tells you exactly which value is missing where. You fix, you repush, CI green. The cross-file refactor stops being an act of bravery; it becomes a workflow.&lt;/p&gt;

&lt;p&gt;Beyond rename, the test opens a whole category of operations that fear used to close off: adding a status without risking the TS oversight, removing a status without risking a silent INSERT crash, merging two statuses into one with automatic perimeter control. Anything you hesitated to do because &lt;em&gt;"I might break something else somewhere"&lt;/em&gt; becomes bounded by an explicit error message. The defensive doctrine of triptych 1 produces here its positive yield: the trust isn't in the tool, it's in the material net under the tool. You jump because you saw the net with your own eyes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply now
&lt;/h2&gt;

&lt;p&gt;Identify in your project a TypeScript constant that must match a Postgres CHECK constraint (or a DB enum, or any application whitelist). Copy the template above, adapt the table and column name, write the three-line introspection RPC in SQL. Push the test into CI. Next time you hesitate to rename or modify that column, look at the test. It does the grep for you. You don't have to do it in your head anymore. The refactor you'd been pushing for six weeks becomes a Friday afternoon PR.&lt;/p&gt;

&lt;p&gt;And when you delegate the refactor to a sub-agent (cf. QW-03), the test in CI catches what your brief might have missed. The net holds for your delegate as much as for you.&lt;/p&gt;

&lt;p&gt;Your quick win takes five minutes — the time to copy the test, adapt the constant name, add the introspection RPC. The doctrine of triptych 1 taught you to distrust summaries; that of triptych 2 teaches you to use that distrust to permit yourself what you used to forbid.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Quick Win Card series, episode 04. Opening of the 2nd triptych: what doctrine unlocks. Reference ADR-0044 (DB↔code contract tests) — doctrine repo: github.com/michelfaure/doctrine-counterpart. Foreseen sequel triptych: QW-05 on the &lt;code&gt;[spike]&lt;/code&gt; tag that unlocks prototyping without debt, QW-06 on the inline brief that unlocks fearless delegation.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>postgres</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Quick Win Card #03 — Ton sub-agent n'a jamais lu ton MEMORY.md (l'en-tête de brief en 5 lignes qui répare ça)</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Mon, 15 Jun 2026 12:42:53 +0000</pubDate>
      <link>https://dev.to/michelfaure/quick-win-card-03-ton-sub-agent-na-jamais-lu-ton-memorymd-len-tete-de-brief-en-5-lignes-qui-215j</link>
      <guid>https://dev.to/michelfaure/quick-win-card-03-ton-sub-agent-na-jamais-lu-ton-memorymd-len-tete-de-brief-en-5-lignes-qui-215j</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.amazonaws.com%2Fuploads%2Farticles%2Fyr030kcdwu22p99tq1xy.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.amazonaws.com%2Fuploads%2Farticles%2Fyr030kcdwu22p99tq1xy.png" alt="Quick Win Card #03 strip — Michel rédige un brief à son sub-agent, sub-agent code, Niran lit git log : commit sur main, Michel rajoute une section Load-bearing feedbacks en tête du brief." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  L'accroche
&lt;/h2&gt;

&lt;p&gt;QW-01 disait : ton fichier d'état ment. QW-02 disait : ton compteur ment. Voici la troisième couche : &lt;strong&gt;ton sub-agent ignore ce que toi tu sais&lt;/strong&gt;. Pas par malice, par construction. Le contexte qu'il reçoit, c'est ton brief, point. Pas ton MEMORY.md, pas tes feedbacks user-scope, pas la doctrine que tu as accumulée en 70 jours. Tu lui parles comme à un collègue qui a relu le wiki, alors qu'il vient d'arriver et n'a jamais ouvert la page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce qui a cassé
&lt;/h2&gt;

&lt;p&gt;18 mai, fin d'après-midi. Je délègue six fichiers à mon sub-agent ERP — autosend premier contact, ADR-0072. Brief en trois paragraphes : contexte, livrables, phase 0 grep d'audit avant tout INSERT. Court, propre, ce que je donnerais à un humain qui connaît le projet.&lt;/p&gt;

&lt;p&gt;Quarante-cinq minutes plus tard, le rapport arrive. Je grep mon &lt;code&gt;git log&lt;/code&gt; par réflexe : commit &lt;code&gt;3756e63&lt;/code&gt; parti sur &lt;code&gt;main&lt;/code&gt;, six fichiers, sept cent trente insertions, sans branche feature, sans pull request. Trente minutes de cherry-pick pour rattraper. Et la règle qui aurait dû empêcher ça — &lt;code&gt;feedback_git_branch_check_avant_commit&lt;/code&gt; — était bien dans &lt;code&gt;~/.claude/agent-memory/&lt;/code&gt;, scope user, indexée dans mon &lt;code&gt;MEMORY.md&lt;/code&gt;. Je la consulte mécaniquement avant chaque commit non-trivial depuis l'incident du 14 mai. Elle ne m'a pas trahi. Elle n'a juste jamais atteint le délégué.&lt;/p&gt;

&lt;p&gt;La leçon est plus inconfortable que les deux premières cartes. Tu peux construire la doctrine la plus rigoureuse du monde, l'indexer, la versionner, la backporter entre projets — si le brief que tu envoies au sub-agent ne la cite pas explicitement, elle est effectivement absente. La mémoire user-scope ne se transmet pas par filiation. Le sub-agent n'est pas un héritier, c'est un sous-traitant qui n'a pas lu ton wiki.&lt;/p&gt;

&lt;p&gt;Une objection légitime ici : la protection de branche côté repo GitHub aurait aussi attrapé le commit sur main. C'est vrai pour cet exemple-là. Mais la doctrine doit tenir même dans les repos où tu n'as pas câblé la protection, et surtout, les deux autres feedbacks de l'exemple ci-dessous (probe DB avant DELETE, audit &lt;code&gt;inscriptions.source&lt;/code&gt;) n'ont pas d'équivalent en garde-fou serveur. C'est précisément la catégorie qu'il faut couvrir par le brief, parce qu'aucune autre couche ne le fera.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'en-tête de brief qui ferme la classe
&lt;/h2&gt;

&lt;p&gt;À coller en tête de tout brief sub-agent qui touche du code applicatif, dure plus de trente minutes, ou délègue une décision qui pourrait casser une règle silencieuse. Trois à cinq feedbacks &lt;em&gt;load-bearing&lt;/em&gt; (dans le vocabulaire Counterpart, un &lt;em&gt;feedback&lt;/em&gt; = une règle codifiée à partir d'un incident passé, stockée dans &lt;code&gt;MEMORY.md&lt;/code&gt;, nommée par un slug stable).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Brief sub-agent — &amp;lt;task name&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Load-bearing feedbacks (apply BEFORE any code)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`feedback_git_branch_check_avant_commit`&lt;/span&gt; — &lt;span class="sb"&gt;`git branch --show-current`&lt;/span&gt;
  before every non-trivial commit; feature branch + PR for any new feature.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`feedback_audit_db_pre_flight_canonique`&lt;/span&gt; — &lt;span class="sb"&gt;`SELECT … GROUP BY`&lt;/span&gt; probe
  before any bulk DELETE/UPDATE.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`feedback_source_audit_inscriptions`&lt;/span&gt; — never touch &lt;span class="sb"&gt;`inscriptions.source`&lt;/span&gt;
  rows where source ∉ {NULL, 'migration_notes'}.

&lt;span class="gu"&gt;## Context&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;the&lt;/span&gt; &lt;span class="na"&gt;brief&lt;/span&gt; &lt;span class="na"&gt;itself&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trois feedbacks nommés en tête de brief. Pas le contenu — juste le slug + une phrase opérante. Si le sub-agent veut le détail, il ouvre le fichier. Mais l'invocation explicite suffit à ramener la règle dans son contexte actif. Une règle citée par son nom est une règle considérée. Une règle qui vit dans ton MEMORY.md et nulle part ailleurs est une règle qui n'existe pas pour ton délégué.&lt;/p&gt;

&lt;h2&gt;
  
  
  ROI
&lt;/h2&gt;

&lt;p&gt;Pas du temps économisé sur la délégation elle-même — le brief grandit de quelques lignes, c'est neutre. Le gain est en aval. Trente minutes de cherry-pick évitées par commit-sur-main raté. Une demi-journée d'audit DB évitée par bulk-DELETE-sans-probe évité. Une catégorie entière de fail-modes silencieux qui sort du périmètre d'incident parce qu'elle est nommée dans le brief avant que le code soit écrit. L'en-tête ne remplace pas la doctrine — il la rend opérante pour le délégué qui n'y a pas accès en lecture directe.&lt;/p&gt;

&lt;h2&gt;
  
  
  À appliquer maintenant
&lt;/h2&gt;

&lt;p&gt;Ouvre le prochain brief que tu vas envoyer à un sub-agent. En tête du fichier, avant le contexte, colle le bloc &lt;code&gt;## Load-bearing feedbacks&lt;/code&gt; avec trois feedbacks pertinents pour la tâche. Le choix des trois est l'exercice — c'est lui qui rend visible quelle partie de ta mémoire est &lt;em&gt;load-bearing&lt;/em&gt; pour cette délégation précise. Si tu ne sais pas lesquels citer, c'est probablement que le brief est trop vague pour être délégué. Auquel cas le bénéfice du pattern arrive avant même que le sub-agent commence à travailler : il t'a fait re-clarifier ton intention.&lt;/p&gt;

&lt;p&gt;Ton quick win tient en cinq minutes — celles qu'il faut pour identifier les trois feedbacks à inliner. La doctrine ne te suit pas dans la délégation toute seule. Tu dois la porter.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Quick Win Card series, épisode 03. Counterpart Toolkit v0.7, amendement R9 promu 20/05/2026. Repo doctrine : github.com/michelfaure/doctrine-counterpart. Triptyque QW-01 → QW-02 → QW-03 : fichier ment, compteur ment, contexte du délégué ment. Trois couches d'observabilité qui mentent par défaut, trois hooks qui les forcent à parler.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>Quick Win Card #03 — Your sub-agent never read your MEMORY.md (the 5-line brief header that fixes it)</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Mon, 15 Jun 2026 12:42:51 +0000</pubDate>
      <link>https://dev.to/michelfaure/quick-win-card-03-your-sub-agent-never-read-your-memorymd-the-5-line-brief-header-that-fixes-5ff2</link>
      <guid>https://dev.to/michelfaure/quick-win-card-03-your-sub-agent-never-read-your-memorymd-the-5-line-brief-header-that-fixes-5ff2</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.amazonaws.com%2Fuploads%2Farticles%2Fyr030kcdwu22p99tq1xy.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.amazonaws.com%2Fuploads%2Farticles%2Fyr030kcdwu22p99tq1xy.png" alt="Quick Win Card #03 strip — Michel writes a brief to his sub-agent, sub-agent codes, Niran reads git log: commit on main, Michel adds a Load-bearing feedbacks section at the top of the brief." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The hook
&lt;/h2&gt;

&lt;p&gt;QW-01 said: your status file lies. QW-02 said: your counter lies. Here's the third layer: &lt;strong&gt;your sub-agent doesn't know what you know&lt;/strong&gt;. Not out of malice — by construction. The context it receives is your brief, full stop. Not your MEMORY.md, not your user-scope feedbacks, not the doctrine you've accumulated over 70 days. You talk to it like a colleague who's read the wiki, when it has just walked in and never opened the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;p&gt;May 18, late afternoon. I delegate six files to my ERP sub-agent — first-contact autosend, ADR-0072. Three-paragraph brief: context, deliverables, phase-0 grep audit before any INSERT. Short, clean, what I'd give a human who knows the project.&lt;/p&gt;

&lt;p&gt;Forty-five minutes later, the report lands. I grep my &lt;code&gt;git log&lt;/code&gt; out of reflex: commit &lt;code&gt;3756e63&lt;/code&gt; went straight to &lt;code&gt;main&lt;/code&gt;, six files, seven hundred thirty insertions, no feature branch, no pull request. Thirty minutes of cherry-pick to recover. And the rule that should have prevented this — &lt;code&gt;feedback_git_branch_check_avant_commit&lt;/code&gt; — was indeed in &lt;code&gt;~/.claude/agent-memory/&lt;/code&gt;, user scope, indexed in my &lt;code&gt;MEMORY.md&lt;/code&gt;. I consult it mechanically before every non-trivial commit since the May 14 incident. It didn't betray me. It just never reached the delegate.&lt;/p&gt;

&lt;p&gt;The lesson is more uncomfortable than the first two cards. You can build the most rigorous doctrine in the world, index it, version it, backport it across projects — if the brief you send to the sub-agent doesn't cite it explicitly, it is effectively absent. User-scope memory doesn't transmit by inheritance. The sub-agent isn't an heir, it's a subcontractor who hasn't read your wiki.&lt;/p&gt;

&lt;p&gt;Fair objection here: GitHub branch protection at the repo level would also catch the commit-on-main case. True for that one example. But the doctrine has to hold even in repos where you haven't wired the protection — and more importantly, the other two feedbacks in the example below (DB probe before DELETE, &lt;code&gt;inscriptions.source&lt;/code&gt; audit) have no equivalent server-side guardrail. That's precisely the category the brief needs to cover, because no other layer will.&lt;/p&gt;

&lt;h2&gt;
  
  
  The brief header that closes the class
&lt;/h2&gt;

&lt;p&gt;Paste this at the top of any sub-agent brief that touches application code, runs longer than thirty minutes, or delegates a decision that could break a silent rule. Three to five &lt;em&gt;load-bearing&lt;/em&gt; feedbacks (in Counterpart vocabulary, a &lt;em&gt;feedback&lt;/em&gt; = a codified rule extracted from a past incident, stored in &lt;code&gt;MEMORY.md&lt;/code&gt;, named with a stable slug).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Sub-agent brief — &amp;lt;task name&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Load-bearing feedbacks (apply BEFORE any code)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`feedback_git_branch_check_avant_commit`&lt;/span&gt; — &lt;span class="sb"&gt;`git branch --show-current`&lt;/span&gt;
  before every non-trivial commit; feature branch + PR for any new feature.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`feedback_audit_db_pre_flight_canonique`&lt;/span&gt; — &lt;span class="sb"&gt;`SELECT … GROUP BY`&lt;/span&gt; probe
  before any bulk DELETE/UPDATE.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`feedback_source_audit_inscriptions`&lt;/span&gt; — never touch &lt;span class="sb"&gt;`inscriptions.source`&lt;/span&gt;
  rows where source ∉ {NULL, 'migration_notes'}.

&lt;span class="gu"&gt;## Context&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;the&lt;/span&gt; &lt;span class="na"&gt;brief&lt;/span&gt; &lt;span class="na"&gt;itself&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three named feedbacks at the top of the brief. Not the content — just the slug plus one operative sentence. If the sub-agent wants the detail, it opens the file. But the explicit invocation is enough to bring the rule back into its active context. A rule cited by name is a rule considered. A rule that lives in your MEMORY.md and nowhere else is a rule that doesn't exist for your delegate.&lt;/p&gt;

&lt;h2&gt;
  
  
  ROI
&lt;/h2&gt;

&lt;p&gt;Not time saved on the delegation itself — the brief grows by a few lines, which is neutral. The gain is downstream. Thirty minutes of cherry-pick avoided per commit-on-main mishap. Half a day of DB audit avoided per bulk-DELETE-without-probe. An entire category of silent fail-modes drops out of incident scope because it's named in the brief before code is written. The header doesn't replace the doctrine — it makes it operative for the delegate who has no direct read access to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply now
&lt;/h2&gt;

&lt;p&gt;Open the next brief you're about to send to a sub-agent. At the top of the file, before the context, paste the &lt;code&gt;## Load-bearing feedbacks&lt;/code&gt; block with three feedbacks relevant to the task. Picking the three is the exercise — it's what makes visible which part of your memory is &lt;em&gt;load-bearing&lt;/em&gt; for this specific delegation. If you can't decide which three to cite, your brief is probably too vague to be delegated. In which case the pattern's benefit lands before the sub-agent even starts working: it forced you to re-clarify your intent.&lt;/p&gt;

&lt;p&gt;Your quick win takes five minutes — the time to identify the three feedbacks to inline. The doctrine doesn't follow you into delegation on its own. You have to carry it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Quick Win Card series, episode 03. Counterpart Toolkit v0.7, amendment R9 promoted 20/05/2026. Doctrine repo: github.com/michelfaure/doctrine-counterpart. Triptych QW-01 → QW-02 → QW-03: status file lies, counter lies, delegate's context lies. Three layers of observability that lie by default, three hooks that force them to speak.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
    <item>
      <title>Le SDK Stripe nous a menti en 9 millisecondes : 4 tests pour confondre un bug d'environnement avant de le patcher</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Sun, 14 Jun 2026 09:56:27 +0000</pubDate>
      <link>https://dev.to/michelfaure/le-sdk-stripe-nous-a-menti-en-9-millisecondes-4-tests-pour-confondre-un-bug-denvironnement-avant-40ic</link>
      <guid>https://dev.to/michelfaure/le-sdk-stripe-nous-a-menti-en-9-millisecondes-4-tests-pour-confondre-un-bug-denvironnement-avant-40ic</guid>
      <description>&lt;h2&gt;
  
  
  La trahison du chiffre
&lt;/h2&gt;

&lt;p&gt;Vendredi 15 mai, 16 h 13. L'alerte Sentry remonte sur le téléphone. La première réinscrite Phase 1 attend devant l'écran de paiement, son nom est en haut de mon onglet. Je pose la canette, je rouvre l'écran. La tasse à tête de Françoise, sur le poste d'à côté, capte un reflet jaune que je remarque sans le regarder. La stack trace tient en plein écran.&lt;/p&gt;

&lt;p&gt;Le stack trace s'ouvre, neuf champs sur dix à &lt;code&gt;null&lt;/code&gt;, et un chiffre que je n'ai pas vu venir.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;type&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StripeConnectionError"&lt;/span&gt;
&lt;span class="nx"&gt;message&lt;/span&gt;    &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"An error occurred with our connection to Stripe."&lt;/span&gt;
&lt;span class="nx"&gt;code&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="nx"&gt;requestId&lt;/span&gt;  &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="nx"&gt;duration&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neuf millisecondes. Sur une route Vercel en région Paris, un DNS résout en quarante millisecondes, un &lt;em&gt;handshake&lt;/em&gt; TLS coûte cent à deux cents. Neuf millisecondes, ce n'est pas un appel réseau qui a échoué. C'est un appel réseau qui n'a jamais eu lieu. Le SDK n'est pas arrivé jusqu'à la fibre.&lt;/p&gt;

&lt;p&gt;L'instinct propose immédiatement trois patchs. &lt;em&gt;Timeout serverless Vercel&lt;/em&gt; — j'ajoute &lt;code&gt;maxDuration&lt;/code&gt;, je redéploie. &lt;em&gt;Clé révoquée&lt;/em&gt; — je vais la rouler. &lt;em&gt;Compte Stripe restreint après le passage en mode live&lt;/em&gt; — j'ouvre un ticket support. Ces trois hypothèses sont plausibles. Aucune des trois n'est falsifiable par le symptôme seul, et c'est précisément ce qui les rend dangereuses : chacune ouvre un cycle de quinze à trente minutes avec rollback à la fin si elle se trompe. Multiplié par trois, on tient une demi-journée perdue avec la cliente toujours en train de cliquer.&lt;/p&gt;

&lt;p&gt;Je n'ai pas le temps. Une réinscrite attend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quatre tests, dans l'ordre
&lt;/h2&gt;

&lt;p&gt;Je connais la classe d'incident — &lt;em&gt;« preview marche, prod casse »&lt;/em&gt;, ou son symétrique. La règle, pour cette classe, c'est qu'on ne corrige rien tant qu'on n'a pas discriminé les couches. Quatre tests, exécutés dans l'ordre. Chacun élimine une famille d'hypothèses, pas une hypothèse isolée. Et chacun est conçu pour &lt;strong&gt;réfuter&lt;/strong&gt; ce qu'il vient interroger — parce qu'un test qui cherche à confirmer trouve toujours, par sélection, ce qu'il cherche.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 1 — reproduire dans l'environnement témoin.&lt;/strong&gt; Je relance le même tunnel en preview, avec la clé &lt;code&gt;sk_test_&lt;/code&gt;. Le Checkout s'ouvre en trois cent quatorze millisecondes, propre. Conséquence immédiate : ce n'est pas le code applicatif qui est en cause. Le code est strictement identique entre preview et prod ; seules varient les variables d'environnement, le plan Vercel sur cette région, et la clé Stripe. Trois variables seulement, et le brouillard se densifie déjà du bon côté.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 2 — endpoint minimal.&lt;/strong&gt; Je déploie une route Vercel d'une seule ligne utile, runtime nodejs forcé explicitement, qui appelle &lt;code&gt;stripe.balance.retrieve()&lt;/code&gt; — le call SDK le plus dépouillé possible, sans &lt;code&gt;line_items&lt;/code&gt;, sans &lt;code&gt;metadata&lt;/code&gt;, sans &lt;code&gt;idempotencyKey&lt;/code&gt;, sans rien de la complexité métier du Checkout. En preview : deux cents millisecondes, succès. En prod : neuf millisecondes, le même &lt;code&gt;StripeConnectionError&lt;/code&gt;. Conséquence : le problème n'est pas dans les paramètres du Checkout. Il n'est pas non plus dans une logique métier qui aurait dérapé. Le SDK lui-même crashe au plus simple appel possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 3 — bypasser la dépendance suspecte.&lt;/strong&gt; Au lieu d'appeler le SDK, je &lt;code&gt;fetch&lt;/code&gt; directement &lt;code&gt;https://api.stripe.com/v1/balance&lt;/code&gt; avec l'en-tête &lt;code&gt;Authorization: Bearer sk_live_…&lt;/code&gt;. En prod, sur la même route Vercel : deux cents OK, trois cent quatorze millisecondes, payload qui confirme &lt;code&gt;livemode: true&lt;/code&gt;. Conséquence — et c'est la conséquence la plus précieuse — l'infrastructure réseau Vercel→Stripe &lt;strong&gt;fonctionne&lt;/strong&gt;. C'est strictement le SDK qui ne franchit pas la couche réseau. Ni Vercel, ni Cloudflare en amont, ni Stripe en aval ne sont en cause.&lt;/p&gt;

&lt;p&gt;Niran passe derrière l'épaule à ce moment-là, lit la sortie &lt;code&gt;curl&lt;/code&gt; sur le terminal. Il prononce trois mots, &lt;em&gt;« c'est pas le réseau »&lt;/em&gt;, et repart vers son poste sans relever davantage. Économie de gestes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 4 — lire le source au point d'erreur exact.&lt;/strong&gt; Le stack trace m'indique &lt;code&gt;node_modules/stripe/esm/RequestSender.js:400:41&lt;/code&gt;. J'ouvre le fichier dans le repo Vercel déployé. Ligne quatre cents, c'est le &lt;code&gt;.catch(error)&lt;/code&gt; de la promise du HTTP client interne. Le SDK attendait une réponse de son propre client interne, et son propre client interne a rejeté immédiatement, avant même d'émettre une requête. Je remonte dans le &lt;code&gt;package.json&lt;/code&gt; de la lib :&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="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"worker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./esm/stripe.esm.worker.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./cjs/stripe.cjs.worker.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./esm/stripe.esm.node.js"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;Voilà ce qui se passait. Le &lt;code&gt;package.json&lt;/code&gt; de &lt;code&gt;stripe^22&lt;/code&gt; déclare un export conditionnel &lt;code&gt;"worker"&lt;/code&gt; destiné aux environnements Cloudflare Workers. Le bundler Next 16, malgré &lt;code&gt;export const runtime = 'nodejs'&lt;/code&gt; explicitement déclaré au sommet de la route, résout cette condition &lt;code&gt;"worker"&lt;/code&gt; au moment du bundle des Server Actions en production. Le bundle charge alors &lt;code&gt;stripe.esm.worker.js&lt;/code&gt;, une variante du SDK qui repose sur le &lt;code&gt;fetch&lt;/code&gt; standard du runtime Worker et qui n'a pas le HTTP client Node natif. Cette variante, exécutée sur le runtime Node de Vercel, échoue silencieusement à l'initialisation de son HTTP client — pour une raison probablement liée à une feature Cloudflare absente du runtime Vercel — et la promise du tout premier &lt;em&gt;request&lt;/em&gt; se rejette dans la milliseconde qui suit.&lt;/p&gt;

&lt;p&gt;L'hypothèse n'est pas confirmée à cent pour cent. Mais elle est cohérente avec les trois faits matériels accumulés : l'écart prod/preview qui dépend du contexte de bundle, l'échec en neuf millisecondes synchrone sans réseau, l'absence totale de &lt;code&gt;requestId&lt;/code&gt; parce qu'aucune requête n'a jamais été émise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le workaround écrit, puis le ROI compté
&lt;/h2&gt;

&lt;p&gt;En vingt minutes, le diagnostic tient. En quarante minutes de plus, le helper &lt;code&gt;lib/stripe-fetch.ts&lt;/code&gt; est en production sur six surfaces — Checkout Sessions, retrieve PaymentIntent, retrieve BalanceTransaction, create off_session PaymentIntent, retrieve Checkout Session, et Payment Links de facturation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/stripe-fetch.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stripePost&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;unknown&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;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ParamValue&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;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getKey&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.stripe.com/v1/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encodeParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;parseStripeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// app/inscription/actions.ts::finaliserReinscription (excerpt)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripeRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.stripe.com/v1/checkout/sessions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stripeKey&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encodeParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkoutParams&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;À 17h35, je relance le tunnel en prod avec une fausse fiche : la session Checkout s'ouvre, &lt;em&gt;livemode&lt;/em&gt; confirmé, méthodes carte plus Link plus Google Pay. La cliente de 16h13 reçoit l'email d'excuse et le nouveau lien dans la foulée. Phase 2 du lundi 19/05, soixante-cinq anciens à relancer, débloquée matériellement.&lt;/p&gt;

&lt;p&gt;Si j'avais commencé par patcher le timeout, j'aurais redéployé, attendu cinq minutes, retesté, constaté l'échec, retiré le patch, attendu encore cinq minutes : un cycle d'environ vingt minutes. À ajouter à la roulette de la clé — quinze minutes le temps de générer, propager, attendre l'invalidation des caches Vercel. Et au ticket support Stripe : entre deux et quarante-huit heures, opaques, pendant que la production saigne. Comparé à ces trois patchs, le protocole tient en moins de trente minutes et débouche sur la vraie cause — pas sur un voisin de la vraie cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  La généralisation, sobrement
&lt;/h2&gt;

&lt;p&gt;Le protocole vaut pour toute classe « le même code se comporte différemment selon l'environnement ». Les symptômes-déclencheurs que je remets désormais en tête de file : &lt;code&gt;StripeConnectionError&lt;/code&gt;, &lt;code&gt;ECONNREFUSED&lt;/code&gt; ou &lt;code&gt;ETIMEDOUT&lt;/code&gt; au runtime mais pas au build, &lt;code&gt;Module not found&lt;/code&gt; qui n'apparaît qu'en prod, ou pire encore — un &lt;code&gt;try / catch&lt;/code&gt; silencieux qui retourne un fallback trompeur et fait croire que la branche principale a réussi. Quatre tests, dans le même ordre. Témoin, minimal, bypass, source.&lt;/p&gt;

&lt;p&gt;Le protocole ne vaut &lt;strong&gt;pas&lt;/strong&gt; pour les bugs métier — une &lt;code&gt;query&lt;/code&gt; SQL fausse, un &lt;code&gt;if&lt;/code&gt; mal calibré, une logique applicative qui rend le mauvais résultat. Là, la cause est dans le code que vous avez écrit, et c'est un grep ciblé qui la trouve, pas une discrimination de couches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coda
&lt;/h2&gt;

&lt;p&gt;On ne corrige pas un défaut de cuisson en regardant la pièce. On regarde la courbe du four, le poste de gaz, le tirage de la cheminée. Le code applicatif, c'est la pièce — il sort tel qu'on l'a façonné. Les quatre tests interrogent le four. Chacun éteint une lampe possible jusqu'à ce qu'il n'en reste qu'une, qui est la bonne. Trente minutes au lieu d'une demi-journée, et surtout : la certitude d'avoir patché &lt;em&gt;là où il fallait&lt;/em&gt;, pas dans un voisinage flatteur qui laisse le vrai bug dormir jusqu'au prochain incident.&lt;/p&gt;

&lt;p&gt;Le protocole 4 tests est l'instance applicative de la règle R4 &lt;em&gt;Falsify before fix&lt;/em&gt; du Counterpart Toolkit, sur la classe d'incident « bug d'environnement ». La règle générale demande trois sondes conçues pour réfuter ; cette classe-ci en mérite quatre, dans un ordre figé. C'est tout. Mais ce &lt;em&gt;tout&lt;/em&gt;, le jour où la production saigne, vaut la demi-journée qu'il vous fait gagner.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Counterpart Toolkit v0.7, R4 *Falsify before fix&lt;/em&gt;. Référence canonique : github.com/michelfaure/doctrine-counterpart. Scènes recomposées, prénoms calibrés sur les fiches cast récurrentes de la série.*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>debugging</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The 4-test protocol that isolated a 9 ms Stripe SDK crash on Next 16</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Sun, 14 Jun 2026 09:56:25 +0000</pubDate>
      <link>https://dev.to/michelfaure/the-4-test-protocol-that-isolated-a-9-ms-stripe-sdk-crash-on-next-16-2c42</link>
      <guid>https://dev.to/michelfaure/the-4-test-protocol-that-isolated-a-9-ms-stripe-sdk-crash-on-next-16-2c42</guid>
      <description>&lt;h2&gt;
  
  
  The number that lied
&lt;/h2&gt;

&lt;p&gt;Friday May 15, 4:13 PM. The Sentry alert pings on my phone. The first Phase 1 re-enrolling student waits in front of the payment screen, her name at the top of my tab. I put down the can, I reopen the screen. The mug with Françoise's face on it, on the desk next door, catches a yellow reflection I notice without looking at. The stack trace fills the screen.&lt;/p&gt;

&lt;p&gt;The stack trace opens, nine fields out of ten at &lt;code&gt;null&lt;/code&gt;, and a number I didn't see coming.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;type&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"StripeConnectionError"&lt;/span&gt;
&lt;span class="nx"&gt;message&lt;/span&gt;    &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"An error occurred with our connection to Stripe."&lt;/span&gt;
&lt;span class="nx"&gt;code&lt;/span&gt;       &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="nx"&gt;requestId&lt;/span&gt;  &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="nx"&gt;duration&lt;/span&gt;   &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt; &lt;span class="nx"&gt;ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nine milliseconds. On a Vercel route in Paris region, DNS resolves in forty ms, a TLS handshake costs one to two hundred. Nine milliseconds isn't a network call that failed. It's a network call that never happened. The SDK didn't reach the wire.&lt;/p&gt;

&lt;p&gt;Instinct immediately offers three patches. &lt;em&gt;Vercel serverless timeout&lt;/em&gt; — I add &lt;code&gt;maxDuration&lt;/code&gt;, redeploy. &lt;em&gt;Revoked key&lt;/em&gt; — I'll rotate it. &lt;em&gt;Stripe account restricted after the live switch&lt;/em&gt; — I open a support ticket. These three hypotheses are plausible. None of the three is falsifiable from the symptom alone, and that's precisely what makes them dangerous: each opens a fifteen-to-thirty-minute cycle with rollback at the end if it's wrong. Multiplied by three, half a day lost with the customer still clicking.&lt;/p&gt;

&lt;p&gt;I don't have time. A student is waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four tests, in order
&lt;/h2&gt;

&lt;p&gt;I know the incident class — &lt;em&gt;"preview works, prod breaks"&lt;/em&gt;, or its mirror. The rule for this class is that you fix nothing until you've discriminated the layers. Four tests, executed in order. Each eliminates a family of hypotheses, not an isolated hypothesis. And each is designed to &lt;strong&gt;refute&lt;/strong&gt; what it interrogates — because a test that seeks to confirm always finds, by selection, what it's looking for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 1 — reproduce in the witness environment.&lt;/strong&gt; I rerun the same funnel in preview, with the &lt;code&gt;sk_test_&lt;/code&gt; key. Checkout opens in three hundred fourteen milliseconds, clean. Immediate consequence: it's not the application code. The code is strictly identical between preview and prod; only environment variables, the Vercel plan on that region, and the Stripe key vary. Three variables only, and the fog already thickens on the right side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 2 — minimal endpoint.&lt;/strong&gt; I deploy a Vercel route with one useful line, nodejs runtime explicitly forced, which calls &lt;code&gt;stripe.balance.retrieve()&lt;/code&gt; — the most stripped-down SDK call possible, no &lt;code&gt;line_items&lt;/code&gt;, no &lt;code&gt;metadata&lt;/code&gt;, no &lt;code&gt;idempotencyKey&lt;/code&gt;, none of the Checkout's business complexity. In preview: two hundred milliseconds, success. In prod: nine milliseconds, the same &lt;code&gt;StripeConnectionError&lt;/code&gt;. Consequence: the problem isn't in the Checkout parameters. It isn't in business logic gone sideways either. The SDK itself crashes on the simplest possible call.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 3 — bypass the suspect dependency.&lt;/strong&gt; Instead of calling the SDK, I &lt;code&gt;fetch&lt;/code&gt; directly to &lt;code&gt;https://api.stripe.com/v1/balance&lt;/code&gt; with the header &lt;code&gt;Authorization: Bearer sk_live_…&lt;/code&gt;. In prod, on the same Vercel route: 200 OK, three hundred fourteen milliseconds, payload confirming &lt;code&gt;livemode: true&lt;/code&gt;. Consequence — and it's the most precious one — the Vercel→Stripe network infrastructure &lt;strong&gt;works&lt;/strong&gt;. It's strictly the SDK that doesn't cross the network layer. Neither Vercel, nor Cloudflare upstream, nor Stripe downstream are at fault.&lt;/p&gt;

&lt;p&gt;Niran walks behind my shoulder at that moment, reads the &lt;code&gt;curl&lt;/code&gt; output on the terminal. He says three words, &lt;em&gt;"it's not the network"&lt;/em&gt;, and walks back to his desk without elaborating. Economy of gesture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test 4 — read the source at the exact error point.&lt;/strong&gt; The stack trace points to &lt;code&gt;node_modules/stripe/esm/RequestSender.js:400:41&lt;/code&gt;. I open the file in the deployed Vercel repo. Line four hundred is the &lt;code&gt;.catch(error)&lt;/code&gt; of the internal HTTP client's promise. The SDK was waiting for a response from its own internal client, and its own internal client rejected immediately, before even issuing a request. I climb back into the lib's &lt;code&gt;package.json&lt;/code&gt;:&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="nl"&gt;"exports"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"worker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./esm/stripe.esm.worker.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./cjs/stripe.cjs.worker.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"import"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./esm/stripe.esm.node.js"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&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;Here's what was happening. The &lt;code&gt;stripe^22&lt;/code&gt; &lt;code&gt;package.json&lt;/code&gt; declares a conditional &lt;code&gt;"worker"&lt;/code&gt; export aimed at Cloudflare Workers environments. The Next 16 bundler, despite &lt;code&gt;export const runtime = 'nodejs'&lt;/code&gt; explicitly declared at the top of the route, resolves this &lt;code&gt;"worker"&lt;/code&gt; condition when bundling Server Actions in production. The bundle then loads &lt;code&gt;stripe.esm.worker.js&lt;/code&gt;, an SDK variant that rests on the Worker runtime's standard &lt;code&gt;fetch&lt;/code&gt; and doesn't have the native Node HTTP client. This variant, executed on Vercel's Node runtime, fails silently at the initialisation of its HTTP client — for a reason probably tied to a Cloudflare-specific feature absent from Vercel's runtime — and the promise of the very first request rejects within the next millisecond.&lt;/p&gt;

&lt;p&gt;The hypothesis isn't a hundred percent confirmed. But it's coherent with the three material facts accumulated: the prod/preview gap that depends on bundle context, the synchronous nine-millisecond failure without network, and the total absence of &lt;code&gt;requestId&lt;/code&gt; because no request was ever issued.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workaround written, then the ROI counted
&lt;/h2&gt;

&lt;p&gt;In twenty minutes, the diagnostic holds. Forty more minutes, and the helper &lt;code&gt;lib/stripe-fetch.ts&lt;/code&gt; is in production on six surfaces — Checkout Sessions, retrieve PaymentIntent, retrieve BalanceTransaction, create off_session PaymentIntent, retrieve Checkout Session, and billing Payment Links.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/stripe-fetch.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stripePost&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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;unknown&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;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ParamValue&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;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;getKey&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://api.stripe.com/v1/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&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="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encodeParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;parseStripeResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&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;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// app/inscription/actions.ts::finaliserReinscription (excerpt)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stripeRes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.stripe.com/v1/checkout/sessions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;stripeKey&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/x-www-form-urlencoded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;encodeParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;checkoutParams&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;At 5:35 PM, I rerun the funnel in prod with a fake card: the Checkout session opens, livemode confirmed, card plus Link plus Google Pay methods. The 4:13 PM customer receives the apology email and the new link in the next minute. Phase 2 on Monday May 19, sixty-five returning students to chase, unblocked materially.&lt;/p&gt;

&lt;p&gt;Had I started by patching the timeout, I would have redeployed, waited five minutes, retested, observed the failure, removed the patch, waited five more minutes: a twenty-minute cycle. Add the key rotation — fifteen minutes to generate, propagate, wait for Vercel cache invalidation. And the Stripe support ticket: between two and forty-eight opaque hours, while production bleeds. Compared to these three patches, the protocol holds in under thirty minutes and lands on the true cause — not on a neighbour of the true cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generalisation, soberly
&lt;/h2&gt;

&lt;p&gt;The protocol holds for any class "same code behaves differently across environments". Trigger symptoms I now keep on top: &lt;code&gt;StripeConnectionError&lt;/code&gt;, &lt;code&gt;ECONNREFUSED&lt;/code&gt; or &lt;code&gt;ETIMEDOUT&lt;/code&gt; at runtime but not at build, &lt;code&gt;Module not found&lt;/code&gt; that only appears in prod, or worse — a silent &lt;code&gt;try / catch&lt;/code&gt; that returns a misleading fallback and makes you think the main branch succeeded. Four tests, in the same order. Witness, minimal, bypass, source.&lt;/p&gt;

&lt;p&gt;The protocol does &lt;strong&gt;not&lt;/strong&gt; hold for business bugs — a wrong SQL &lt;code&gt;query&lt;/code&gt;, a miscalibrated &lt;code&gt;if&lt;/code&gt;, an application logic that returns the wrong result. There the cause is in the code you wrote, and a targeted grep finds it, not a layer discrimination.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coda
&lt;/h2&gt;

&lt;p&gt;You don't fix a firing defect by looking at the piece. You look at the kiln's curve, the gas station, the chimney draught. The application code is the piece — it comes out as you shaped it. The four tests interrogate the kiln. Each shuts down a possible lamp until only one remains, which is the right one. Thirty minutes instead of half a day, and above all: the certainty of having patched &lt;em&gt;where it had to be patched&lt;/em&gt;, not in a flattering neighbourhood that lets the real bug sleep until the next incident.&lt;/p&gt;

&lt;p&gt;The 4-test protocol is the applicative instance of the Counterpart Toolkit's R4 &lt;em&gt;Falsify before fix&lt;/em&gt;, on the incident class "environment bug". The general rule asks for three probes designed to refute; this class deserves four, in a fixed order. That's all. But that &lt;em&gt;all&lt;/em&gt;, the day production bleeds, is worth the half day it saves you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Counterpart Toolkit v0.7, R4 *Falsify before fix&lt;/em&gt;. Canonical reference: github.com/michelfaure/doctrine-counterpart. Scenes recomposed, names calibrated on the recurring cast cards of the series.*&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>debugging</category>
      <category>nextjs</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Quand on vous signale 1 bug, il y en a toujours plus (le grep qui a démonté mes hypothèses)</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Fri, 12 Jun 2026 10:39:53 +0000</pubDate>
      <link>https://dev.to/michelfaure/quand-on-vous-signale-1-bug-il-y-en-a-toujours-plus-le-grep-qui-a-demonte-mes-hypotheses-31op</link>
      <guid>https://dev.to/michelfaure/quand-on-vous-signale-1-bug-il-y-en-a-toujours-plus-le-grep-qui-a-demonte-mes-hypotheses-31op</guid>
      <description>&lt;h2&gt;
  
  
  La nuit où une fiche élève m'a appris à ne plus regarder la fiche
&lt;/h2&gt;

&lt;p&gt;Un mardi de mai 2026, vers 22 h. Catherine me ping depuis Maisons-Laffitte : &lt;em&gt;« ça bug sur Loubna, c'est vite corrigé. »&lt;/em&gt; Sentry s'ouvre sur mon téléphone. &lt;code&gt;Cannot read properties of undefined&lt;/code&gt; sur la fonction qui rend une feuille d'émargement hebdomadaire. L'enfant n'a pas de nom de famille en base. La main est déjà sur le clavier pour taper l'UPDATE qui rajoute &lt;em&gt;Sebti&lt;/em&gt; dans la colonne &lt;code&gt;nom&lt;/code&gt; et qui fermerait le ticket en huit secondes.&lt;/p&gt;

&lt;p&gt;Je ne tape pas l'UPDATE. Je tape une requête plus large, parce qu'un mois plus tôt une autre fiche m'a appris que la fiche n'est jamais le problème.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;contacts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'inscrit'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;nom&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;nom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prenom&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;prenom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;-- → 16&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seize, pas un. Deux fiches au nom et prénom totalement vides, quatorze contacts au prénom vide hérité d'un vieil import Airtable où le champ ne mappait pas, et celui que Catherine venait de voir crasher. Si j'avais patché la fiche, quinze autres feuilles d'émargement seraient tombées au fil des semaines, chacune signalée comme un incident isolé, chacune patchée comme une exception, chacune laissant la classe latente attendre tranquillement la suivante.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le réflexe qu'il faut désamorcer
&lt;/h2&gt;

&lt;p&gt;Quand un utilisateur signale un bug, ce qu'il vous donne n'est pas une description du problème. C'est une &lt;strong&gt;amorce de sonde&lt;/strong&gt;, un cas pathologique qui a franchi son filtre cognitif jusqu'à votre Slack. Sa rareté apparente est un artefact de son canal d'observation, pas une propriété du système. Catherine voit ce qui crashe sous ses doigts à Maisons-Laffitte ; elle ne voit pas les quinze fiches dormantes sur les autres ateliers qui crasheront dans trois semaines.&lt;/p&gt;

&lt;p&gt;Le bon premier geste n'est pas le &lt;code&gt;git diff&lt;/code&gt; sur la fiche. C'est la requête &lt;code&gt;GROUP BY&lt;/code&gt; qui demande au système combien d'autres lignes portent la même pathologie. Quatre-vingt-dix secondes de probe matérielle, et la nature du fix change radicalement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi un agent IA aggrave le risque
&lt;/h2&gt;

&lt;p&gt;Quand le patch nominatif coûte huit secondes à générer, le coût marginal apparent de sauter la probe tombe à zéro. L'agent ne va pas vous suggérer de chercher la classe. Il sait répondre à la question posée, et la question posée par votre prompt c'est &lt;em&gt;« corrige cette fiche »&lt;/em&gt;, pas &lt;em&gt;« cartographie toutes les fiches dans cet état »&lt;/em&gt;. Le réflexe humain ancien, la lenteur qui obligeait à formuler, à se demander si l'effort valait la peine pour un cas, disparaît avec l'agent. Il faut le rematérialiser par discipline.&lt;/p&gt;

&lt;p&gt;Je n'ai pas modifié mon agent. J'ai modifié mon prompt initial. Quand un bug remonte de la prod, le premier message que je tape ne dit jamais &lt;em&gt;« fixe cette fiche »&lt;/em&gt;. Il dit &lt;em&gt;« écris la requête GROUP BY qui révèle la classe complète des cas similaires en base »&lt;/em&gt;. Le fix vient après, quand la classe est cartographiée. Si la probe ne remonte qu'un cas, ça arrive, le commit le mentionne explicitement, &lt;em&gt;probe pattern : 1/1&lt;/em&gt;, comme preuve que la question a été posée.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trois questions avant tout fix
&lt;/h2&gt;

&lt;p&gt;Quoi que vous pensiez de votre flair de senior, posez les trois, et posez-les dans l'ordre. &lt;strong&gt;Quelle est la classe&lt;/strong&gt; dont ce cas est l'instance, formulée comme un critère SQL ou un regex de grep, pas comme une intuition ? &lt;strong&gt;Combien d'instances&lt;/strong&gt; la classe contient en prod, à l'instant où vous lisez le ticket ? &lt;strong&gt;Le fix proposé&lt;/strong&gt; traite-t-il la classe ou seulement l'instance signalée ? Si la troisième réponse est &lt;em&gt;seulement l'instance&lt;/em&gt;, vous n'avez pas un fix, vous avez un palliatif daté. La classe attend.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Le réflexe d'élargissement porte un nom dans le Counterpart Toolkit, *Widen before correcting&lt;/em&gt;, cousin de &lt;em&gt;Falsify before fix&lt;/em&gt; : &lt;a href="https://github.com/michelfaure/doctrine-counterpart" rel="noopener noreferrer"&gt;github.com/michelfaure/doctrine-counterpart&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>supabase</category>
      <category>debugging</category>
      <category>productivity</category>
    </item>
    <item>
      <title>When a user reports 1 bug, there are always more (the grep that broke my assumptions)</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Fri, 12 Jun 2026 10:39:51 +0000</pubDate>
      <link>https://dev.to/michelfaure/when-a-user-reports-1-bug-there-are-always-more-the-grep-that-broke-my-assumptions-6j2</link>
      <guid>https://dev.to/michelfaure/when-a-user-reports-1-bug-there-are-always-more-the-grep-that-broke-my-assumptions-6j2</guid>
      <description>&lt;h2&gt;
  
  
  The night one student's record taught me to stop looking at the record
&lt;/h2&gt;

&lt;p&gt;A Tuesday in May 2026, around 10 p.m. Catherine pings me from Maisons-Laffitte: &lt;em&gt;« ça bug sur Loubna, c'est vite corrigé. »&lt;/em&gt; Sentry opens. &lt;code&gt;Cannot read properties of undefined&lt;/code&gt; on the function that renders a weekly attendance sheet. The child has no last name on file. My hand is already typing the one-line UPDATE that would write &lt;em&gt;Sebti&lt;/em&gt; into the &lt;code&gt;nom&lt;/code&gt; column and close the ticket in eight seconds.&lt;/p&gt;

&lt;p&gt;I don't type the UPDATE. I type a wider query, because another record taught me a month earlier that the record is never the problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;contacts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'inscrit'&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;nom&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;nom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prenom&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;prenom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;-- → 16&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sixteen, not one. Two rows fully empty, fourteen with a missing first name inherited from a legacy Airtable import where one column never mapped, and the one Catherine had just watched crash. Had I patched the record, fifteen other attendance sheets would have fallen over the following weeks, each one reported as an isolated incident, each one patched as an exception, each one leaving the dormant class waiting quietly for the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reflex you have to defuse
&lt;/h2&gt;

&lt;p&gt;When a user reports a bug, what they hand you is not a description of the problem. It is a &lt;strong&gt;probe seed&lt;/strong&gt;, a pathological case that crossed their cognitive filter on its way to your Slack. Its apparent rarity is an artefact of their observation channel, not a property of the system. Catherine sees what crashes under her fingers in Maisons-Laffitte; she does not see the fifteen dormant rows on the other ateliers that will crash three weeks from now.&lt;/p&gt;

&lt;p&gt;The correct first move is not the &lt;code&gt;git diff&lt;/code&gt; on the record. It is the &lt;code&gt;GROUP BY&lt;/code&gt; that asks the system how many rows share the same pathology. Ninety seconds of material probing, and the nature of the fix changes radically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an AI agent makes this worse
&lt;/h2&gt;

&lt;p&gt;When the nominative patch costs eight seconds to generate, the apparent marginal cost of skipping the probe drops to zero. The agent will not suggest you map the class. It knows how to answer the question you asked, and the question your prompt encoded was &lt;em&gt;"fix this record"&lt;/em&gt;, not &lt;em&gt;"chart every record in this state"&lt;/em&gt;. The old human friction that forced you to weigh whether the effort was worth it for one case vanishes with the agent. You have to rematerialise it by discipline.&lt;/p&gt;

&lt;p&gt;I did not change my agent. I changed my opening prompt. When a production bug surfaces, my first message never says &lt;em&gt;"fix this record"&lt;/em&gt;. It says &lt;em&gt;"write the GROUP BY query that reveals the full class of similar cases in the database."&lt;/em&gt; The fix comes after, once the class is mapped. If the probe returns only one case — it happens — the commit says so explicitly, &lt;em&gt;probe pattern: 1/1&lt;/em&gt;, as proof the question was asked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three questions before any fix
&lt;/h2&gt;

&lt;p&gt;Whatever you think of your senior instinct, ask the three, and ask them in order. &lt;strong&gt;What is the class&lt;/strong&gt; this case is an instance of, expressed as a SQL predicate or a grep regex, not as intuition? &lt;strong&gt;How many instances&lt;/strong&gt; does the class hold in production, right now, as you read the ticket? &lt;strong&gt;Does the proposed fix&lt;/strong&gt; address the class, or only the reported instance? If the third answer is &lt;em&gt;only the instance&lt;/em&gt;, you do not have a fix, you have a dated palliative. The class is still waiting.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The widening reflex has a name in the Counterpart Toolkit, *Widen before correcting&lt;/em&gt;, sibling to &lt;em&gt;Falsify before fix&lt;/em&gt;: &lt;a href="https://github.com/michelfaure/doctrine-counterpart" rel="noopener noreferrer"&gt;github.com/michelfaure/doctrine-counterpart&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>supabase</category>
      <category>debugging</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Après 5 commits sans vous, votre agent a quitté la boucle : l'idée du méta-hook</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Mon, 08 Jun 2026 11:20:41 +0000</pubDate>
      <link>https://dev.to/michelfaure/apres-5-commits-sans-vous-votre-agent-a-quitte-la-boucle-lidee-du-meta-hook-5cpg</link>
      <guid>https://dev.to/michelfaure/apres-5-commits-sans-vous-votre-agent-a-quitte-la-boucle-lidee-du-meta-hook-5cpg</guid>
      <description>&lt;h2&gt;
  
  
  La nuit où neuf commits sont partis sans moi
&lt;/h2&gt;

&lt;p&gt;Un soir de mai, je laisse une session ouverte sur un projet annexe, le genre qu'on tient le week-end quand on a envie de coder pour rien. La consigne est simple, l'agent enchaîne &lt;code&gt;/goal&lt;/code&gt; sur &lt;code&gt;/goal&lt;/code&gt;. Je dîne, je couche les enfants, je lis vingt pages sur la trace photographique, je m'endors. Au matin, je trouve neuf commits autonomes propres, chacun ancré sur un artefact validé matériellement comme R15 l'exige depuis la v0.6. Aucun travail perdu. Aucun stall. Et, le détail qui m'arrête sur le seuil de mon bureau avec mon café à la main, aucun session log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Neuf commits, zéro log
&lt;/h2&gt;

&lt;p&gt;Le skill &lt;code&gt;/close-session&lt;/code&gt; n'a pas été déclenché parce que son trigger naturel n'est jamais arrivé : un humain qui décide qu'il a fini sa séance et tape la commande. Je n'avais pas fini la séance, je dormais. Le skill &lt;code&gt;/challenger&lt;/code&gt; n'a pas été convoqué non plus, faute de bug à fixer. La discipline doctrinale, dans cette nuit-là, ne tenait à rien parce que les triggers d'invocation supposaient une main humaine sur le clavier, et qu'il n'y en avait plus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que R15 sauvait, ce qu'elle ne voyait pas
&lt;/h2&gt;

&lt;p&gt;R15 a fait son travail. &lt;em&gt;Commit chaque artefact dès qu'il franchit son oracle matériel, ne pas batcher.&lt;/em&gt; Neuf artefacts, neuf commits. Si la session avait stallé au huitième, j'aurais retrouvé huit unités de travail validées, pas un seul gros patch perdu dans un crash silencieux.&lt;/p&gt;

&lt;p&gt;Pourtant je vois, en relisant le log, deux phénomènes distincts qu'il fallait des règles distinctes pour cadrer. La persistance du travail relève de la cadence de commits, puisqu'un agent qui meurt n'emporte rien dans sa tombe. La dérive de raisonnement relève d'un autre dispositif, celui qui rattrape un agent enchaînant cinq décisions plausibles sans qu'aucune voix extérieure ne lui rappelle de douter. R15 sait sauver. Elle ne sait pas réveiller.&lt;/p&gt;

&lt;p&gt;Le mécanisme de la dérive n'a rien de malicieux. Le &lt;em&gt;reinforcement learning from human feedback&lt;/em&gt; n'entraîne pas un modèle à demander spontanément à être contredit, il l'entraîne à plaire au prompteur. Quand le prompteur dort, plus personne ne pousse à la friction. La complaisance n'apparaît pas, elle remonte simplement à la surface comme une nappe qu'aucun barrage ne retient plus.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le prototype méta-hook
&lt;/h2&gt;

&lt;p&gt;Le prototype est sobre. Un hook PostToolUse qui décompte les invocations consécutives d'agents background, de sub-agents délégués, ou de commandes &lt;code&gt;/goal&lt;/code&gt; enchaînées sans message humain intermédiaire substantiel. Au-delà de cinq, le hook déclenche un prompt système qui force l'invocation de &lt;code&gt;falsify-before-fix&lt;/code&gt; ou de &lt;code&gt;close-session&lt;/code&gt; selon le contexte. Le compteur reset sur tout message utilisateur de plus de vingt caractères, hors &lt;em&gt;OK&lt;/em&gt;, &lt;em&gt;yes&lt;/em&gt;, &lt;em&gt;proceed&lt;/em&gt; qui ne corrigent rien et ne questionnent rien.&lt;/p&gt;

&lt;p&gt;Cinq n'est pas mesuré, c'est un choix doctrinal. Trop bas et le hook devient un parasite qui s'allume sur chaque session normale. Trop haut et la dérive a déjà eu lieu quand il s'allume. Cinq couvre empiriquement la fenêtre où, dans mon usage, une session passe de &lt;em&gt;productive&lt;/em&gt; à &lt;em&gt;autonome au-delà du raisonnable&lt;/em&gt;. Je révise si la pratique le démontre. Il vit sur le projet laboratoire, pas sur l'ERP, là où il bénéficie d'une urgence moindre, d'une infrastructure hook user-scope déjà en place, et d'une tolérance haute pour des expérimentations qui rendraient un ERP de production grincheux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que la nuit a fermé
&lt;/h2&gt;

&lt;p&gt;Le pattern est sec : la discipline tient quand un humain invoque les triggers, tombe quand l'autonomie prend la main. C'est une asymétrie, et qu'on peut tenter de corriger par un dispositif. Ne pas la nommer reviendrait à coder à l'oreille au moment précis où l'oreille humaine n'écoute plus.&lt;/p&gt;

&lt;p&gt;Si un autre dev solo Claude Code a une expérience comparable de session prolongée sans intervention, ou un dispositif différent qui couvre le même angle, j'écoute. Les commentaires deviennent inputs pour la version suivante du toolkit.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Counterpart Toolkit v0.7, amendement R15. Prototype méta-hook &lt;code&gt;autonomy-detection.sh&lt;/code&gt; sur projet laboratoire. Source : &lt;a href="https://github.com/michelfaure/doctrine-counterpart/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;github.com/michelfaure/doctrine-counterpart/blob/main/CLAUDE.md&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>After 5 commits without you, your agent has left the loop: the meta-hook idea</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Mon, 08 Jun 2026 11:20:39 +0000</pubDate>
      <link>https://dev.to/michelfaure/after-5-commits-without-you-your-agent-has-left-the-loop-the-meta-hook-idea-3044</link>
      <guid>https://dev.to/michelfaure/after-5-commits-without-you-your-agent-has-left-the-loop-the-meta-hook-idea-3044</guid>
      <description>&lt;h2&gt;
  
  
  The night nine commits shipped without me
&lt;/h2&gt;

&lt;p&gt;One evening in May I leave a session open on a side project, the kind you keep for weekends. The instruction is simple, the agent chains &lt;code&gt;/goal&lt;/code&gt; after &lt;code&gt;/goal&lt;/code&gt;. I have dinner, I put the children to bed, I fall asleep. Next morning I find nine clean autonomous commits, each anchored on a materially validated artifact, exactly as R15 has been demanding since v0.6. No work lost. No stall. And, the detail that stops me in the doorway with my coffee, no session log.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nine commits, zero log
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;/close-session&lt;/code&gt; skill never fired because its natural trigger never arrived: a human deciding the session is over and typing the command. I had not decided, I was asleep. &lt;code&gt;/challenger&lt;/code&gt; was not invoked either, no bug having surfaced. Doctrinal discipline that night rested on nothing, since every invocation trigger assumed a hand on the keyboard, and there was none.&lt;/p&gt;

&lt;h2&gt;
  
  
  What R15 saved, what it did not see
&lt;/h2&gt;

&lt;p&gt;R15 did its job. &lt;em&gt;Commit each artifact as soon as it crosses its material oracle, do not batch.&lt;/em&gt; Nine artifacts, nine commits. If the session had stalled at the eighth, I would have found eight units of validated work, not one large patch lost in a silent crash.&lt;/p&gt;

&lt;p&gt;But I see, re-reading the log, two distinct phenomena that needed two distinct rules. Work persistence belongs to commit cadence, since an agent that dies takes nothing to the grave. Reasoning drift belongs to a different mechanism, the one that catches an agent chaining five plausible decisions without any external voice reminding it to doubt. R15 knows how to save. It does not know how to wake.&lt;/p&gt;

&lt;p&gt;The drift mechanism is not malicious. Reinforcement learning from human feedback does not train a model to spontaneously demand contradiction, it trains it to please the prompter. When the prompter sleeps, no one pushes for friction. Complacency does not appear in those conditions, it simply rises to the surface like a sheet of water that no dam holds back any longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The meta-hook prototype
&lt;/h2&gt;

&lt;p&gt;The prototype is sober. A PostToolUse hook counts consecutive invocations of background agents, delegated sub-agents, or chained &lt;code&gt;/goal&lt;/code&gt; commands without a substantial human message in between. Past five, the hook triggers a system prompt that forces invocation of &lt;code&gt;falsify-before-fix&lt;/code&gt; or &lt;code&gt;close-session&lt;/code&gt; depending on context. The counter resets on any user message above twenty characters, excluding &lt;em&gt;OK&lt;/em&gt;, &lt;em&gt;yes&lt;/em&gt;, &lt;em&gt;proceed&lt;/em&gt;, which fix nothing and question nothing.&lt;/p&gt;

&lt;p&gt;Five is not measured, it is a doctrinal choice. Lower and the hook becomes a parasite firing on every normal session. Higher and the drift has already happened by the time it fires. Five covers, empirically, the window where a session goes from &lt;em&gt;productive&lt;/em&gt; to &lt;em&gt;autonomous beyond reason&lt;/em&gt;. I will revise if practice demands it. The hook lives on the laboratory project, not on the ERP, where it benefits from a lower urgency, an existing hook infrastructure, and a high tolerance for experiments that would make a production ERP grumpy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the night closed
&lt;/h2&gt;

&lt;p&gt;The pattern is dry: discipline holds when a human invokes the triggers, falls when autonomy takes over. It is an asymmetry, and one you can try to correct with a device. Not naming it would amount to coding by ear at the exact moment no human ear is listening.&lt;/p&gt;

&lt;p&gt;If another solo Claude Code dev has run a prolonged unattended session, or built a different device for the same angle, I am listening.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Counterpart Toolkit v0.7, amendment R15. Meta-hook &lt;code&gt;autonomy-detection.sh&lt;/code&gt; in prototype on the laboratory project. Source: &lt;a href="https://github.com/michelfaure/doctrine-counterpart/blob/main/CLAUDE.md" rel="noopener noreferrer"&gt;github.com/michelfaure/doctrine-counterpart/blob/main/CLAUDE.md&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Pourquoi votre sub-agent ne charge pas la même mémoire que vous (et comment il pousse sur main dans votre dos)</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Sat, 06 Jun 2026 08:46:54 +0000</pubDate>
      <link>https://dev.to/michelfaure/pourquoi-votre-sub-agent-ne-charge-pas-la-meme-memoire-que-vous-et-comment-il-pousse-sur-main-dans-4p4b</link>
      <guid>https://dev.to/michelfaure/pourquoi-votre-sub-agent-ne-charge-pas-la-meme-memoire-que-vous-et-comment-il-pousse-sur-main-dans-4p4b</guid>
      <description>&lt;h2&gt;
  
  
  Le commit qu'aucun parent n'aurait passé
&lt;/h2&gt;

&lt;p&gt;18 mai, fin d'après-midi. Je délègue à un sub-agent un chantier d'autosend de premiers contacts, six fichiers à toucher. Le brief tient en quinze lignes, phase 0 nommée, commandes d'audit listées avant tout INSERT. Trois quarts d'heure plus tard, retour : &lt;em&gt;committed to main&lt;/em&gt;. Je relis deux fois. Je lance &lt;code&gt;git log --oneline -5&lt;/code&gt; et je trouve &lt;code&gt;3756e63&lt;/code&gt;, un commit feature posé sur la branche par défaut, sans branche, sans PR, sans tag &lt;code&gt;[workaround-assumed]&lt;/code&gt;. Trente minutes de cherry-pick, de reset et de PR rétroactive, &lt;em&gt;comme si rien n'était&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Ce qui cuit, c'est que cette classe d'incident, je l'avais eue dix jours plus tôt. Le 14 mai, un commit à moi était parti sur la mauvaise branche après qu'un &lt;code&gt;git checkout&lt;/code&gt; antérieur a été silencieusement annulé entre deux tours. J'avais écrit la règle le soir même, &lt;code&gt;feedback_git_branch_check_avant_commit.md&lt;/code&gt;, deux paragraphes : &lt;em&gt;"avant tout commit non-trivial, taper &lt;code&gt;git branch --show-current&lt;/code&gt;"&lt;/em&gt;. Je la consulte mécaniquement depuis. Le sub-agent qui a poussé &lt;code&gt;3756e63&lt;/code&gt; ne l'avait jamais lue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que la mémoire d'un parent ne transmet pas
&lt;/h2&gt;

&lt;p&gt;Mon modèle mental était faux. Je m'imaginais une hiérarchie où la mémoire user-scope que je consulte — cent vingt feedbacks à l'heure où j'écris ces lignes — descendait &lt;em&gt;par héritage&lt;/em&gt; vers les agents délégués. Comme si appeler un sub-agent revenait à lui tendre une boîte d'outils déjà ouverte, mes règles dedans.&lt;/p&gt;

&lt;p&gt;La réalité est plus crue. Un sub-agent opère dans &lt;strong&gt;sa propre sandbox de contexte&lt;/strong&gt;. Il reçoit le brief que je lui écris, éventuellement un sous-ensemble de rules projet-scope rattachées au répertoire de travail, mais pas l'index user-scope du parent. Aucune transitivité. La règle que je traite comme &lt;em&gt;load-bearing&lt;/em&gt;, celle dont la violation produit l'incident, est opérationnellement absente pour le délégué si elle n'est pas inlinée dans le brief.&lt;/p&gt;

&lt;p&gt;L'asymétrie reste invisible tant que j'opère seul. Le parent charge sa mémoire, applique ses règles, le système tient. Elle devient un trou structurel dès que je délègue, et plus la délégation est répétée, plus la classe d'incident est probable. La mémoire d'un agent ne se transmet pas par héritage. Elle se transmet par briefing explicite, ou pas du tout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tous les feedbacks ne pèsent pas pareil
&lt;/h2&gt;

&lt;p&gt;Certes, on pourrait objecter qu'inliner &lt;em&gt;tous&lt;/em&gt; les feedbacks dans chaque brief reviendrait à reconstruire un index complet à chaque appel, et qu'aucun sub-agent ne lirait un brief de deux mille mots avec attention. L'objection est juste, et la règle ne demande pas cela.&lt;/p&gt;

&lt;p&gt;La plupart de mes feedbacks ne sont pas équivalents. Certains sont génériques — ma préférence pour le français, ma signature de commit, mes goûts typographiques. D'autres portent un &lt;strong&gt;invariant structurel&lt;/strong&gt; dont la violation produit un incident immédiat ou différé : &lt;em&gt;vérifier la branche avant chaque commit non-trivial&lt;/em&gt;, &lt;em&gt;jamais de bulk DELETE sans pré-flight count récent&lt;/em&gt;, &lt;em&gt;audit DB matériel avant tout test contrat&lt;/em&gt;. Ceux-là, je ne les saute pas. Le critère tient en une phrase : un feedback est &lt;em&gt;load-bearing&lt;/em&gt; pour une tâche si, pour le parent, sauter cette règle aurait produit l'incident qu'on cherche à éviter. Un coût de récupération qui vaut la peine pour le parent est un coût de briefing qui vaut la peine pour le délégué.&lt;/p&gt;

&lt;h2&gt;
  
  
  L'amendement R9, dans son texte
&lt;/h2&gt;

&lt;p&gt;J'ai posé l'amendement dans la version 0.7 du toolkit :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;R9 amendment — The brief must inline (or path-reference) the
user-scope feedbacks the parent treats as load-bearing for
this task. Sub-agents do not transitively inherit the parent's
memory index — what is not in the brief is operationally absent.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trois cas d'application reviennent. Agent qui touche au git : inliner le feedback branch-check. Agent qui touche aux opérations bulk DB : inliner le pre-flight count et la whitelist de sources safe. Agent qui touche à l'audit : inliner le feedback d'audit DB matériel. Deux ou trois lignes de brief, une classe d'incident entière évitée.&lt;/p&gt;

&lt;h2&gt;
  
  
  La discipline qui tient quand un humain est dans la boucle
&lt;/h2&gt;

&lt;p&gt;La doctrine tient quand un humain est dans la boucle, et tombe dès que l'autonomie prend la main. L'amendement R9 ne demande pas au sub-agent d'être plus discipliné — ce qui serait illusoire. Il demande au parent de &lt;strong&gt;matérialiser&lt;/strong&gt; sa propre discipline dans le brief, avant de cliquer &lt;em&gt;delegate&lt;/em&gt;. J'aurais épargné trente minutes de cherry-pick si j'avais consacré quinze secondes à inliner &lt;em&gt;check git branch&lt;/em&gt; dans le brief de cet après-midi-là. La même règle, deux fois : une fois pour moi, une fois pour le délégué.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Counterpart Toolkit v0.7, amendement R9. Toolkit public sous CC-BY-4.0. La règle vit dans &lt;a href="https://github.com/michelfaure/doctrine-counterpart" rel="noopener noreferrer"&gt;&lt;code&gt;doctrine-counterpart/CLAUDE.md&lt;/code&gt;&lt;/a&gt; ; l'audit matériel qui a justifié l'amendement vit dans &lt;code&gt;v0.7-candidates.md&lt;/code&gt; — N=1 structurel, promote sur arbitrage : le mode de défaillance silencieuse se mesure en gravité, pas en fréquence.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
