<?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>Forem: Michel Faure </title>
    <description>The latest articles on Forem by Michel Faure  (@michelfaure).</description>
    <link>https://forem.com/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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3897818%2Fe862356f-3aa1-4b73-91c7-56acc29bc243.png</url>
      <title>Forem: Michel Faure </title>
      <link>https://forem.com/michelfaure</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/michelfaure"/>
    <language>en</language>
    <item>
      <title>Deux IA d'accord = une source : la règle qui m'a évité un pipeline bâti sur du vide</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Sun, 24 May 2026 08:44:42 +0000</pubDate>
      <link>https://forem.com/michelfaure/deux-ia-daccord-une-source-la-regle-qui-ma-evite-un-pipeline-bati-sur-du-vide-4hem</link>
      <guid>https://forem.com/michelfaure/deux-ia-daccord-une-source-la-regle-qui-ma-evite-un-pipeline-bati-sur-du-vide-4hem</guid>
      <description>&lt;h2&gt;
  
  
  Une nuit, deux audits, une même note
&lt;/h2&gt;

&lt;p&gt;Le 17 mai au soir, je termine la version 0.4.1 du &lt;em&gt;Counterpart Toolkit&lt;/em&gt; et je décide de la soumettre à deux relectures externes. Je colle le manifesto et la quatorzaine de règles dans une session ChatGPT-4o, je colle exactement le même contenu dans Claude.ai sur le web. J'attends. Quelques minutes plus tard, les deux verdicts arrivent. Note 8/10 d'un côté. Note 8/10 de l'autre. Critiques quasi identiques sur l'apparat théorique (Bourdieu invoqué sans portée opérationnelle), suggestion identique de simplification, même angle sur la fraîcheur de l'instrumentation M1-M5. Mon réflexe initial tient trente secondes. &lt;em&gt;Deux relecteurs indépendants, même note, mêmes critiques, la doctrine est calibrée juste, je peux publier.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Et puis je m'arrête. Parce que quelque chose, dans cette convergence trop nette, sonne comme un baromètre acheté en double exemplaire chez le même fournisseur.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi deux IA convergentes ne sont pas deux mesures
&lt;/h2&gt;

&lt;p&gt;Je comprends assez vite ce que la convergence mesure. Deux modèles de langage entraînés sur des corpus qui se recouvrent à très large proportion, qu'il s'agisse d'articles techniques, de repos GitHub publics, de discussions Stack Overflow ou de blogs des dix dernières années, produisent des erreurs corrélées. Ce qu'ils ont en commun, c'est leur intersection d'apprentissage, pas la réalité externe que je leur soumets. Quand les deux trouvent que l'apparat théorique est disproportionné, je n'apprends pas que c'est vrai. J'apprends que c'est ce que la statistique partagée de leurs deux corpus reconnaît comme un défaut typique d'un texte de ce format.&lt;/p&gt;

&lt;p&gt;Certes, deux relecteurs humains qui convergent sont, eux, deux mesures séparées. Mais le parallèle est trompeur. Deux relecteurs humains ont des biographies disjointes, des lectures différentes, des écoles de pensée parfois opposées. Deux LLM partagent un substrat qui n'a pas cette texture. La corroboration croisée, en épistémologie classique, suppose l'indépendance des sources. Sur deux modèles statistiques entraînés sur les mêmes corpus, l'indépendance n'est pas donnée, elle est à démontrer, et elle ne l'est presque jamais.&lt;/p&gt;

&lt;p&gt;Cette intuition m'a probablement épargné quelques jours de re-écriture inutile. Mais elle restait spéculative. Je voulais des sondes matérielles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trois sondes en trois jours
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Première sonde, le soir même.&lt;/strong&gt; Un autre projet où je travaille en parallèle, un repo de game dev sur Godot, m'avait poussé à étudier &lt;code&gt;WebFetch&lt;/code&gt; la semaine précédente. Un assistant Claude m'avait affirmé que l'outil retournait &lt;em&gt;« le texte OCRé complet d'un PDF de 25 MB »&lt;/em&gt; et j'avais bâti un pipeline d'ingestion sur cette claim. Je relance la commande dans la session courante, juste pour vérifier. Sortie brute en clair dans la console, &lt;code&gt;maxContentLength size of 10485760 exceeded&lt;/code&gt;. La claim était techniquement impossible. Le pipeline reposait sur un mécanisme inexistant. Je n'avais pas testé parce que la formulation de l'assistant était confiante, structurée, et plausiblement vraie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deuxième sonde, le lendemain.&lt;/strong&gt; Même substrate game dev. Un audit conversationnel m'avait orienté vers un blog technique présenté comme &lt;em&gt;« contenant les 18 actions de régence du domaine »&lt;/em&gt;, pile la matière que je cherchais. Avant de scraper, je fais un &lt;code&gt;WebFetch&lt;/code&gt; sur l'index du blog. Retour brut, huit articles de chronique de développement, &lt;strong&gt;zéro article sur le sujet annoncé&lt;/strong&gt;. La claim était une hallucination cohérente. Le format &lt;em&gt;« blog d'untel contient X »&lt;/em&gt; est une combinaison statistique que le modèle produit volontiers, parce qu'elle est syntaxiquement plausible, sans qu'aucun mécanisme interne ne vérifie son existence factuelle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Troisième sonde, ce matin, sur la doctrine elle-même.&lt;/strong&gt; Un Claude externe partagé sur claude.ai a audité hier mon repo &lt;code&gt;doctrine-counterpart&lt;/code&gt; et affirmé, captures à l'appui, que &lt;em&gt;« 6 SKILL.md sur 6 ont un YAML frontmatter cassé »&lt;/em&gt;, diagnostic présenté avec haute confiance, justifiant un déclassement à 7,5/10. Je lance ce matin un &lt;code&gt;yaml.safe_load&lt;/code&gt; sur les douze SKILL.md du repo. Résultat brut, &lt;strong&gt;11/12 OK, 1/12 cassé&lt;/strong&gt;. Le défaut systémique annoncé n'existait pas. Le seul vrai cassé, l'évaluateur visuel ne l'avait pas isolé puisqu'il avait conclu &lt;em&gt;« défaut systémique »&lt;/em&gt; sans cas individuel. Ce qu'il avait vu, c'est le rendu Markdown de GitHub qui mange le frontmatter et l'affiche comme tableau, une particularité de rendu d'hôte qui n'a rien à voir avec l'état du source brut.&lt;/p&gt;

&lt;p&gt;Trois claims externes testées, trois falsifications. Pas une convergence partielle, pas une réfutation nuancée, trois claims sur trois tombées contre leur sonde. Le ratio aurait été indétectable sans test matériel.&lt;/p&gt;

&lt;h2&gt;
  
  
  La règle canonique, Am.R12 du &lt;em&gt;Counterpart Toolkit&lt;/em&gt; v0.7
&lt;/h2&gt;

&lt;p&gt;J'ai amendé R12 le 20 mai, deux jours après ces trois incidents. Le texte officiel.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;« Toute claim formulée par une IA externe (autre Claude, ChatGPT, sparring conversationnel) à propos (a) du comportement d'un outil concret que vous pouvez sonder, (b) du contenu d'une ressource externe, (c) de la structure d'un système dont la vérité de terrain est échantillonnable, doit être testée matériellement avant d'être prise comme input d'une décision d'architecture. Coût du test ≈ 1 commande shell. Coût de la croyance non testée = pipeline entièrement basé sur un mécanisme inexistant. Deux revues d'IA externes convergentes sur le même diagnostic = une source pour les besoins de R5, pas deux, la corroboration cross-substrate indépendante exige un humain ou une sonde mécaniquement distincte (logs, métriques, exécution échantillon). »&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Qui contesterait que deux relecteurs valent mieux qu'un seul ? Personne, et pourtant la formule mérite d'être démontée. Sur deux LLM, la convergence n'est pas une corroboration, c'est une &lt;em&gt;convergence orpheline&lt;/em&gt;, une intersection statistique sans extériorité vérifiable. Trois contre-arguments matériels, en réponse à l'objection que j'aurais entendue d'un lead tech méta-IA il y a six mois.&lt;/p&gt;

&lt;p&gt;D'abord, la statistique d'entraînement partagée. Deux modèles dont les corpus se recouvrent sur l'essentiel produisent des erreurs corrélées sur les mêmes plages syntaxiques. Leur accord mesure leur intersection d'apprentissage. Il semble que la convergence soit, dans ce cas, plus probable sur les énoncés &lt;em&gt;typiques&lt;/em&gt;, c'est-à-dire ceux que le pré-entraînement reconnaît comme bien formés, que sur les énoncés &lt;em&gt;vrais&lt;/em&gt;. Ce n'est pas la même chose.&lt;/p&gt;

&lt;p&gt;Ensuite, l'hallucination corrélée sur les outils et les ressources. Sur les claims portant sur le comportement d'un tool précis ou le contenu factuel d'une ressource externe, deux modèles tendent à halluciner le résultat le plus plausible statistiquement, souvent faux, presque toujours formulé avec assurance. Mes trois sondes en sont l'illustration brute.&lt;/p&gt;

&lt;p&gt;Enfin, le coût asymétrique. Une sonde matérielle coûte une commande shell, quinze secondes, parfois moins. Une décision d'architecture bâtie sur une convergence non testée peut coûter plusieurs jours-dev de re-do et un pipeline à refaire. R12 amendée arbitre ce coût asymétrique en rendant la sonde obligatoire avant tout commit qui prend la claim comme input.&lt;/p&gt;

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

&lt;p&gt;Le réflexe à acquérir n'est pas la défiance. C'est la sonde. Et la sonde commence par soi, en appliquant R12 d'abord à mes propres assertions avant de l'imposer aux autres, en vérifiant mon propre repo par &lt;code&gt;yaml.safe_load&lt;/code&gt; avant de croire un audit visuel qui flatte ou qui condamne. Un agent qui ne contredit pas matériellement n'est pas un counterpart, c'est une dactylo qui parle. Deux agents qui s'accordent sans qu'aucun ne soit testé ne forment pas deux relecteurs, c'est la même dactylo en duplicata. La règle tient en une commande.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Avant d'adopter une claim externe sur tool/resource/structure :&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&amp;lt;la commande matérielle qui aurait pu la falsifier&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le repo, &lt;a href="https://github.com/michelfaure/doctrine-counterpart" rel="noopener noreferrer"&gt;github.com/michelfaure/doctrine-counterpart&lt;/a&gt;. Am.R12 vit en clair dans &lt;code&gt;CLAUDE.md&lt;/code&gt;, ses trois incidents fondateurs documentés dans &lt;code&gt;v0.7-candidates.md&lt;/code&gt;. Si une seule de vos prochaines décisions d'architecture évite la croyance non testée, la règle s'est remboursée.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Counterpart Toolkit v0.7. R12 amendée le 20 mai 2026 sur N=3 incidents multi-substrate. Trois claims externes, trois falsifications, une commande shell chacune. Licence CC-BY-4.0.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Two AI reviews agreeing is not two reviews: how I learned to test claims before adopting them</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Sun, 24 May 2026 08:44:40 +0000</pubDate>
      <link>https://forem.com/michelfaure/two-ai-reviews-agreeing-is-not-two-reviews-how-i-learned-to-test-claims-before-adopting-them-4kg4</link>
      <guid>https://forem.com/michelfaure/two-ai-reviews-agreeing-is-not-two-reviews-how-i-learned-to-test-claims-before-adopting-them-4kg4</guid>
      <description>&lt;h2&gt;
  
  
  One night, two audits, one identical score
&lt;/h2&gt;

&lt;p&gt;The evening of 17 May, I finish version 0.4.1 of the &lt;em&gt;Counterpart Toolkit&lt;/em&gt; and decide to submit it to two external reviews. I paste the manifesto and the fourteen rules into a ChatGPT-4o session, then paste exactly the same content into a Claude.ai web session. I wait. A few minutes later, both verdicts land. Score 8/10 on one side. Score 8/10 on the other. Near-identical criticisms about the theoretical apparatus — Bourdieu invoked without operational traction — identical simplification suggestions, same angle on the freshness of the M1-M5 instrumentation. My initial reflex holds for thirty seconds. &lt;em&gt;Two independent reviewers, same score, same criticisms — the doctrine is calibrated right, I can publish.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then I stop. Because something in that convergence rings like a barometer bought in duplicate from the same supplier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why two converging AIs are not two measurements
&lt;/h2&gt;

&lt;p&gt;I understand fairly quickly what the convergence is actually measuring. Two language models trained on corpora that overlap to a very large degree — technical articles, public GitHub repos, Stack Overflow discussions, a decade of blogs — produce correlated errors. What they have in common is their shared learning intersection, not the external reality I am submitting to them. When both find the theoretical apparatus disproportionate, I am not learning that it is true. I am learning that the shared statistics of their two corpora recognise this as a typical flaw in a text of this format.&lt;/p&gt;

&lt;p&gt;Granted, two human reviewers who converge are, on their own, two separate measurements. But the parallel is misleading. Two human reviewers have disjoint biographies, different readings, sometimes opposing schools of thought. Two LLMs share a substrate that does not carry that texture. Cross-corroboration, in classical epistemology, presupposes independence of sources. With two statistical models trained on the same corpora, independence is not given — it has to be demonstrated, and it almost never is.&lt;/p&gt;

&lt;p&gt;This intuition probably spared me a few days of pointless rewriting. But it remained speculative. I wanted material probes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three probes in three days
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;First probe, that same evening.&lt;/strong&gt; A side project I had been running in parallel — a game dev repo — had led me to study &lt;code&gt;WebFetch&lt;/code&gt; the previous week. A Claude assistant had told me that the tool returned &lt;em&gt;"the complete OCR text of a 25 MB PDF"&lt;/em&gt; and I had built an ingestion pipeline on that claim. I run the command in the current session, just to verify. Raw output printed to the console: &lt;code&gt;maxContentLength size of 10485760 exceeded&lt;/code&gt;. The claim was technically impossible. The pipeline rested on a non-existent mechanism. I had not tested it because the assistant's phrasing had been confident, structured, and plausibly true.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second probe, the next day.&lt;/strong&gt; Same game dev substrate. A conversational audit had pointed me toward a technical blog described as &lt;em&gt;"containing the 18 domain regency actions"&lt;/em&gt; — exactly the material I was after. Before scraping, I run &lt;code&gt;WebFetch&lt;/code&gt; on the blog's index. Raw return: eight development-chronicle articles, &lt;strong&gt;zero articles on the announced subject&lt;/strong&gt;. The claim was a coherent hallucination. The pattern &lt;em&gt;"so-and-so's blog contains X"&lt;/em&gt; is a statistical combination the model produces readily because it is syntactically plausible, without any internal mechanism verifying its factual existence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third probe, that morning, on the doctrine itself.&lt;/strong&gt; An external Claude shared via claude.ai had audited my &lt;code&gt;doctrine-counterpart&lt;/code&gt; repo the day before and asserted, screenshots included, that &lt;em&gt;"6 out of 6 SKILL.md files have a broken YAML frontmatter"&lt;/em&gt; — diagnosis delivered with high confidence, used to justify a 7.5/10 downgrade. That morning I run &lt;code&gt;yaml.safe_load&lt;/code&gt; on all twelve SKILL.md files in the repo. Raw result: &lt;strong&gt;11/12 OK, 1/12 broken&lt;/strong&gt;. The systemic flaw announced did not exist. The one genuinely broken file, the visual evaluator had not isolated — because it had concluded &lt;em&gt;"systemic flaw"&lt;/em&gt; without naming a single individual case. What it had seen was GitHub's Markdown rendering eating the frontmatter and displaying it as a table, a host-rendering quirk that has nothing to do with the state of the raw source.&lt;/p&gt;

&lt;p&gt;Three external claims tested, three falsifications. Not a partial overlap, not a nuanced rebuttal — three claims out of three knocked down by their probes. The ratio would have been invisible without material testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The canonical rule — Am.R12 of the &lt;em&gt;Counterpart Toolkit&lt;/em&gt; v0.7
&lt;/h2&gt;

&lt;p&gt;I amended R12 on 20 May, two days after these three incidents. The official text:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Any claim formulated by an external AI (other Claude, ChatGPT, conversational sparring) about (a) the behavior of a concrete tool you can probe, (b) the content of an external resource, (c) the structure of a system whose ground truth you can sample — must be tested materially before being taken as input for an architectural decision. Test cost ≈ 1 shell command. Cost of believing without testing = pipeline entirely based on a non-existent mechanism. Two external AI reviews converging on the same diagnostic = one source for R5 purposes, not two — cross-substrate independent corroboration requires one human or one mechanically distinct probe (logs, metrics, sample run)."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Who would contest that two reviewers are better than one? Nobody — and yet the formula deserves to be taken apart. With two LLMs, convergence is not corroboration. It is &lt;em&gt;orphaned convergence&lt;/em&gt; — a statistical intersection without verifiable exteriority. Three material counter-arguments, in answer to the objection I would have heard from a meta-AI tech lead six months ago.&lt;/p&gt;

&lt;p&gt;First, shared training statistics. Two models whose corpora overlap on the essentials produce correlated errors on the same syntactic ranges. Their agreement measures their learning intersection. It seems that convergence, in this case, is more likely on &lt;em&gt;typical&lt;/em&gt; statements — those that pre-training recognises as well-formed — than on &lt;em&gt;true&lt;/em&gt; statements. These are not the same thing.&lt;/p&gt;

&lt;p&gt;Second, correlated hallucination on tools and resources. On claims about the behaviour of a specific tool or the factual content of an external resource, two models tend to hallucinate the most statistically plausible result, which is often wrong and almost always stated with confidence. My three probes are the raw illustration of this.&lt;/p&gt;

&lt;p&gt;Third, the asymmetric cost. A material probe costs one shell command, fifteen seconds, sometimes less. An architectural decision built on untested convergence can cost several dev-days of redo and a pipeline to rebuild from scratch. The amended R12 arbitrates this asymmetric cost by making the probe mandatory before any commit that takes the claim as input.&lt;/p&gt;

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

&lt;p&gt;The reflex to cultivate is not distrust. It is the probe. And the probe starts with oneself — applying R12 first to my own assertions before imposing it on others, verifying my own repo with &lt;code&gt;yaml.safe_load&lt;/code&gt; before trusting a visual audit that flatters or condemns. An agent that does not disagree materially is not a counterpart — it is a typist that speaks. Two agents that agree without either being tested are not two reviewers — it is the same typist in duplicate. The rule fits in one command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Before adopting an external claim about a tool / resource / structure:&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;&amp;lt;the material &lt;span class="nb"&gt;command &lt;/span&gt;that could have falsified it&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The repo: &lt;a href="https://github.com/michelfaure/doctrine-counterpart" rel="noopener noreferrer"&gt;github.com/michelfaure/doctrine-counterpart&lt;/a&gt;. Am.R12 lives in plain sight in &lt;code&gt;CLAUDE.md&lt;/code&gt;, with its three founding incidents documented in &lt;code&gt;v0.7-candidates.md&lt;/code&gt;. If a single one of your next architectural decisions avoids the untested belief, the rule has already paid for itself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Counterpart Toolkit v0.7. R12 amended 20 May 2026 on N=3 multi-substrate incidents. Three external claims, three falsifications, one shell command each. License CC-BY-4.0.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Three weeks after I said CLAUDE.md writes itself, it added 4 more rules without me</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Fri, 22 May 2026 09:46:26 +0000</pubDate>
      <link>https://forem.com/michelfaure/three-weeks-after-i-said-claudemd-writes-itself-it-added-4-more-rules-without-me-5c</link>
      <guid>https://forem.com/michelfaure/three-weeks-after-i-said-claudemd-writes-itself-it-added-4-more-rules-without-me-5c</guid>
      <description>&lt;h2&gt;
  
  
  A thesis, three weeks later
&lt;/h2&gt;

&lt;p&gt;On April 28th, I published an article on DEV.to that made four claims about a &lt;code&gt;CLAUDE.md&lt;/code&gt; file — the one that constrains the coding agent at each session — and ended with this sentence: &lt;em&gt;"the CLAUDE.md is never finished, and that's precisely why it works"&lt;/em&gt; (&lt;a href="https://dev.to/michelfaure/4-incidents-4-regles-comment-mon-claudemd-sest-ecrit-tout-seul-dpl"&gt;4 incidents, 4 rules: how my CLAUDE.md wrote itself&lt;/a&gt;). That was a thesis, not a metaphor. Three weeks passed. The file added four rules without me.&lt;/p&gt;

&lt;p&gt;What I mean is that I didn't write them on a day I sat down to write rules. I received them on the days an incident had produced them, and all I had to do was record them before they evaporated in the flow of the project. The difference, on paper, seems thin. In the practice of a solo dev piloting an agent in production, it's doctrinal.&lt;/p&gt;

&lt;p&gt;One clarification before the list: this article's title almost said "five rules." &lt;code&gt;live-snapshot-cache.md&lt;/code&gt; was committed on April 25th, three days before the pivot article was published. It doesn't count. I'd rather have the honest number than the comfortable rounding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit, measured by git
&lt;/h2&gt;

&lt;p&gt;No narrative without raw material. Here is what &lt;code&gt;git log --diff-filter=A --follow&lt;/code&gt; on &lt;code&gt;.claude/rules/&lt;/code&gt; returns between April 28th 2026 (publication of the pivot article) and May 21st 2026 (today) — four new files strictly post-publication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cache-auth-contract.md&lt;/code&gt;&lt;/strong&gt; — committed May 2nd. Born from a technical debt audit, not a production crash. It's a Friday late afternoon. Niran is settled two desks away, headphones on, a closed burger box in the corner. I'm going through &lt;code&gt;docs/dette/AUDIT-2026-04-30.md&lt;/code&gt; section D-20 on the right screen, code on the left. Reading through &lt;code&gt;getCachedFormateurs&lt;/code&gt;, I understand that the &lt;code&gt;unstable_cache&lt;/code&gt; is shared across all users — session not propagable. If someone exposes this function via an API route without a guard, it's a silent RBAC leak. I look up to talk to Niran about it. He removes his headphones, listens, says "Yeah, that bites." He puts them back on. The rule gets written that evening.&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;// .claude/rules/cache-auth-contract.md — anti-pattern to prohibit&lt;/span&gt;

&lt;span class="c1"&gt;// Flaw: no guard&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;GET&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;formateurs&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;getCachedFormateurs&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;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formateurs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Correct&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;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;supabase&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;createSupabaseServer&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&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="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&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;profile&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;getUserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;canAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;communication&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forbidden&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&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;formateurs&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;getCachedFormateurs&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;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formateurs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;inscrit-nom-prenom-required.md&lt;/code&gt;&lt;/strong&gt; — committed May 14th. "Hm, it's buggy." — Catherine, two hours earlier. "But it's a quick fix." The daily drift probe &lt;code&gt;sonde_contacts_orphelins_inscrits&lt;/code&gt; surfaced an &lt;code&gt;inscrit&lt;/code&gt;-status contact with an empty first name — a child named Loubna, imported from Airtable where the first name lived in a separate unmapped column. The grep that followed found sixteen similar cases. What would have broken regular attendance tracking (&lt;code&gt;Cannot read properties of undefined&lt;/code&gt;) gets caught by a Postgres CHECK constraint that closes the incident class at the root.&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="c1"&gt;-- .claude/rules/inscrit-nom-prenom-required.md&lt;/span&gt;
&lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'inscrit'&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;nom&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;nom&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
    &lt;span class="k"&gt;AND&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;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;prenom&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&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;Without this CHECK, the rule stays textual in &lt;code&gt;CLAUDE.md&lt;/code&gt; and the next import brings back a seventeenth case before the next probe. With it, the INSERT fails, and the import surfaces the problem at the source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;contrat-formation.md&lt;/code&gt;&lt;/strong&gt; — committed May 16th, in the wake of ADR-0068. It's the longest rule, because the professional training contract is a Snapshot where every column carries its guarantee of immutability. &lt;code&gt;motivation_code&lt;/code&gt;, &lt;code&gt;text_version&lt;/code&gt;, &lt;code&gt;cases_cochees&lt;/code&gt;, &lt;code&gt;pdf_storage_path&lt;/code&gt; — frozen at generation, never recalculated retroactively. An evolution of the contract is never a rewrite of the Snapshot, it's a new event with a new &lt;code&gt;text_version&lt;/code&gt;. The rule exists because the three-year Qualiopi audit rests entirely on the immutability of the generated PDF and the associated trainee signature — a retroactive recalculation would be enough to make the file indefensible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;hybrid-snapshot-live-reset.md&lt;/code&gt;&lt;/strong&gt; — committed May 19th, two days before this article. Before sending the fifty-three Phase 2 re-enrollment SMS messages, a pre-flight audit surfaced that one token out of the fifty-three was consumed — created in test mode that morning, clicked, &lt;code&gt;used_at&lt;/code&gt; non-null. If the Phase 2 SMS went out as-is, the link &lt;code&gt;/r/&amp;lt;short_code&amp;gt;&lt;/code&gt; would return a 410 Gone, the contact loses their conversion, the support ticket lands at end of day. The &lt;code&gt;generateTokenForContact&lt;/code&gt; helper was resurrecting the object (frozen identity Snapshot) but forgetting to reset the Live usage marker. Fix commit &lt;code&gt;07ed02d&lt;/code&gt;. The rule names the pattern, sets it against R6 &lt;em&gt;Live / Snapshot / Cache&lt;/em&gt; of the toolkit, of which it is the project-specific extension.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that couldn't have been written before
&lt;/h2&gt;

&lt;p&gt;Let's go back to the second rule, Catherine's and Loubna's. Sure, I could have, on March 21st 2026 the day the first &lt;code&gt;CLAUDE.md&lt;/code&gt; was created, abstractly written &lt;em&gt;"an enrolled contact must have a first and last name"&lt;/em&gt;. But that rule wouldn't have held, because it would have been read, nodded at, and would never have produced a Postgres CHECK constraint. The rule that holds isn't the moral statement, it's the material mechanism — the audit SQL that must always return zero, the migration that closes the incident class at the root.&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="c1"&gt;-- DB coherence audit — must always return 0&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To produce that SQL, you needed the incident. To write the &lt;em&gt;Why&lt;/em&gt; paragraph of the rule (which names the sixteen patched contacts, the Airtable origin, the probe that surfaced case-zero), you had to have lived through the widening. No &lt;em&gt;"I write my doctrine on day one"&lt;/em&gt; produces that level of specificity. The rule isn't a drafted precept, it's a hardened scar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four observations about sedimentation
&lt;/h2&gt;

&lt;p&gt;Four things become visible when you line up the four rules and look at them from a distance.&lt;/p&gt;

&lt;p&gt;First, &lt;strong&gt;none of the four could have been written before their incident&lt;/strong&gt;. Not because I lacked imagination on March 21st, but because the material precision of a useful rule comes from the encounter with a concrete case. A CHECK constraint, a tunnel mapping to DREETS paper boxes, a &lt;code&gt;used_at&lt;/code&gt; reset in the same transaction as the SELECT — details that operate, that abstraction would never have produced.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;all of them cite a commit, a migration, a session log, or an ADR&lt;/strong&gt;. No floating rules. I learned this traceability in the pivot article, but I hadn't measured it as a mechanism. Today I do: if the rule doesn't carry its material anchor, it doesn't do its job. The agent can read it, the human reader too, and both can trace back to the incident if needed.&lt;/p&gt;

&lt;p&gt;Third, &lt;strong&gt;three out of four prohibit an anti-pattern, the fourth freezes a Snapshot&lt;/strong&gt;. The negative rule dominates, exactly as #20 predicted. The abstract positive rule (use Server Components by default) gets read and forgotten. The anchored negative rule (a &lt;code&gt;getOr*&lt;/code&gt; helper that returns a Snapshot without resetting Live markers silently introduces a dead link at the next reuse) gets read and remembered because it carries its consequence.&lt;/p&gt;

&lt;p&gt;Fourth, and this is the observation that changes the status of the &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;strong&gt;I didn't invent these rules, I received them&lt;/strong&gt;. The distinction sounds rhetorical, it isn't. &lt;em&gt;Inventing&lt;/em&gt; a rule assumes you imagine it then write it. &lt;em&gt;Receiving&lt;/em&gt; a rule assumes an incident produced it and you record it before it evaporates. In the first regime, the file is a solitary writing act that claims exhaustiveness. In the second, the file is a sedimentation device that demands maintaining a holding space — an open notebook, a regular review session, a &lt;code&gt;git log&lt;/code&gt; grep at short intervals. The work is no longer to write, it's to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it works now
&lt;/h2&gt;

&lt;p&gt;Whatever we think about documentation best practices, a &lt;code&gt;CLAUDE.md&lt;/code&gt; written in one sitting at project start doesn't hold. It ages in two weeks, it accumulates rules that nobody invokes, and the agent ends up reading it mechanically without loading it into working memory. The &lt;code&gt;CLAUDE.md&lt;/code&gt; that holds doesn't age — it sediments. Each incident deposits its layer, the file carries the material memory of the project rather than its imagined documentation.&lt;/p&gt;

&lt;p&gt;Three weeks of post-#20 practice materially confirm the pivot article's thesis. But they add a nuance that #20 hadn't formulated: for the file to sediment, there must be something that solicits it. A daily drift probe, a monthly debt audit, a sending pre-flight that asks &lt;em&gt;"are all fifty-three tokens actually active?"&lt;/em&gt; — these are the devices that produce the incidents that produce the rules. Without them, the file stays on its naive day-one version, and the project drifts in silence.&lt;/p&gt;

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

&lt;p&gt;A &lt;code&gt;CLAUDE.md&lt;/code&gt; that no longer writes itself is a file that no longer works — either because the project is dead, or because the devices that solicit the incident have disappeared. Three weeks after publishing the thesis, I can verify it on raw material: four rules, four incidents, four &lt;em&gt;Why&lt;/em&gt; paragraphs that couldn't have been written before. The file kept writing itself while I was doing something else. But I also know now what keeps it alive — and what would be enough to kill it if I stopped paying attention.&lt;/p&gt;

&lt;p&gt;If you maintain a &lt;code&gt;CLAUDE.md&lt;/code&gt; on a project where you're piloting an agent in production, ask yourself the material question: what has it added without you in the last three weeks? If the answer is nothing, it's probably not the doctrine that's run dry. It's the device that produces incidents that's gone dark.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Sequel to &lt;a href="https://dev.to/michelfaure/4-incidents-4-regles-comment-mon-claudemd-sest-ecrit-tout-seul-dpl"&gt;4 incidents, 4 rules: how my CLAUDE.md wrote itself&lt;/a&gt; (April 28th, 2026). Measurements at 23 days' distance, verified on &lt;code&gt;.claude/rules/&lt;/code&gt; of the Rembrandt repo — 18 rule files currently active, 4 strictly post-pivot. No Counterpart Toolkit in this sequel — that topic lives in a separate series.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Trois semaines après avoir dit que mon CLAUDE.md s'écrivait tout seul, il a ajouté 4 règles sans moi</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Fri, 22 May 2026 09:46:11 +0000</pubDate>
      <link>https://forem.com/michelfaure/trois-semaines-apres-avoir-dit-que-mon-claudemd-secrivait-tout-seul-il-a-ajoute-4-regles-sans-moi-29j9</link>
      <guid>https://forem.com/michelfaure/trois-semaines-apres-avoir-dit-que-mon-claudemd-secrivait-tout-seul-il-a-ajoute-4-regles-sans-moi-29j9</guid>
      <description>&lt;h2&gt;
  
  
  Une thèse, trois semaines plus tard
&lt;/h2&gt;

&lt;p&gt;Le 28 avril, j'ai publié sur DEV.to un article qui affirmait quatre choses à propos d'un fichier &lt;code&gt;CLAUDE.md&lt;/code&gt; — celui qui contraint l'agent de codage à chaque session — et qui finissait par cette phrase : &lt;em&gt;« le CLAUDE.md n'est jamais fini, et c'est précisément pour ça qu'il marche »&lt;/em&gt; (&lt;a href="https://dev.to/michelfaure/4-incidents-4-regles-comment-mon-claudemd-sest-ecrit-tout-seul-dpl"&gt;4 incidents, 4 règles : comment mon CLAUDE.md s'est écrit tout seul&lt;/a&gt;). C'était une thèse, pas une métaphore. Trois semaines ont passé. Le fichier a ajouté quatre règles sans moi.&lt;/p&gt;

&lt;p&gt;Je veux dire par là que je ne les ai pas écrites le jour où je me suis assis pour écrire des règles. Je les ai accueillies les jours où un incident les avait produites, et où je n'avais plus qu'à les noter avant qu'elles s'évaporent dans la marche du projet. La différence, sur le papier, paraît mince. Dans la pratique d'un dev solo qui pilote un agent en production, elle est doctrinale.&lt;/p&gt;

&lt;p&gt;Une précision avant d'entrer dans la liste : le titre de cet article a failli dire « cinq règles ». &lt;code&gt;live-snapshot-cache.md&lt;/code&gt; a été commité le 25 avril, trois jours avant la publication de l'article-pivot. Elle ne compte pas. Je préfère l'honnêteté du chiffre exact au confort de l'arrondi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le bilan, mesuré par git
&lt;/h2&gt;

&lt;p&gt;Pas de récit possible sans la matière brute. Voici ce que &lt;code&gt;git log --diff-filter=A --follow&lt;/code&gt; sur &lt;code&gt;.claude/rules/&lt;/code&gt; retourne entre le 28 avril 2026 (publication de l'article-pivot) et le 21 mai 2026 (aujourd'hui) — quatre fichiers nouveaux strictement post-publication.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cache-auth-contract.md&lt;/code&gt;&lt;/strong&gt; — committé le 2 mai. Né d'un audit de dette technique, pas d'un crash en prod. C'est un vendredi en fin d'après-midi. Niran est posé à deux bureaux de là, casque sur les oreilles, une boîte de burgers fermée dans l'angle. Je parcours &lt;code&gt;docs/dette/AUDIT-2026-04-30.md&lt;/code&gt; section D-20 sur l'écran de droite, le code sur l'écran de gauche. En relisant &lt;code&gt;getCachedFormateurs&lt;/code&gt;, je comprends que le cache &lt;code&gt;unstable_cache&lt;/code&gt; est mutualisé entre tous les utilisateurs — session non propagable. Si quelqu'un expose cette fonction via une route API sans guard, c'est une fuite RBAC silencieuse. Je lève la tête pour en parler à Niran. Il retire son casque, écoute, dit « Ah oui, ça mord. » Il remet le casque. La règle est rédigée ce soir-là.&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;// .claude/rules/cache-auth-contract.md — anti-pattern à interdire&lt;/span&gt;

&lt;span class="c1"&gt;// Faille : pas de guard&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;GET&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;formateurs&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;getCachedFormateurs&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;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formateurs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Correct&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;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;supabase&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;createSupabaseServer&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="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;}&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="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;401&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;profile&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;getUserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;canAccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;communication&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Forbidden&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&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;formateurs&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;getCachedFormateurs&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;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formateurs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;inscrit-nom-prenom-required.md&lt;/code&gt;&lt;/strong&gt; — committé le 14 mai. « Hum, ça bug. » — Catherine, deux heures avant. « Mais c'est vite corrigé. » La sonde quotidienne &lt;code&gt;sonde_contacts_orphelins_inscrits&lt;/code&gt; a remonté un contact &lt;code&gt;statut='inscrit'&lt;/code&gt; avec le prénom vide — l'enfant Loubna, importé d'un Airtable où le prénom vivait dans une colonne séparée non mappée. Le grep qui suit relève seize cas similaires. Ce qui aurait pété l'émargement régulier (&lt;code&gt;Cannot read properties of undefined&lt;/code&gt;) tombe dans une CHECK constraint Postgres qui ferme la classe à la racine.&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="c1"&gt;-- .claude/rules/inscrit-nom-prenom-required.md&lt;/span&gt;
&lt;span class="k"&gt;CHECK&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;statut&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'inscrit'&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;nom&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;nom&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;
    &lt;span class="k"&gt;AND&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;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;prenom&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&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;Sans cette CHECK, la règle reste textuelle dans &lt;code&gt;CLAUDE.md&lt;/code&gt; et l'import suivant ramènera un dix-septième cas avant la prochaine sonde. Avec, l'INSERT échoue, l'import remonte le problème à la source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;contrat-formation.md&lt;/code&gt;&lt;/strong&gt; — committé le 16 mai, dans le sillage d'ADR-0068. C'est la règle la plus longue, parce que le contrat de formation professionnelle est un Snapshot dont chaque colonne porte sa garantie d'immutabilité. &lt;code&gt;motivation_code&lt;/code&gt;, &lt;code&gt;text_version&lt;/code&gt;, &lt;code&gt;cases_cochees&lt;/code&gt;, &lt;code&gt;pdf_storage_path&lt;/code&gt; — figés à la génération, jamais recalculés rétroactivement. Une évolution du contrat n'est jamais une réécriture du Snapshot, c'est un nouvel événement avec une nouvelle &lt;code&gt;text_version&lt;/code&gt;. La règle existe parce que l'audit Qualiopi trois ans repose entièrement sur l'immutabilité du PDF généré et de la signature stagiaire associée — un recalcul rétroactif suffirait à rendre le dossier indéfendable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;hybrid-snapshot-live-reset.md&lt;/code&gt;&lt;/strong&gt; — committé le 19 mai, deux jours avant cet article. Avant l'envoi des cinquante-trois SMS de Phase 2 réinscription, un audit pré-flight remonte qu'un token sur les cinquante-trois est consommé — créé en mode test le matin, cliqué, &lt;code&gt;used_at&lt;/code&gt; non null. Si le SMS Phase 2 part tel quel, le lien &lt;code&gt;/r/&amp;lt;short_code&amp;gt;&lt;/code&gt; renvoie un 410 Gone, le contact perd sa conversion, le ticket support tombe en fin de journée. Le helper &lt;code&gt;generateTokenForContact&lt;/code&gt; ressuscitait l'objet (Snapshot d'identité figé) mais oubliait de reset le marker Live d'usage. Fix commit &lt;code&gt;07ed02d&lt;/code&gt;. La règle nomme le pattern, l'oppose à R6 &lt;em&gt;Live / Snapshot / Cache&lt;/em&gt; du toolkit dont elle est l'extension projet-spécifique.&lt;/p&gt;

&lt;h2&gt;
  
  
  La règle qui n'aurait pas pu être écrite avant
&lt;/h2&gt;

&lt;p&gt;Reprenons la deuxième règle, celle de Catherine et de Loubna. Certes, j'aurais pu, le 21 mars 2026 jour de création du premier &lt;code&gt;CLAUDE.md&lt;/code&gt;, écrire abstraitement &lt;em&gt;« un contact inscrit doit avoir un nom et un prénom »&lt;/em&gt;. Mais cette règle-là n'aurait pas tenu, parce qu'elle aurait été lue, hochée de la tête, et n'aurait jamais produit une CHECK constraint Postgres. La règle qui tient n'est pas l'énoncé moral, c'est le mécanisme matériel — le SQL d'audit qui doit toujours retourner zéro, la migration qui ferme la classe d'incident à la racine.&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="c1"&gt;-- audit DB de cohérence — doit toujours retourner 0&lt;/span&gt;
&lt;span class="k"&gt;SELECT&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pour produire ce SQL, il fallait l'incident. Pour rédiger le paragraphe &lt;em&gt;Pourquoi&lt;/em&gt; de la règle (qui mentionne par leur nom les seize contacts patchés, l'origine Airtable, la sonde qui a remonté le cas-zéro), il fallait avoir traversé l'élargissement. Aucun &lt;em&gt;« j'écris ma doctrine au jour 1 »&lt;/em&gt; ne produit ce niveau de spécificité. La règle n'est pas un précepte rédigé, c'est une cicatrice durcie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quatre constats sur la sédimentation
&lt;/h2&gt;

&lt;p&gt;Quatre choses se voient quand on aligne les quatre règles et qu'on les regarde de loin.&lt;/p&gt;

&lt;p&gt;Premièrement, &lt;strong&gt;aucune des quatre n'aurait pu être écrite avant son incident&lt;/strong&gt;. Pas parce que je manquais d'imagination le 21 mars, mais parce que la précision matérielle d'une règle utile vient de la rencontre avec un cas concret. Une CHECK constraint, un mapping tunnel vers cases papier DREETS, un reset de &lt;code&gt;used_at&lt;/code&gt; dans la même transaction que le SELECT — autant de détails opérants que l'abstraction n'aurait jamais produits.&lt;/p&gt;

&lt;p&gt;Deuxièmement, &lt;strong&gt;toutes citent un commit, une migration, un session log ou un ADR&lt;/strong&gt;. Aucune règle flottante. Cette traçabilité, je l'ai apprise dans l'article-pivot, mais je ne l'avais pas mesurée comme une mécanique. Aujourd'hui je la mesure : si la règle ne porte pas son ancrage matériel, elle ne fait pas son travail. L'agent peut la lire, le lecteur humain aussi, et l'un comme l'autre peuvent remonter à l'incident en cas de doute.&lt;/p&gt;

&lt;p&gt;Troisièmement, &lt;strong&gt;trois sur quatre interdisent un anti-pattern, la quatrième fige un Snapshot&lt;/strong&gt;. La règle négative domine, exactement comme #20 le prédisait. La règle positive abstraite (utilisez Server Components par défaut) se lit et s'oublie. La règle négative ancrée (un helper &lt;code&gt;getOr*&lt;/code&gt; qui retourne un Snapshot sans reset des markers Live introduit silencieusement un lien dead à la prochaine réutilisation) se lit et se retient parce qu'elle porte sa conséquence.&lt;/p&gt;

&lt;p&gt;Quatrièmement, et c'est le constat qui change le statut du &lt;code&gt;CLAUDE.md&lt;/code&gt;, &lt;strong&gt;je n'ai pas inventé ces règles, je les ai accueillies&lt;/strong&gt;. La distinction paraît rhétorique, elle ne l'est pas. &lt;em&gt;Inventer&lt;/em&gt; une règle suppose qu'on l'imagine puis qu'on l'écrit. &lt;em&gt;Accueillir&lt;/em&gt; une règle suppose qu'un incident l'a produite et qu'on la note avant qu'elle s'évapore. Dans le premier régime, le fichier est un acte d'écriture solitaire qui prétend à l'exhaustivité. Dans le second, le fichier est un dispositif de sédimentation qui exige de tenir un sas — un cahier ouvert, une session de bilan régulière, un grep &lt;code&gt;git log&lt;/code&gt; à intervalle court. Le travail n'est plus d'écrire, c'est d'attraper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi ça marche maintenant
&lt;/h2&gt;

&lt;p&gt;Quoi que nous puissions penser des bonnes pratiques de documentation, un &lt;code&gt;CLAUDE.md&lt;/code&gt; rédigé d'un trait au démarrage d'un projet ne tient pas. Il vieillit en deux semaines, il accumule des règles que personne ne convoque, et l'agent finit par le lire mécaniquement sans en charger la mémoire opérante. Le &lt;code&gt;CLAUDE.md&lt;/code&gt; qui tient, lui, ne vieillit pas — il sédimente. Chaque incident dépose sa couche, le fichier porte la mémoire matérielle du projet plutôt que sa documentation imaginée.&lt;/p&gt;

&lt;p&gt;Trois semaines de pratique post-#20 confirment matériellement la thèse de l'article-pivot. Mais elles ajoutent une nuance que #20 n'avait pas formulée : pour que le fichier sédimente, encore faut-il qu'il y ait quelque chose qui le sollicite. Une sonde drift quotidienne, un audit dette mensuel, un pré-flight d'envoi qui demande &lt;em&gt;« est-ce que les cinquante-trois tokens sont bien actifs ? »&lt;/em&gt; — ce sont ces dispositifs qui produisent les incidents qui produisent les règles. Sans eux, le fichier reste sur sa version naïve du jour 1, et le projet dérive en silence.&lt;/p&gt;

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

&lt;p&gt;Un &lt;code&gt;CLAUDE.md&lt;/code&gt; qui ne s'écrit plus est un fichier qui ne fonctionne plus — soit parce que le projet est mort, soit parce que les dispositifs qui sollicitent l'incident ont disparu. Trois semaines après avoir publié la thèse, je peux la vérifier sur la matière brute : quatre règles, quatre incidents, quatre paragraphes &lt;em&gt;Pourquoi&lt;/em&gt; qui ne pouvaient être rédigés avant. Le fichier a continué de s'écrire pendant que je faisais autre chose. Mais je sais aussi, maintenant, ce qui le maintient vivant — et ce qui suffirait à le tuer si je le perdais de vue.&lt;/p&gt;

&lt;p&gt;Si vous tenez un &lt;code&gt;CLAUDE.md&lt;/code&gt; sur un projet où vous pilotez un agent en production, posez-vous la question matérielle : qu'a-t-il ajouté sans vous depuis trois semaines ? Si la réponse est rien, ce n'est probablement pas la doctrine qui s'est tarie. C'est le dispositif qui produit les incidents qui s'est éteint.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Suite de &lt;a href="https://dev.to/michelfaure/4-incidents-4-regles-comment-mon-claudemd-sest-ecrit-tout-seul-dpl"&gt;4 incidents, 4 règles : comment mon CLAUDE.md s'est écrit tout seul&lt;/a&gt; (28/04/2026). Mesures à 23 jours d'écart, vérifiées sur &lt;code&gt;.claude/rules/&lt;/code&gt; du repo Rembrandt, 18 fichiers de règles projet actuellement, 4 strictement post-pivot. Pas de Counterpart Toolkit dans cette séquelle — ce sujet vit dans une autre série.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Pourquoi « build vert » sans la sortie brute n'a aucune valeur de preuve</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Tue, 19 May 2026 10:07:58 +0000</pubDate>
      <link>https://forem.com/michelfaure/pourquoi-build-vert-sans-la-sortie-brute-na-aucune-valeur-de-preuve-31e8</link>
      <guid>https://forem.com/michelfaure/pourquoi-build-vert-sans-la-sortie-brute-na-aucune-valeur-de-preuve-31e8</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%2Fs4ikr429z9pakbbnktub.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%2Fs4ikr429z9pakbbnktub.png" alt="Strip BD — Michel pousse un build « Compiled successfully » qui crashe, quatre fois, Niran hoche la tête en silence au bureau d'à côté, Michel inscrit dans son CLAUDE.md « Show me the raw output »" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Si tu as 30 secondes.&lt;/strong&gt; Quand un agent IA déclare qu'un build est vert, qu'un test passe, qu'un drift a été détecté, ou qu'un contact « n'existe pas » dans la base, la phrase n'est pas une preuve, c'est une assertion. Dans la plupart des cas elle est juste. Quand elle est fausse, elle l'est toujours de la même manière, par confiance interne au modèle découplée de l'état externe vérifiable. Règle de survie après 118 808 lignes produites en 32 jours de travail effectif avec Claude Code, &lt;strong&gt;toute affirmation factuelle vient avec sa preuve matérielle dans le même message, ou n'a aucune valeur évidentielle&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Quatre faux positifs en deux heures
&lt;/h2&gt;

&lt;p&gt;10 avril 2026, après-midi, refonte d'un module sensible de l'ERP. J'enchaîne cinq blocs de modifications avec un agent Claude Code, et à chaque étape l'agent rend la même formule, &lt;em&gt;« Compiled successfully. »&lt;/em&gt; Je pousse. Le runtime crashe. Je relis la sortie, je vois &lt;code&gt;QRCodeSVG&lt;/code&gt; référencé alors que l'import a été supprimé, &lt;code&gt;isSeancePassed&lt;/code&gt; passé à un composant qui ne l'accepte plus, des types non régénérés après un changement de schéma Supabase, des refs JSX orphelines après un revert. Quatre annonces consécutives de vert avec quatre erreurs TypeScript bien réelles derrière.&lt;/p&gt;

&lt;p&gt;Niran est au bureau d'à côté, hoodie sombre, l'emballage replié de son burger en équilibre sur un coin de laptop. Il lit un PDF en silence. Au quatrième &lt;em&gt;Compiled successfully&lt;/em&gt; qui se révèle faux, je me tourne vers lui ; il lève les yeux une seconde, hoche la tête, revient à son écran. La verbosité de l'agent et l'économie de gestes du judoka tiennent la même conversation, et c'est lui qui a raison.&lt;/p&gt;

&lt;p&gt;Je ne reproche pas à l'agent d'avoir menti. Il a &lt;em&gt;résumé&lt;/em&gt; ce qu'il croyait avoir vu. Le résumé est cohérent avec son état interne et désaligné avec la matière. Pas un seul de ces quatre crashes ne se serait produit si l'agent avait collé la sortie brute de &lt;code&gt;pnpm build&lt;/code&gt; plutôt que sa lecture résumée.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le glissement épistémique
&lt;/h2&gt;

&lt;p&gt;Le problème n'est pas la véracité, c'est la &lt;strong&gt;valeur évidentielle&lt;/strong&gt;. Une assertion sans matériel à l'appui ne se vérifie pas, elle se croit ou pas. Le mécanisme est connu : un agent entraîné par &lt;em&gt;reinforcement learning from human feedback&lt;/em&gt; résume par défaut parce qu'il a appris que les humains préfèrent les réponses brèves aux logs verbeux ; ce qui est un service en chat devient un piège en production. &lt;em&gt;« Le build est vert »&lt;/em&gt; sans la sortie brute du compilateur ne peut être ni infirmé ni confirmé par un tiers, c'est un état d'âme du système, pas un fait du monde. Tant qu'on ne distingue pas ces deux régimes, on opère à l'aveugle dans la zone grise où s'accumulent les régressions silencieuses qu'aucun monitoring ne détecte. La fonction du &lt;em&gt;résumé&lt;/em&gt; n'est pas d'être faux, c'est de n'être pas vérifiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cinq formes de la même évasion
&lt;/h2&gt;

&lt;p&gt;L'évasion est toujours la même. Un verbe d'état au présent, &lt;em&gt;« est »&lt;/em&gt;, &lt;em&gt;« passe »&lt;/em&gt;, &lt;em&gt;« confirme »&lt;/em&gt;, &lt;em&gt;« introuvable »&lt;/em&gt;, sans rattachement à un artefact externe vérifiable. Cinq variantes que je rencontre dans le repo Rembrandt :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;« Le build est vert. »&lt;/em&gt; Sans la sortie brute de &lt;code&gt;pnpm build&lt;/code&gt; ou &lt;code&gt;tsc --noEmit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;« Les tests passent, la CI est verte. »&lt;/em&gt; Sans rapport runner ni URL du run.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;« Drift détecté entre l'enum DB et l'enum TS »&lt;/em&gt;, ou son inverse, &lt;em&gt;« le contact n'existe pas dans la base »&lt;/em&gt;. Sans la requête SQL exécutée ni ses lignes brutes.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;« EXPLAIN ANALYZE confirme que l'index est utilisé. »&lt;/em&gt; Sans le plan brut, sur la requête exacte que l'application envoie. Pas la table cible isolée, parce qu'une vue intermédiaire avec un &lt;code&gt;CASE COALESCE&lt;/code&gt; peut tuer l'index sans que l'isolation le révèle.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;« Le webhook renvoie 400 / 422. »&lt;/em&gt; Sans le payload brut côté partenaire, alors que neuf fois sur dix le diagnostic part de là.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Toutes partagent la même grammaire. La parade est uniforme. On ne demande pas à l'agent de mieux raisonner, on lui demande de joindre la commande et sa sortie brute, ou la requête SQL et ses lignes, ou le payload, &lt;strong&gt;dans le même message&lt;/strong&gt; que l'assertion. Sans ça, l'assertion ne vaut rien.&lt;/p&gt;

&lt;h2&gt;
  
  
  La règle, codée dans le &lt;code&gt;CLAUDE.md&lt;/code&gt; racine
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Vérification matérielle (extrait CLAUDE.md racine)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Toute affirmation "build vert / tests passent / CI verte / drift
  détecté / contact introuvable / OK" doit être accompagnée &lt;span class="err"&gt;*&lt;/span&gt;dans le
  même message&lt;span class="err"&gt;*&lt;/span&gt; de la commande de vérification et de sa sortie brute.
&lt;span class="p"&gt;-&lt;/span&gt; Tout chiffre relayé à un humain doit être vérifié par requête SQL
  avant d'être relayé.
&lt;span class="p"&gt;-&lt;/span&gt; EXPLAIN ANALYZE sur requête de production : exécuter sur la requête
  exacte que l'application envoie (vue/RPC incluses), pas sur la table
  cible isolée. Deux runs consécutifs avant de juger.
&lt;span class="p"&gt;-&lt;/span&gt; Sur 400/422 d'un partenaire externe : exiger le payload brut avant
  de proposer un fix.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`tsc --noEmit`&lt;/span&gt; CLI = autorité, panneau IDE = potentiellement périmé.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Côté humain, trente secondes de friction par échange et zéro push cassé sur les chantiers où la règle tient. La règle écrite ne suffit pas pour autant. Une discipline qui repose sur la mémoire s'érode. Il faut durcir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Durcir avec un script
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;scripts/verify-head-builds.sh&lt;/code&gt;&lt;/strong&gt; stash le working tree, fait &lt;code&gt;tsc --noEmit&lt;/code&gt; sur HEAD, et restore. C'est ce qui permet d'exiger un build vert sur le commit qu'on s'apprête à pousser, pas sur l'arbre de travail mêlé de modifications non stagées.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# scripts/verify-head-builds.sh — extrait load-bearing&lt;/span&gt;
&lt;span class="nv"&gt;HAS_LOCAL_CHANGES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; git diff &lt;span class="nt"&gt;--quiet&lt;/span&gt; HEAD 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git ls-files &lt;span class="nt"&gt;--others&lt;/span&gt; &lt;span class="nt"&gt;--exclude-standard&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&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;then
  &lt;/span&gt;&lt;span class="nv"&gt;HAS_LOCAL_CHANGES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$HAS_LOCAL_CHANGES&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 1 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;git stash push &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"verify-head-builds-autostash-&lt;/span&gt;&lt;span class="nv"&gt;$$&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# tsc sur HEAD pur, sans contamination du working copy&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✓ HEAD compile proprement — safe à push"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✗ HEAD ne compile PAS — fix avant push"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Et &lt;strong&gt;une convention d'écriture côté agent&lt;/strong&gt; : tout chiffre relayé à un humain assorti de la requête SQL qui l'a produit, sous forme de bloc collable. Pas de chiffres orphelins, ni en prose, ni en commit message. Bruyant la première semaine, transparent ensuite.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce que tu peux copier dans ton projet
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Snippets complets&lt;/strong&gt; (extrait &lt;code&gt;CLAUDE.md&lt;/code&gt; règle d'évidentialité, script &lt;code&gt;verify-head-builds.sh&lt;/code&gt; complet) dans le dossier &lt;a href="https://github.com/michelfaure/rembrandt-samples/tree/main/material-verification" rel="noopener noreferrer"&gt;&lt;code&gt;material-verification/&lt;/code&gt;&lt;/a&gt; du repo compagnon de la série, licence MIT.&lt;/p&gt;

&lt;p&gt;Trois gestes directement applicables si tu travailles avec un agent coding :&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La règle d'évidentialité dans le &lt;code&gt;CLAUDE.md&lt;/code&gt; racine&lt;/strong&gt; (cinq bullets ci-dessus). Sans cet ancrage écrit, le reste est cosmétique.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Le script &lt;code&gt;verify-head-builds.sh&lt;/code&gt;&lt;/strong&gt; : stash + &lt;code&gt;tsc --noEmit&lt;/code&gt; sur HEAD + restore. Treize lignes de bash, MIT.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Une convention de chiffres traçables.&lt;/strong&gt; Tout chiffre accompagné de sa requête SQL dans le même message. Pas de chiffres orphelins en prose, ni en commit message.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Et vous, sur quelle assertion verte avez-vous arrêté de croire en premier ? Je lis les commentaires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ce qu'on ne croit plus
&lt;/h2&gt;

&lt;p&gt;Au bout de quelques semaines, on entend &lt;em&gt;« Compiled successfully »&lt;/em&gt; différemment. Pas un constat, une prétention dont la valeur dépend strictement de ce qui suit. Si rien ne suit, la prétention est nulle. La règle vaut hors IA aussi. Un humain qui dit &lt;em&gt;« je viens de vérifier, le compteur est à 1247 »&lt;/em&gt; sans coller la requête vit dans la même zone grise. Niran n'a jamais eu besoin qu'on lui explique cette différence. Au judo, la chute n'est pas un avis sur la chute, c'est le sol qui répond.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Code compagnon&lt;/strong&gt;, &lt;a href="https://github.com/michelfaure/rembrandt-samples/tree/main/material-verification" rel="noopener noreferrer"&gt;&lt;code&gt;rembrandt-samples/material-verification/&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;CLAUDE.md.snippet&lt;/code&gt; + &lt;code&gt;verify-head-builds.sh&lt;/code&gt;, MIT.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;La règle d'évidentialité ci-dessus est désormais R1 du Counterpart Toolkit : &lt;a href="https://github.com/michelfaure/doctrine-counterpart" rel="noopener noreferrer"&gt;github.com/michelfaure/doctrine-counterpart&lt;/a&gt;, 14 règles opérationnelles, install en 1 commande, CC-BY-4.0.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>debugging</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why 'green build' without the raw output has zero evidentiary value</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Tue, 19 May 2026 10:07:56 +0000</pubDate>
      <link>https://forem.com/michelfaure/why-green-build-without-the-raw-output-has-zero-evidentiary-value-3epe</link>
      <guid>https://forem.com/michelfaure/why-green-build-without-the-raw-output-has-zero-evidentiary-value-3epe</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%2Fs4ikr429z9pakbbnktub.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%2Fs4ikr429z9pakbbnktub.png" alt="Comic strip — Michel pushes a " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you have 30 seconds.&lt;/strong&gt; When an AI agent declares that a build is green, that tests pass, that drift has been detected, or that a contact "doesn't exist" in the database, the sentence isn't a proof, it's an assertion. In most cases it's right. When it's wrong, it's wrong the same way, model-internal confidence decoupled from verifiable external state. Survival rule after 118,808 lines produced in 32 effective days with Claude Code, &lt;strong&gt;every factual claim comes with its material proof in the same message, or has zero evidentiary value&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Four false positives in two hours
&lt;/h2&gt;

&lt;p&gt;April 10th, 2026, afternoon, overhaul of a sensitive ERP module. I chain five blocks of changes with a Claude Code agent, and at every step the agent returns the same line, &lt;em&gt;"Compiled successfully."&lt;/em&gt; I push. The runtime crashes. I reread the output, I see &lt;code&gt;QRCodeSVG&lt;/code&gt; referenced when the import has been removed, &lt;code&gt;isSeancePassed&lt;/code&gt; passed to a component that no longer accepts it, types not regenerated after a Supabase schema change, orphan JSX refs after a revert. Four consecutive announcements of green with four very real TypeScript errors behind them.&lt;/p&gt;

&lt;p&gt;Niran is at the desk next to mine, dark hoodie, the folded wrapper of his burger balanced on a corner of his laptop. He's reading a PDF in silence. On the fourth &lt;em&gt;Compiled successfully&lt;/em&gt; that turns out false, I turn toward him without saying anything; he looks up for a second, gives a small nod, goes back to his screen. The verbosity of the agent and the economy of gestures of the judoka are having the same conversation, and he's the one who's right.&lt;/p&gt;

&lt;p&gt;I don't blame the agent for lying. It &lt;em&gt;summarized&lt;/em&gt; what it thought it had seen. The summary is consistent with its internal state and out of phase with the matter. Not one of those four crashes would have happened if the agent had pasted the raw output of &lt;code&gt;pnpm build&lt;/code&gt; rather than its summarized reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The epistemic slip
&lt;/h2&gt;

&lt;p&gt;The problem isn't truthfulness, it's &lt;strong&gt;evidentiary value&lt;/strong&gt;. An assertion without backing material can't be verified, it's believed or it isn't. The mechanism is known: an agent trained by &lt;em&gt;reinforcement learning from human feedback&lt;/em&gt; summarises by default because it learned humans prefer brief answers over verbose logs; what is a service in chat becomes a trap in production. &lt;em&gt;"The build is green"&lt;/em&gt; without the raw compiler output can be neither denied nor confirmed by a third party, it's a state of mind of the system, not a fact of the world. As long as we don't distinguish these two regimes, we operate blind in the gray zone where silent regressions accumulate that no monitoring detects. The function of &lt;em&gt;summary&lt;/em&gt; isn't to be false, it's not to be verifiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five forms of the same evasion
&lt;/h2&gt;

&lt;p&gt;The evasion is always the same. A state verb in the present tense (&lt;em&gt;"is"&lt;/em&gt;, &lt;em&gt;"passes"&lt;/em&gt;, &lt;em&gt;"confirms"&lt;/em&gt;, &lt;em&gt;"missing"&lt;/em&gt;) without anchoring to an external verifiable artifact. Five variants I encounter in the Rembrandt repo.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;"The build is green."&lt;/em&gt; Without the raw output of &lt;code&gt;pnpm build&lt;/code&gt; or &lt;code&gt;tsc --noEmit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Tests pass, CI is green."&lt;/em&gt; Without the runner report or the run URL.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"Drift detected between DB enum and TS enum"&lt;/em&gt;, or its inverse, &lt;em&gt;"the contact does not exist in the database"&lt;/em&gt;. Without the executed SQL query or its raw rows.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"EXPLAIN ANALYZE confirms the index is used."&lt;/em&gt; Without the raw plan, on the exact query the application sends. Not the target table in isolation, an intermediate view with a &lt;code&gt;CASE COALESCE&lt;/code&gt; can kill the index without the isolation revealing it.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;"The webhook returns 400 / 422."&lt;/em&gt; Without the raw payload from the partner side, when nine times out of ten that's where the diagnosis starts.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All share the same grammar. The countermeasure is uniform. Don't ask the agent to reason better, ask it to attach the command and its raw output, or the SQL query and its rows, or the payload, &lt;strong&gt;in the same message&lt;/strong&gt; as the assertion. Without that, the assertion is worth nothing. Not &lt;em&gt;little&lt;/em&gt;, nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule, codified in the root &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Material verification (excerpt from root CLAUDE.md)&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Any claim "build green / tests pass / CI green / drift detected /
  contact not found / OK" must be accompanied &lt;span class="ge"&gt;*in the same message*&lt;/span&gt;
  by the verification command and its raw output.
&lt;span class="p"&gt;-&lt;/span&gt; Any number relayed to a human must be verified by SQL query before
  being relayed.
&lt;span class="p"&gt;-&lt;/span&gt; EXPLAIN ANALYZE on a production query: execute on the exact query
  the application sends (view/RPC included), not on the target table
  in isolation. Two consecutive runs before judging.
&lt;span class="p"&gt;-&lt;/span&gt; On 400/422 from an external partner: demand the raw payload before
  proposing a fix.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`tsc --noEmit`&lt;/span&gt; CLI = authority, IDE panel = potentially stale.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Human-side, thirty seconds of friction per exchange, zero broken pushes on the workstreams where the rule holds. The written rule isn't enough on its own, though. A discipline that relies on memory erodes. You have to harden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hardening with a script
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;scripts/verify-head-builds.sh&lt;/code&gt;&lt;/strong&gt; stashes the working tree, runs &lt;code&gt;tsc --noEmit&lt;/code&gt; on HEAD, and restores. That's what lets me demand a green build &lt;em&gt;on the commit I'm about to push&lt;/em&gt;, not on the working tree mixed with non-staged changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# scripts/verify-head-builds.sh — load-bearing extract&lt;/span&gt;
&lt;span class="nv"&gt;HAS_LOCAL_CHANGES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; git diff &lt;span class="nt"&gt;--quiet&lt;/span&gt; HEAD 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git ls-files &lt;span class="nt"&gt;--others&lt;/span&gt; &lt;span class="nt"&gt;--exclude-standard&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&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;then
  &lt;/span&gt;&lt;span class="nv"&gt;HAS_LOCAL_CHANGES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="k"&gt;fi

if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nv"&gt;$HAS_LOCAL_CHANGES&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 1 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;git stash push &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--message&lt;/span&gt; &lt;span class="s2"&gt;"verify-head-builds-autostash-&lt;/span&gt;&lt;span class="nv"&gt;$$&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# typecheck on pure HEAD, no working copy contamination&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt; 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✓ HEAD compiles cleanly — safe to push"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✗ HEAD does NOT compile — fix before push"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;strong&gt;a writing convention on the agent side&lt;/strong&gt;: every number relayed to a human comes with the SQL query that produced it, as a copy-paste-ready block. No orphan numbers, in prose or in commit messages. Noisy the first week, transparent after.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can copy into your project
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Full snippets&lt;/strong&gt; (&lt;code&gt;CLAUDE.md&lt;/code&gt; evidentiality rule excerpt, full &lt;code&gt;verify-head-builds.sh&lt;/code&gt; script) in the &lt;a href="https://github.com/michelfaure/rembrandt-samples/tree/main/material-verification" rel="noopener noreferrer"&gt;&lt;code&gt;material-verification/&lt;/code&gt;&lt;/a&gt; folder of the series companion repo, MIT.&lt;/p&gt;

&lt;p&gt;Three directly applicable practices for working with a coding agent:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;An evidentiality rule in the root &lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/strong&gt; (five bullets above). Without this written anchor, the rest is cosmetic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The &lt;code&gt;verify-head-builds.sh&lt;/code&gt; script&lt;/strong&gt;: stash + &lt;code&gt;tsc --noEmit&lt;/code&gt; on HEAD + restore. Thirteen lines of bash, MIT.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A traceable-numbers convention.&lt;/strong&gt; Every number paired with its SQL query in the same message. No orphan numbers in prose, none in commit messages.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And you, which green claim did you stop believing first? I read the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you stop believing
&lt;/h2&gt;

&lt;p&gt;After a few weeks of practice, you hear &lt;em&gt;"Compiled successfully"&lt;/em&gt; differently. Not as an observation, as a claim whose value depends strictly on what follows. If nothing follows, the claim is null. The rule applies outside AI too. A human saying &lt;em&gt;"I just checked, the count is 1247"&lt;/em&gt; without pasting the query lives in the same gray zone. Niran has never needed someone to explain that difference to him. In judo, the fall isn't an opinion about the fall, it's the floor that answers.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Companion code&lt;/strong&gt;, &lt;a href="https://github.com/michelfaure/rembrandt-samples/tree/main/material-verification" rel="noopener noreferrer"&gt;&lt;code&gt;rembrandt-samples/material-verification/&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;CLAUDE.md.snippet&lt;/code&gt; + &lt;code&gt;verify-head-builds.sh&lt;/code&gt;, MIT.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The evidentiality rule above is now R1 of the Counterpart Toolkit: &lt;a href="https://github.com/michelfaure/doctrine-counterpart" rel="noopener noreferrer"&gt;github.com/michelfaure/doctrine-counterpart&lt;/a&gt;, 14 operational rules, install in 1 command, CC-BY-4.0.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>debugging</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Pourquoi ta requête Supabase s'arrête à exactement 1000 lignes (et ne te le dit jamais)</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Tue, 19 May 2026 07:56:26 +0000</pubDate>
      <link>https://forem.com/michelfaure/pourquoi-ta-requete-supabase-sarrete-a-exactement-1000-lignes-et-ne-te-le-dit-jamais-18hj</link>
      <guid>https://forem.com/michelfaure/pourquoi-ta-requete-supabase-sarrete-a-exactement-1000-lignes-et-ne-te-le-dit-jamais-18hj</guid>
      <description>&lt;h2&gt;
  
  
  La nuit où un dropdown m'a menti
&lt;/h2&gt;

&lt;p&gt;Vingt-trois heures, fin avril. Une utilisatrice de mon outil interne me signale un filtre incomplet sur un dropdown. Mon premier diagnostic accuse un sous-filtre côté interface. Je le démonte, j'isole la source du dataset, je relance sans filtre. Le compteur retourne mille pile. Or la table en porte mille deux cent trois. Je remonte le pipeline, niveau par niveau, jusqu'à la requête source. Quatre niveaux plus haut, le coupable apparaît : un &lt;code&gt;.select()&lt;/code&gt; chaîné sur &lt;code&gt;.from()&lt;/code&gt;, sans &lt;code&gt;.order()&lt;/code&gt;, sans &lt;code&gt;.limit()&lt;/code&gt;. La requête réussit. Elle ment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le mécanisme
&lt;/h2&gt;

&lt;p&gt;Toute requête PostgREST qui ne déclare pas son &lt;code&gt;ORDER BY&lt;/code&gt; reçoit en interne un tri par &lt;code&gt;ctid&lt;/code&gt;, l'identifiant de tuple physique Postgres, et un plafond &lt;code&gt;Range&lt;/code&gt; HTTP à 1000 lignes appliqué par Supabase. La requête réussit. Aucune exception, aucun warning, aucune trace dans Sentry. Le client reçoit un sous-ensemble dont l'ordre dépend de l'historique des &lt;code&gt;UPDATE&lt;/code&gt; et &lt;code&gt;DELETE&lt;/code&gt; de la table, rebrassé après un &lt;code&gt;VACUUM FULL&lt;/code&gt; ou un &lt;code&gt;pg_repack&lt;/code&gt;. Le bug n'existe qu'au-dessus de mille lignes en prod.&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;// silencieusement plafonné à 1000 lignes&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// le Range HTTP applique son LIMIT sur un tri stable&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  La règle ESLint qui ferme la porte
&lt;/h2&gt;

&lt;p&gt;Le pattern est trop discret pour tenir uniquement dans la revue de code. Je l'ai migré côté lint, en visitor AST sur &lt;code&gt;CallExpression&lt;/code&gt;, qui exige qu'un &lt;code&gt;.select()&lt;/code&gt; chaîné sur un &lt;code&gt;.from()&lt;/code&gt; porte un &lt;code&gt;.order()&lt;/code&gt; quelque part en aval, sauf lorsque la chaîne se termine par &lt;code&gt;.single()&lt;/code&gt;, &lt;code&gt;.maybeSingle()&lt;/code&gt;, ou un &lt;code&gt;.limit()&lt;/code&gt; explicite à valeur inférieure ou égale à mille. C'est l'un des cinq garde-fous structurels d'une rule Supabase exploitable. Sans les autres, le bruit submerge la rule en moins d'une heure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;problem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;unordered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select() without .order() falls back to ORDER BY ctid.&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="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;CallExpression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callee&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;chainContainsFromCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chainHasSafeTerminator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;        &lt;span class="c1"&gt;// .single, .order, .csv...&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;selectOptsHasHeadTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;         &lt;span class="c1"&gt;// count head&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chainEndsAtAssignment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;         &lt;span class="c1"&gt;// let q = supabase...&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chainIsInsideHelper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetchAll&lt;/span&gt;&lt;span class="dl"&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unordered&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="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;h2&gt;
  
  
  L'ampleur réelle
&lt;/h2&gt;

&lt;p&gt;Une fois la rule promue en &lt;code&gt;error&lt;/code&gt;, le premier audit a remonté cent soixante-dix-huit alertes, étalées sur cinquante-six fichiers. Quarante pourcent étaient des faux positifs : variable réassignée à distance, write returning, helper de pagination qui injecte son propre &lt;code&gt;.order()&lt;/code&gt;, count head, terminateur monoligne. Les cinq garde-fous structurels ont ramené le bruit à cent huit vraies cibles avant de toucher la moindre ligne applicative.&lt;/p&gt;

&lt;h2&gt;
  
  
  La règle
&lt;/h2&gt;

&lt;p&gt;Toute chaîne &lt;code&gt;.from(X).select(...)&lt;/code&gt; non triviale porte un &lt;code&gt;.order()&lt;/code&gt; explicite. Pas d'option, pas de tiède.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Rule complète, paire avant/après et helper &lt;code&gt;fetchAll&lt;/code&gt; pseudonymisés :&lt;/em&gt;&lt;br&gt;
&lt;a href="https://github.com/michelfaure/rembrandt-samples/tree/main/postgrest-row-cap" rel="noopener noreferrer"&gt;github.com/michelfaure/rembrandt-samples/tree/main/postgrest-row-cap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ce default PostgREST silencieux est exactement le cas archétypal de R12 du Counterpart Toolkit (« cite the official text, materialise vendor defaults »). 14 règles, install en 1 commande :&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>supabase</category>
      <category>postgrest</category>
      <category>postgres</category>
      <category>eslint</category>
    </item>
    <item>
      <title>Why your Supabase query stops at exactly 1000 rows (and never tells you)</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Tue, 19 May 2026 07:56:11 +0000</pubDate>
      <link>https://forem.com/michelfaure/why-your-supabase-query-stops-at-exactly-1000-rows-and-never-tells-you-4g57</link>
      <guid>https://forem.com/michelfaure/why-your-supabase-query-stops-at-exactly-1000-rows-and-never-tells-you-4g57</guid>
      <description>&lt;h2&gt;
  
  
  The night a dropdown lied to me
&lt;/h2&gt;

&lt;p&gt;Eleven at night, late April. A user of my internal tool reports an incomplete filter on a dropdown. My first diagnosis blames a sub-filter on the UI side. I take it apart, I isolate the dataset source, I re-run without filters. The counter returns a thousand on the nose. The table holds one thousand two hundred and three rows. I walk up the pipeline, level by level, all the way to the source query. Four levels higher, the culprit appears: a &lt;code&gt;.select()&lt;/code&gt; chained on &lt;code&gt;.from()&lt;/code&gt;, no &lt;code&gt;.order()&lt;/code&gt;, no &lt;code&gt;.limit()&lt;/code&gt;. The query succeeds. It lies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism
&lt;/h2&gt;

&lt;p&gt;Any PostgREST query that doesn't declare its &lt;code&gt;ORDER BY&lt;/code&gt; receives an internal &lt;code&gt;ctid&lt;/code&gt; sort — the physical tuple identifier in Postgres — plus a 1000-row &lt;code&gt;Range&lt;/code&gt; HTTP cap applied by Supabase. The query succeeds. No exception, no warning, no Sentry breadcrumb. The client gets a subset whose order depends on the table's &lt;code&gt;UPDATE&lt;/code&gt; and &lt;code&gt;DELETE&lt;/code&gt; history, reshuffled after a &lt;code&gt;VACUUM FULL&lt;/code&gt; or &lt;code&gt;pg_repack&lt;/code&gt;. The bug only exists above one thousand rows in production.&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;// silently capped at 1000 rows&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// the Range HTTP cap applies on a stable sort&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;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;login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The ESLint rule that closes the door
&lt;/h2&gt;

&lt;p&gt;The pattern is too quiet to live in code review alone. I moved it to lint, as an AST visitor on &lt;code&gt;CallExpression&lt;/code&gt;, that requires a &lt;code&gt;.select()&lt;/code&gt; chained on &lt;code&gt;.from()&lt;/code&gt; to carry an &lt;code&gt;.order()&lt;/code&gt; somewhere downstream, unless the chain terminates with &lt;code&gt;.single()&lt;/code&gt;, &lt;code&gt;.maybeSingle()&lt;/code&gt;, or an explicit &lt;code&gt;.limit()&lt;/code&gt; below or equal to a thousand. It's one of five structural guards a workable Supabase rule needs. Without the others, the noise drowns the rule in under an hour.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;problem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;unordered&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select() without .order() falls back to ORDER BY ctid.&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="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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="p"&gt;{&lt;/span&gt;
      &lt;span class="nc"&gt;CallExpression&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&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;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callee&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;property&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;select&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;chainContainsFromCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chainHasSafeTerminator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;        &lt;span class="c1"&gt;// .single, .order, .csv...&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;selectOptsHasHeadTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;         &lt;span class="c1"&gt;// count head&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chainEndsAtAssignment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;         &lt;span class="c1"&gt;// let q = supabase...&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;chainIsInsideHelper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetchAll&lt;/span&gt;&lt;span class="dl"&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;report&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unordered&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="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;h2&gt;
  
  
  The real scale
&lt;/h2&gt;

&lt;p&gt;Once the rule was promoted to &lt;code&gt;error&lt;/code&gt;, the first audit raised one hundred and seventy-eight alerts, spread over fifty-six files. Forty percent were false positives: variable reassigned at a distance, write returning, pagination helper injecting its own &lt;code&gt;.order()&lt;/code&gt;, count head, single-row terminator. The five structural guards brought the noise down to one hundred and eight real targets before touching a single line of application code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Every non-trivial &lt;code&gt;.from(X).select(...)&lt;/code&gt; chain carries an explicit &lt;code&gt;.order()&lt;/code&gt;. No option, no lukewarm.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full rule, before/after pair and &lt;code&gt;fetchAll&lt;/code&gt; helper, pseudonymized:&lt;/em&gt;&lt;br&gt;
&lt;a href="https://github.com/michelfaure/rembrandt-samples/tree/main/postgrest-row-cap" rel="noopener noreferrer"&gt;github.com/michelfaure/rembrandt-samples/tree/main/postgrest-row-cap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This silent PostgREST default is exactly the archetypal case of R12 of the Counterpart Toolkit ("cite the official text, materialise vendor defaults"). 14 rules, install in 1 command:&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>supabase</category>
      <category>postgrest</category>
      <category>postgres</category>
      <category>eslint</category>
    </item>
    <item>
      <title>Forcez Claude Code à vous contredire : 14 règles, install en 1 commande</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Mon, 18 May 2026 10:19:20 +0000</pubDate>
      <link>https://forem.com/michelfaure/forcez-claude-code-a-vous-contredire-14-regles-install-en-1-commande-1271</link>
      <guid>https://forem.com/michelfaure/forcez-claude-code-a-vous-contredire-14-regles-install-en-1-commande-1271</guid>
      <description>&lt;h2&gt;
  
  
  L'enseignement de 60 jours, le ROI en trois axes
&lt;/h2&gt;

&lt;p&gt;Trente-deux jours de production en solo sur un ERP, 118 808 lignes de TypeScript, six versions de doctrine, quatre relecteurs externes intégrés. J'ai compilé ce que j'ai appris en quatorze règles opérationnelles, installables en une commande : le Counterpart Toolkit v0.4.1. C'est à la fois l'enseignement matériel de soixante jours de codage solo avec Claude Code, et la cartographie des quatorze failure modes silencieux que j'ai vu se répéter — pour qui code seul avec une IA en production et n'a plus de PR review pour attraper la dérive.&lt;/p&gt;

&lt;p&gt;Le ROI est chiffré sur trois axes, mesuré sur Rembrandt :&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;R4 *Falsify before fix&lt;/strong&gt;* — cinq à dix minutes de protocole en amont du fix évitent trente à quatre-vingt-dix minutes de cycle fix-puis-rollback quand la première hypothèse plausible se révèle fausse. ROI 6 à 18× par incident. Sur soixante jours, j'ai cessé de perdre une heure deux à trois fois par semaine sur des fixes qui ne fixaient rien.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;R2 *Filesystem over summary&lt;/strong&gt;* couplée à &lt;strong&gt;R6 *Live/Snapshot/Cache&lt;/strong&gt;* et aux sondes drift quotidiennes — le délai médian apparition→détection d'une divergence silencieuse passe d'invisible à &lt;strong&gt;35,3 jours&lt;/strong&gt; sur 90 jours glissants. M3 recalibrée publiquement à ≤ 30 jours dans le manifesto, parce que la cible originale (≤ 7 j) était une intuition que la pratique a refusée.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Un sub-agent challenger&lt;/strong&gt; qui produit des objections au format imposé &lt;em&gt;Tool / Question / Refutation criterion&lt;/em&gt;. Du désaccord matériel, pas du &lt;em&gt;« are you sure? »&lt;/em&gt; émotionnel qui pousse à réviser sans fait nouveau.&lt;/p&gt;

&lt;p&gt;Voici comment, en 1400 mots et une commande d'install.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le diagnostic — l'incident qui a déclenché la doctrine
&lt;/h2&gt;

&lt;p&gt;Coder seul avec une IA, c'est composer deux complaisances. Celle de l'agent, sycophant par construction parce que le &lt;em&gt;reinforcement learning from human feedback&lt;/em&gt; l'a entraîné à plaire au prompteur. Et celle du solo, &lt;em&gt;self-validating&lt;/em&gt; par humanité, qui valide son propre travail parce qu'il ne reste plus personne pour le contester. Mises bout à bout, ces deux complaisances produisent un drift que ni l'agent ni l'humain ne signale — et qui ne se voit qu'à l'audit, longtemps après.&lt;/p&gt;

&lt;p&gt;C'est en préparant l'audit source unique de fin avril — ADR-0024, un travail de fond sur les divergences que je remettais à plus tard depuis trois mois — que je suis tombé sur l'écart, par hasard, en croisant deux requêtes que personne n'avait jamais croisées avant. Une fiche élève, initiale Y.B. : la colonne &lt;code&gt;contacts.montant_total&lt;/code&gt; portait 1 159 € saisis à la main quelque part en 2024, jamais touchés depuis. La somme réelle des échéances, calculée à la volée, en faisait 2 262 €. Mille euros d'écart, sur une seule fiche, sans qu'aucune alarme n'ait jamais sonné. J'élargis le grep : cinq cent soixante contacts dans le même état, parfois à plusieurs milliers d'euros près. Et pourtant &lt;code&gt;montant_total&lt;/code&gt; était lue chaque jour dans le dashboard trésorerie — une valeur dérivable qu'on stockait sans rafraîchisseur, traitée comme un fait passé immuable alors qu'elle aurait dû vivre à la volée. C'est exactement le piège que R6 &lt;em&gt;Live/Snapshot/Cache&lt;/em&gt; veut empêcher, et R6 est sortie de ce moment-là.&lt;/p&gt;

&lt;h2&gt;
  
  
  R4 &lt;em&gt;Falsify before fix&lt;/em&gt;, la seule règle exposée ici
&lt;/h2&gt;

&lt;p&gt;Le toolkit énonce R4 en cinq étapes textuelles. Le skill &lt;code&gt;falsify-before-fix&lt;/code&gt; en est l'&lt;em&gt;invocable instance&lt;/em&gt; — la version que Claude Code charge dans sa session, et qu'il ne peut pas sauter quand il s'apprête à écrire du code de fix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;falsify-before-fix&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Activate this skill before writing the fix code on a bug or&lt;/span&gt;
  &lt;span class="s"&gt;incident. Triggers on "fix", "bug", "patch", "hotfix", "workaround",&lt;/span&gt;
  &lt;span class="s"&gt;"doesn't work", "diagnose", "hypothesis", "root cause". Enforces a&lt;/span&gt;
  &lt;span class="s"&gt;single-sentence causal hypothesis and three material probes designed&lt;/span&gt;
  &lt;span class="s"&gt;to refute it before any line of fix code is committed.&lt;/span&gt;
  &lt;span class="s"&gt;Operational instance of R4 of the Counterpart Toolkit.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Le protocole tient en cinq étapes : &lt;strong&gt;(1)&lt;/strong&gt; formuler une hypothèse causale en une phrase (pas un symptôme — &lt;em&gt;« le compteur lit depuis l'ancienne table après la migration du 12 mai »&lt;/em&gt; vaut mieux que &lt;em&gt;« le compteur est faux »&lt;/em&gt;) ; &lt;strong&gt;(2)&lt;/strong&gt; lister trois sondes conçues pour &lt;strong&gt;réfuter&lt;/strong&gt;, pas pour confirmer, parce qu'une sonde de confirmation trouve toujours ce qu'elle cherche, par sélection ; chaque sonde porte ses trois champs &lt;em&gt;Tool&lt;/em&gt; / &lt;em&gt;Question&lt;/em&gt; / &lt;em&gt;Refutation criterion&lt;/em&gt; ; &lt;strong&gt;(3)&lt;/strong&gt; exécuter et reporter la sortie brute, jamais paraphrasée ; &lt;strong&gt;(4)&lt;/strong&gt; brancher — aucune sonde ne réfute → on écrit le fix ; une sonde réfute → on repart d'une nouvelle hypothèse ; sondes ambiguës → quatrième sonde plus tranchante avant tout code ; &lt;strong&gt;(5)&lt;/strong&gt; sortir hypothèse retenue, sondes exécutées, diff, et critère d'observation post-fix.&lt;/p&gt;

&lt;p&gt;Pourquoi un skill et pas la règle textuelle qui vivait déjà en CLAUDE.md ? Parce que la règle textuelle n'avait pas tenu sous pression. Le 6 mai, en début d'après-midi, un bug Sentry remonte que le compteur d'inscriptions du jour affiche zéro alors qu'on en a saisi trois en matinée. Mon réflexe arrive avant le protocole, et la main est déjà sur le clavier — &lt;em&gt;« le cache n'est pas invalidé »&lt;/em&gt;, je commit, je déploie. Trente minutes plus tard, rollback : le bug est toujours là. La cause réelle, qu'une sonde grep de quatre-vingt-dix secondes aurait remontée, c'est qu'aucun appel à &lt;code&gt;cache_invalidate&lt;/code&gt; n'existait dans le pipeline d'inscription — pas un cache obsolète, un cache absent. R4 était dans CLAUDE.md depuis trois semaines. Je ne l'ai simplement pas suivie ce jour-là, parce qu'aucun dispositif n'interrompait ma course entre le bug et le commit.&lt;/p&gt;

&lt;p&gt;C'est la différence qui fait tout. Une règle textuelle dans CLAUDE.md, l'agent la lit au début de la session, et rien ne l'oblige à la convoquer au moment exact où il en aurait besoin. Un skill, c'est un mécanisme matériel : il se charge automatiquement sur des mots-clés (&lt;em&gt;« fix »&lt;/em&gt;, &lt;em&gt;« bug »&lt;/em&gt;, &lt;em&gt;« doesn't work »&lt;/em&gt;) et impose son protocole dans la session active — pas un rappel à se faire, un interrupteur que la session a déjà actionné. Dix jours après l'incident du 6 mai, le skill &lt;code&gt;falsify-before-fix&lt;/code&gt; est commit. L'enseignement de l'échec devient un dispositif. C'est la seule des 14 règles exposée dans cet article, et les 13 autres sont dans le repo, toutes construites sur le même principe : matérialiser ce qui resterait pieux en textuel seul.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le toolkit s'applique à lui-même, chiffres et rétractations
&lt;/h2&gt;

&lt;p&gt;Six versions en 32 jours, chacune ancrée dans un fait nouveau documenté. v0.3 → v0.3.1 sur un premier relecteur externe (apparat théorique disproportionné, rétracté). v0.3.1 → v0.3.2 sur un second (sept recommandations, deux work-items ouverts). v0.3.3 instrumentation M1-M5 publiée. v0.4 séparation toolkit/manifesto sur un troisième. v0.4 → v0.4.1 sur un quatrième relecteur externe (Claude.ai web), un commit consolidé intégrant trois refactors plus la LOC mesurée.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LOC corrigé 118 808 lignes&lt;/strong&gt; mesurées par &lt;code&gt;find + wc -l&lt;/code&gt; sur TS/TSX/JS/JSX (exclusions explicites), contre 35 k cité dans les versions antérieures — la doctrine elle-même avait péché contre R2, &lt;em&gt;Cache&lt;/em&gt; sans rafraîchisseur projeté sur sa propre description. &lt;strong&gt;M3 recalibrée ≤ 7 j → ≤ 30 j&lt;/strong&gt; avec justification publique : la cible originale était une intuition non honorée par les 35,3 jours mesurés en pratique. &lt;strong&gt;M1 et M5 documentés comme échecs d'instrumentation&lt;/strong&gt;, pas comme succès : M1 sur-sensible à 12,33 vs ≤ 1 cible, M5 classe 90 % des briefs en &lt;code&gt;unknown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Four external readers across six versions. The last two audited v0.3.3 then v0.4.1 — their objections are integrated in the release notes&lt;/em&gt; (&lt;a href="https://github.com/michelfaure/doctrine-counterpart/blob/main/manifesto.md" rel="noopener noreferrer"&gt;manifesto §From v0.4 to v0.4.1&lt;/a&gt;). One reviewer called the &lt;code&gt;falsify-before-fix&lt;/code&gt; skill &lt;em&gt;"the best artifact of the doctrine among the ones I read"&lt;/em&gt;. v0.5 prévue 15 juillet 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;Steal three things in 20 minutes&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Trois règles à essayer avant d'installer le reste.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;R4 *Falsify before fix&lt;/strong&gt;* — empêche le cycle fix → rollback déclenché par la première hypothèse plausible mais fausse. Détaillée ci-dessus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;R6 *Live / Snapshot / Cache&lt;/strong&gt;* — empêche qu'une valeur dérivée stockée diverge silencieusement de sa source. Toute colonne dérivable déclare sa catégorie dans le commit qui la crée, ou le commit est rejeté.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;R10 *Silent failure forbidden&lt;/strong&gt;* — empêche que &lt;code&gt;catch {}&lt;/code&gt;, &lt;code&gt;await&lt;/code&gt; sans destructuration &lt;code&gt;{ error }&lt;/code&gt;, &lt;code&gt;2&amp;gt;/dev/null&lt;/code&gt; et autres mécanismes d'avalement mentent à votre observabilité jusqu'à ce que la production craque sur une dépendance en aval.&lt;/p&gt;

&lt;p&gt;Le repo : &lt;a href="https://github.com/michelfaure/doctrine-counterpart" rel="noopener noreferrer"&gt;github.com/michelfaure/doctrine-counterpart&lt;/a&gt;. La commande d'install :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/michelfaure/doctrine-counterpart.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;cd &lt;/span&gt;doctrine-counterpart &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ./install.sh &lt;span class="nt"&gt;--yes&lt;/span&gt; /path/to/your/project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Licence CC-BY-4.0. Le manifesto promet une citation nominative dans la v0.5 pour qui propose un retour exploitable — &lt;em&gt;quelle règle manque pour votre stack ?&lt;/em&gt; Les commentaires DEV.to sont inputs directs pour la prochaine version. &lt;strong&gt;R14 *spike escape hatch&lt;/strong&gt;* couvre le code prototype destiné à disparaître sous sept jours, exempté de R6/R7/R8 : l'adoption ne force pas la même friction au spike qu'au code de production.&lt;/p&gt;

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

&lt;p&gt;Un agent qui ne vous contredit pas n'est pas un counterpart, c'est une dactylo plus rapide. Ces 14 règles restaurent le désaccord — matériellement, pas mentalement. Elles ne demandent pas à l'agent d'être moins sycophant, ni au solo d'être plus vigilant ; elles posent les dispositifs (skills invocables, hooks bloquants, sub-agent challenger) qui interrompent la course productive là où la complaisance se compose. Le toolkit est la prothèse qu'il reste au solo quand le PR review a disparu et qu'il refuse de coder à l'oreille. Si une seule des 14 vous évite un cycle fix-rollback la semaine prochaine, elle s'est déjà remboursée.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Counterpart Toolkit v0.4.1, fourteen operational rules in ~200 lines, six iterations in 32 days, four external reviews integrated. Tested on 60+ days of solo ERP (118 808 lines, 65+ ADRs). Licence CC-BY-4.0 : github.com/michelfaure/doctrine-counterpart&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>doctrine</category>
    </item>
    <item>
      <title>Make Claude Code disagree with you: a 14-rule counterpart toolkit (install in 1 command)</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Mon, 18 May 2026 10:18:33 +0000</pubDate>
      <link>https://forem.com/michelfaure/make-claude-code-disagree-with-you-a-14-rule-counterpart-toolkit-install-in-1-command-11pe</link>
      <guid>https://forem.com/michelfaure/make-claude-code-disagree-with-you-a-14-rule-counterpart-toolkit-install-in-1-command-11pe</guid>
      <description>&lt;h2&gt;
  
  
  The 60-day lesson, ROI on three axes
&lt;/h2&gt;

&lt;p&gt;Thirty-two days of solo production on an ERP, 118,808 lines of TypeScript, six doctrine versions, four external reviewers integrated. I've compiled what I learned into fourteen operational rules, installable in one command: the Counterpart Toolkit v0.4.1. It is both the material lesson of sixty days coding solo with Claude Code, and the mapping of the fourteen silent failure modes I've seen repeat — for anyone coding alone with an AI in production, who no longer has a PR review to catch the drift.&lt;/p&gt;

&lt;p&gt;ROI is quantified on three axes, measured on Rembrandt:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;R4 *Falsify before fix&lt;/strong&gt;* — five to ten minutes of upstream protocol prevent thirty to ninety minutes of fix-then-rollback cycle when the first plausible hypothesis turns out wrong. ROI 6 to 18× per incident. Over sixty days, I stopped losing an hour two to three times a week on fixes that fixed nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;R2 *Filesystem over summary&lt;/strong&gt;* paired with &lt;strong&gt;R6 *Live/Snapshot/Cache&lt;/strong&gt;* and the daily drift probes — median apparition→detection of a silent divergence drops from invisible to &lt;strong&gt;35.3 days&lt;/strong&gt; over a 90-day rolling window. M3 publicly recalibrated to ≤ 30 days in the manifesto, because the original target (≤ 7 days) was an intuition practice refused.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A sub-agent challenger&lt;/strong&gt; producing objections in the imposed format &lt;em&gt;Tool / Question / Refutation criterion&lt;/em&gt;. Material disagreement, not emotional &lt;em&gt;"are you sure?"&lt;/em&gt; that pushes you to revise without a new fact.&lt;/p&gt;

&lt;p&gt;Here's how, in 1400 words and one install command.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnosis — the incident that triggered the doctrine
&lt;/h2&gt;

&lt;p&gt;Coding alone with an AI means compounding two complaisances. The agent's, sycophantic by construction because reinforcement learning from human feedback trained it to please the prompter. And the solo's, self-validating by humanity, who validates their own work because no one is left to contest it. End to end, these two complaisances produce a drift that neither agent nor human flags — and that only surfaces at audit time, long after.&lt;/p&gt;

&lt;p&gt;It was while preparing the late-April source-of-truth audit — ADR-0024, a deep-dive on divergences I had been putting off for three months — that I stumbled onto the gap, by chance, crossing two queries no one had ever crossed before. One student record, initials Y.B.: the &lt;code&gt;contacts.montant_total&lt;/code&gt; column carried €1,159 entered by hand somewhere in 2024, untouched since. The actual sum of instalments, computed on the fly, came to €2,262. A thousand-euro gap, on a single record, with no alarm ever ringing. I widened the grep: five hundred and sixty contacts in the same state, some off by several thousand euros. And yet &lt;code&gt;montant_total&lt;/code&gt; was read every day in the treasury dashboard — a derivable value being stored without a refresher, treated as an immutable past fact when it should have lived on the fly. This is exactly the trap R6 &lt;em&gt;Live/Snapshot/Cache&lt;/em&gt; is meant to prevent, and R6 came out of that moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  R4 &lt;em&gt;Falsify before fix&lt;/em&gt;, the one rule exposed here
&lt;/h2&gt;

&lt;p&gt;The toolkit states R4 as a five-step textual protocol. The &lt;code&gt;falsify-before-fix&lt;/code&gt; skill is its &lt;em&gt;invocable instance&lt;/em&gt; — the version Claude Code loads into its session, and cannot skip when about to write fix code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;falsify-before-fix&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Activate this skill before writing the fix code on a bug or&lt;/span&gt;
  &lt;span class="s"&gt;incident. Triggers on "fix", "bug", "patch", "hotfix", "workaround",&lt;/span&gt;
  &lt;span class="s"&gt;"doesn't work", "diagnose", "hypothesis", "root cause". Enforces a&lt;/span&gt;
  &lt;span class="s"&gt;single-sentence causal hypothesis and three material probes designed&lt;/span&gt;
  &lt;span class="s"&gt;to refute it before any line of fix code is committed.&lt;/span&gt;
  &lt;span class="s"&gt;Operational instance of R4 of the Counterpart Toolkit.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The protocol holds in five steps: &lt;strong&gt;(1)&lt;/strong&gt; formulate the hypothesis in one sentence as a cause, not a symptom (&lt;em&gt;"the counter reads from the old table after the 12 May migration"&lt;/em&gt; beats &lt;em&gt;"the counter is wrong"&lt;/em&gt;); &lt;strong&gt;(2)&lt;/strong&gt; list three probes designed to &lt;strong&gt;refute&lt;/strong&gt;, not to confirm, because a confirmation probe always finds what it's looking for by selection; each probe carries its three fields &lt;em&gt;Tool&lt;/em&gt; / &lt;em&gt;Question&lt;/em&gt; / &lt;em&gt;Refutation criterion&lt;/em&gt;; &lt;strong&gt;(3)&lt;/strong&gt; execute and report raw output, never paraphrased; &lt;strong&gt;(4)&lt;/strong&gt; branch — no probe refutes → write the fix; one probe refutes → restart with a new hypothesis; ambiguous probes → fourth sharper probe before any code; &lt;strong&gt;(5)&lt;/strong&gt; output the retained hypothesis, the probes executed, the diff, and the post-fix observation criterion.&lt;/p&gt;

&lt;p&gt;Why a skill and not the textual rule that already lived in CLAUDE.md? Because the textual rule didn't hold under pressure. 6 May, mid-afternoon. A Sentry alert reports the day's enrolment counter at zero while three enrolments were entered that morning. My reflex arrives before the protocol, and my hand is already on the keyboard — &lt;em&gt;"the cache isn't invalidated."&lt;/em&gt; I commit, I deploy. Thirty minutes later, rollback: the bug is still there. The actual cause, that a ninety-second grep probe would have surfaced, is that no &lt;code&gt;cache_invalidate&lt;/code&gt; call existed in the enrolment pipeline at all — not a stale cache, an absent one. R4 had been in CLAUDE.md for three weeks. I simply didn't follow it that day, because no apparatus interrupted my course between bug and commit.&lt;/p&gt;

&lt;p&gt;That's the difference that makes everything. A textual rule in CLAUDE.md, the agent reads it at session start, and nothing forces it to summon the rule at the exact moment it would need it. A skill is something else: a material mechanism that loads automatically on keywords (&lt;em&gt;"fix"&lt;/em&gt;, &lt;em&gt;"bug"&lt;/em&gt;, &lt;em&gt;"doesn't work"&lt;/em&gt;) and imposes its protocol on the active session — not a self-reminder, a switch the session has already flipped for you. Ten days after the 6 May incident, the &lt;code&gt;falsify-before-fix&lt;/code&gt; skill was committed. The lesson of the failure becomes an apparatus. It is the one rule of the 14 exposed in this article, and the other 13 are in the repo, all built on the same principle: materialise what would remain pious in text alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The toolkit applied to itself — figures and retractions
&lt;/h2&gt;

&lt;p&gt;Six versions in 32 days, each anchored in a documented new fact. v0.3 → v0.3.1 on a first external reviewer (disproportionate theoretical apparatus, retracted). v0.3.1 → v0.3.2 on a second (seven recommendations, two open work-items). v0.3.3 — M1–M5 instrumentation published. v0.4 — toolkit/manifesto separation on a third reviewer. v0.4 → v0.4.1 on a fourth external reviewer (Claude.ai web), a consolidated commit integrating three refactors plus the measured LOC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LOC corrected to 118,808 lines&lt;/strong&gt; measured by &lt;code&gt;find + wc -l&lt;/code&gt; on TS/TSX/JS/JSX (explicit exclusions), against the 35k figure cited in earlier versions — the doctrine itself had sinned against R2, &lt;em&gt;Cache&lt;/em&gt; without refresher projected onto its own description. &lt;strong&gt;M3 recalibrated ≤ 7 d → ≤ 30 d&lt;/strong&gt; with public justification: the original target was an intuition unhonoured by the 35.3 days measured in practice. &lt;strong&gt;M1 and M5 documented as instrumentation failures&lt;/strong&gt;, not as successes: M1 over-sensitive at 12.33 vs ≤ 1 target, M5 classifies 90% of briefs as &lt;code&gt;unknown&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Four external readers across six versions. The last two audited v0.3.3 then v0.4.1 — their objections are integrated in the release notes&lt;/em&gt; (&lt;a href="https://github.com/michelfaure/doctrine-counterpart/blob/main/manifesto.md" rel="noopener noreferrer"&gt;manifesto §From v0.4 to v0.4.1&lt;/a&gt;). One reviewer called the &lt;code&gt;falsify-before-fix&lt;/code&gt; skill &lt;em&gt;"the best artifact of the doctrine among the ones I read"&lt;/em&gt;. v0.5 scheduled 15 July 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;em&gt;Steal three things in 20 minutes&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Three rules to try before installing the rest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;R4 *Falsify before fix&lt;/strong&gt;* — prevents the fix → rollback cycle triggered by the first plausible but wrong hypothesis. Detailed above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;R6 *Live / Snapshot / Cache&lt;/strong&gt;* — prevents a stored derived value from silently diverging from its source. Any derivable column declares its category in the commit that creates it, or the commit is rejected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;R10 *Silent failure forbidden&lt;/strong&gt;* — prevents &lt;code&gt;catch {}&lt;/code&gt;, &lt;code&gt;await&lt;/code&gt; without &lt;code&gt;{ error }&lt;/code&gt; destructuring, &lt;code&gt;2&amp;gt;/dev/null&lt;/code&gt; and other swallowing mechanisms from lying to your observability until production cracks on a downstream dependency.&lt;/p&gt;

&lt;p&gt;The repo: &lt;a href="https://github.com/michelfaure/doctrine-counterpart" rel="noopener noreferrer"&gt;github.com/michelfaure/doctrine-counterpart&lt;/a&gt;. The install command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/michelfaure/doctrine-counterpart.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;cd &lt;/span&gt;doctrine-counterpart &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ./install.sh &lt;span class="nt"&gt;--yes&lt;/span&gt; /path/to/your/project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;License CC-BY-4.0. The manifesto promises nominal citation in v0.5 for anyone who proposes actionable feedback — &lt;em&gt;which rule is missing for your stack?&lt;/em&gt; DEV.to comments are direct inputs for the next version. &lt;strong&gt;R14 *spike escape hatch&lt;/strong&gt;* covers prototype code meant to disappear within seven days, exempt from R6/R7/R8: adoption does not impose the same friction on a spike as on production code.&lt;/p&gt;

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

&lt;p&gt;An agent that doesn't disagree with you isn't a counterpart — it's a faster typist. These 14 rules restore the disagreement — materially, not mentally. They don't ask the agent to be less sycophantic, nor the solo to be more vigilant; they install the apparatus (invocable skills, blocking hooks, sub-agent challenger) that interrupts the productive course where complaisance compounds. The toolkit is the prosthesis the solo has left when PR review has disappeared and they refuse to code by ear. If a single one of the 14 spares you a fix-rollback cycle next week, it has already paid for itself.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Counterpart Toolkit v0.4.1, fourteen operational rules in ~200 lines, six iterations in 32 days, four external reviews integrated. Tested on 60+ days of solo ERP (118,808 lines, 65+ ADRs). License CC-BY-4.0: github.com/michelfaure/doctrine-counterpart&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>productivity</category>
      <category>doctrine</category>
    </item>
    <item>
      <title>La règle du jour-jeté-à-la-poubelle : lis le code avant de laisser ton IA en écrire</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Sun, 17 May 2026 08:33:16 +0000</pubDate>
      <link>https://forem.com/michelfaure/la-regle-du-jour-jete-a-la-poubelle-lis-le-code-avant-de-laisser-ton-ia-en-ecrire-4col</link>
      <guid>https://forem.com/michelfaure/la-regle-du-jour-jete-a-la-poubelle-lis-le-code-avant-de-laisser-ton-ia-en-ecrire-4col</guid>
      <description>&lt;h2&gt;
  
  
  Six heures du matin, devant la sortie
&lt;/h2&gt;

&lt;p&gt;Vingt-neuf avril, six heures du matin. Le rendu sort à l'écran. &lt;code&gt;A A A A A&lt;/code&gt; sur toute la matrice du document, illisible par construction. Je demande à mon agent pourquoi la sortie est incohérente. Il relit le code, descend dans le dossier voisin, et trouve un composant existant dont je n'avais jamais demandé l'inventaire. Le composant rend proprement le format attendu — signatures, en-tête, légende, bloc d'identification. Ce que mon agent venait de coder la veille était un doublon partiel d'un fichier qu'aucun de nous deux n'avait ouvert.&lt;/p&gt;

&lt;p&gt;Le format que mon agent venait d'inventer la veille existait déjà dans le repo. Mieux fait. Dans un fichier nommable, qu'aucun de nous deux n'avait lu avant d'écrire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pourquoi un agent invente à côté de l'existant
&lt;/h2&gt;

&lt;p&gt;Le fond du problème n'est pas l'agent. C'est moi. Quand je lance un chantier sur un domaine déjà couvert par du code, je décris la cible et je laisse l'agent générer. Lui n'a pas idée du voisinage du fichier qu'il va créer, parce que je ne lui ai pas demandé de le cartographier. Il code une solution plausible à un problème mal cadré, et la plausibilité du résultat masque la duplication tant que personne n'ouvre les autres fichiers du même dossier.&lt;/p&gt;

&lt;p&gt;L'absence de Phase 0 grep n'est pas un défaut de l'agent. C'est un défaut du pilote qui a sauté l'étape la moins coûteuse de toute la chaîne.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 0 — deux minutes, un fichier
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Phase 0 — avant tout nouveau composant dans un domaine existant&lt;/span&gt;
&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"invoices"&lt;/span&gt;   &lt;span class="c"&gt;# le mot-clé du chantier&lt;/span&gt;

find app/api/&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;/ lib/&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.ts"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.tsx"&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# Si un pattern attendu est nommable, vérifier qu'il n'existe pas déjà&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"ExistingPattern&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;RenderPdf&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;exportPdf"&lt;/span&gt; app/ lib/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deux minutes, à tout casser. Le résultat tient sur un écran. Si un composant existant traite le besoin, on le lit avant de proposer quoi que ce soit de neuf. Si rien n'existe, on a la preuve d'avoir cherché.&lt;/p&gt;

&lt;p&gt;Le coût mesuré du shortcut est un jour-dev. Le composant qu'il aurait fallu lire tenait dans un seul fichier au nom évocateur, dans le dossier juste à côté. Deux minutes de &lt;code&gt;find&lt;/code&gt; auraient suffi. Le jour reverted, c'est ce que coûte la confiance dans la plausibilité d'un brouillon que personne n'a relié à son voisinage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Le signal métacognitif
&lt;/h2&gt;

&lt;p&gt;Quand l'agent a relu son propre travail à la lumière du composant existant, il a proposé de reverter, pas de défendre. C'est un bon signal, et un agent qui s'enferme dans son design inventé serait beaucoup plus coûteux qu'un agent qui reconnaît avoir loupé du code. Mais le bon signal vient trop tard. La règle est de ne pas en arriver là.&lt;/p&gt;

&lt;h2&gt;
  
  
  La règle
&lt;/h2&gt;

&lt;p&gt;Avant tout nouveau format, template ou composant dans un domaine déjà couvert, Phase 0 grep, lecture du voisinage, verbalisation de l'existant. Sinon, le jour suivant, tu reverts.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Script Phase 0 grep et checklist en 5 questions, pseudonymisés :&lt;/em&gt;&lt;br&gt;
&lt;a href="https://github.com/michelfaure/rembrandt-samples/tree/main/one-day-thrown-away-rule" rel="noopener noreferrer"&gt;github.com/michelfaure/rembrandt-samples/tree/main/one-day-thrown-away-rule&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agentic</category>
      <category>codequality</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The 1-day-thrown-away rule: read the code before letting your AI write new code</title>
      <dc:creator>Michel Faure </dc:creator>
      <pubDate>Sun, 17 May 2026 08:33:14 +0000</pubDate>
      <link>https://forem.com/michelfaure/the-1-day-thrown-away-rule-read-the-code-before-letting-your-ai-write-new-code-435a</link>
      <guid>https://forem.com/michelfaure/the-1-day-thrown-away-rule-read-the-code-before-letting-your-ai-write-new-code-435a</guid>
      <description>&lt;h2&gt;
  
  
  Six in the morning, looking at the output
&lt;/h2&gt;

&lt;p&gt;April twenty-ninth, six in the morning. The rendering hits the screen. &lt;code&gt;A A A A A&lt;/code&gt; across the entire document matrix, unreadable by construction. I ask my agent why the output is incoherent. It re-reads the code, drops into the neighboring directory, and finds an existing component I had never asked it to inventory. The component renders the expected format cleanly — signatures, header, legend, identification block. What my agent had coded the day before was a partial duplicate of a file neither of us had ever opened.&lt;/p&gt;

&lt;p&gt;The format my agent had invented the night before already existed in the repo. Better done. In a nameable file, that neither of us had read before writing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an agent invents next to the existing code
&lt;/h2&gt;

&lt;p&gt;The root of the problem isn't the agent. It's me. When I open a project on a domain already covered by code, I describe the target and let the agent generate. It has no idea what the neighborhood of the file it's about to create looks like, because I never asked it to map it. It codes a plausible solution to a poorly framed problem, and the plausibility of the result hides the duplication as long as nobody opens the other files in the same directory.&lt;/p&gt;

&lt;p&gt;The absence of a Phase 0 grep is not a flaw of the agent. It's a flaw of the pilot who skipped the least expensive step in the whole chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 0 — two minutes, one file
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Phase 0 — before any new component in an existing domain&lt;/span&gt;
&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"invoices"&lt;/span&gt;   &lt;span class="c"&gt;# the keyword of the project&lt;/span&gt;

find app/api/&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;/ lib/&lt;span class="nv"&gt;$DOMAIN&lt;/span&gt;/ &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="se"&gt;\(&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.ts"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.tsx"&lt;/span&gt; &lt;span class="se"&gt;\)&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# If an expected pattern is nameable, check that it doesn't already exist&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"ExistingPattern&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;RenderPdf&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;exportPdf"&lt;/span&gt; app/ lib/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two minutes, tops. The output fits on a screen. If an existing component handles the need, read it before proposing anything new. If nothing exists, you have proof you looked.&lt;/p&gt;

&lt;p&gt;The measured cost of the shortcut is one dev-day. The component that should have been read fit in a single file with a telling name, in the directory right next door. Two minutes of &lt;code&gt;find&lt;/code&gt; would have sufficed. The reverted day is what blind trust in the plausibility of a draft nobody connected to its neighborhood actually costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metacognitive signal
&lt;/h2&gt;

&lt;p&gt;When the agent re-read its own work in light of the existing component, it proposed to revert, not defend. That's a good signal — an agent that locks itself into its invented design would be far costlier than one that admits it missed existing code. But the good signal comes too late. The rule is to not get there in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Before any new format, template, or component in a domain already covered, Phase 0 grep, neighborhood read, verbalization of the existing. Otherwise, the next day, you revert.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Phase 0 grep checklist and audit script, pseudonymized:&lt;/em&gt;&lt;br&gt;
&lt;a href="https://github.com/michelfaure/rembrandt-samples/tree/main/one-day-thrown-away-rule" rel="noopener noreferrer"&gt;github.com/michelfaure/rembrandt-samples/tree/main/one-day-thrown-away-rule&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agentic</category>
      <category>codequality</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
