---
title: Mon générateur de README n’a pas le droit d’inventer
description: vibetidy écrit les README de dépôts générés par IA, mais le LLM ne fait que mettre en forme des faits vérifiés par un scanner. Pendant que je le construisais, la CI a repéré deux promesses du projet qui ne marchaient pas, et npm a refusé un nom qui semblait libre. Ce que j’en retiens.
date: 2026-09-18
tags: LLM, outils de développement, Node.js, npm, CI
project: https://github.com/brbousnguar/vibetidy
---

Demandez à un LLM d’écrire un README et vous obtenez un texte long, sûr de lui et en partie inventé : des flags qui n’existent pas, des variables d’environnement que personne ne lit, une étape d’installation tirée d’un autre projet.

Alors dans [vibetidy](/fr/projets/vibetidy.html), ce n’est pas le LLM qui décide de ce qui est vrai. Un scanner lit le dépôt, et le modèle se contente de mettre ces faits vérifiés en prose.

Le plus drôle : pendant que je construisais un outil censé ne jamais annoncer ce qui n’existe pas, la CI m’a surpris à faire exactement ça. Deux fois.

## Le scanner, c’est le produit

`vibetidy readme` commence par scanner le dépôt : le manifeste, les scripts, les dépendances, `.env.example`, les points d’entrée, l’arborescence, le README existant. Seuls ces faits partent vers le modèle, et le prompt le dit sans détour : ne jamais inventer une fonctionnalité, un flag, une variable d’environnement, une dépendance ou une version.

Quand les faits sont maigres, vous obtenez un README court et juste plutôt qu’un long README plausible. Et vous pouvez vérifier ce que le modèle verrait avant de dépenser le moindre token :

```bash
npx vibetidy readme --print-context
```

Pas besoin de clé d’API pour ça. La commande affiche les faits scannés et le prompt complet, puis s’arrête.

<figure class="fd-fig vt-scan" aria-labelledby="vt1-cap" data-desc="Comment vibetidy écrit un README. Le scanner lit d’abord le dépôt : le manifeste, les scripts, les dépendances, le fichier .env.example, les points d’entrée, l’arborescence et le README existant. Seuls ces faits vérifiés partent vers le modèle, et le prompt lui interdit d’inventer une fonctionnalité, un flag, une variable d’environnement, une dépendance ou une version. Livré à lui-même, un LLM écrit un README long, sûr de lui et en partie inventé, avec des flags qui n’existent pas, des variables d’environnement que personne ne lit et une étape d’installation tirée d’un autre projet. Avec des faits maigres, vibetidy écrit un README court et juste. Lancer npx vibetidy readme --print-context ne demande aucune clé d’API : la commande affiche les faits scannés et le prompt complet, puis s’arrête.">
<div class="vt-pipe">
<div class="vt-col">
<p class="vt-k">Le dépôt</p>
<ul class="vt-files">
<li>manifeste</li>
<li>scripts</li>
<li>dépendances</li>
<li>.env.example</li>
<li>points d’entrée</li>
<li>arborescence</li>
<li>README existant</li>
</ul>
</div>
<p class="vt-to" aria-hidden="true">→</p>
<div class="vt-col vt-col-facts">
<p class="vt-k">Le scanner</p>
<p class="vt-big">Faits vérifiés</p>
<p class="vt-line">La seule chose que voit le modèle</p>
</div>
<p class="vt-to" aria-hidden="true">→</p>
<div class="vt-col">
<p class="vt-k">Le modèle</p>
<p class="vt-big">Les met en prose</p>
<p class="vt-never-h">Ne jamais inventer</p>
<ul class="vt-never">
<li>une fonctionnalité</li>
<li>un flag</li>
<li>une variable d’env</li>
<li>une dépendance</li>
<li>une version</li>
</ul>
</div>
</div>
<div class="vt-docs">
<div class="vt-doc vt-doc-guess">
<p class="vt-doc-h">README.md<span>Un LLM livré à lui-même</span></p>
<i></i><i></i>
<p class="vt-bad">un flag qui n’existe pas</p>
<i></i>
<p class="vt-bad">une variable d’env que nul ne lit</p>
<i></i><i></i>
<p class="vt-bad">une étape tirée d’un autre projet</p>
<i></i>
<p class="vt-doc-f">Long, sûr de lui, en partie inventé</p>
</div>
<div class="vt-doc vt-doc-ok">
<p class="vt-doc-h">README.md<span>vibetidy, faits maigres</span></p>
<i></i><i></i><i></i>
<p class="vt-doc-f">Court, et juste</p>
</div>
</div>
<p class="mp-strip"><b>Sans clé</b><span><code>--print-context</code> affiche les faits scannés et le prompt complet, puis s’arrête.</span></p>
<figcaption id="vt1-cap">Le modèle n’écrit qu’à partir de ce que le scanner a trouvé. Les deux esquisses de README schématisent leur longueur : ce ne sont pas de vrais fichiers.</figcaption>
</figure>

## Premier bug : un flag documenté, jamais enregistré

`issue-check --help` listait `--no-changelog`. Le tableau des options du README aussi. L’utiliser se soldait par `Unknown option '--no-changelog'`.

Le flag n’avait jamais été enregistré dans le parseur. Le `parseArgs` de Node ne reconnaît un préfixe `--no-` qu’avec `allowNegative`, qui demande un Node plus récent que le `>=20.10.0` annoncé par le paquet. Il est donc devenu un simple `--skip-changelog`.

Le correctif tenait en une ligne. Le plus intéressant, c’est que rien ne l’avait repéré : la doc avait l’air juste, le code paraissait plausible, tous les tests passaient. J’ai donc ajouté un test pour toute cette classe de bugs. Il extrait chaque flag de la sortie `--help` de chaque commande et vérifie que le parseur l’accepte :

```js
for (const flag of flagsIn(help)) {
  const res = runCli([command, flag, 'x', '--help']);
  assert.notEqual(res.status, 2, `${flag} is documented but rejected`);
}
```

Pour m’assurer qu’il mord, j’ai glissé un faux `--phantom-flag` dans le texte d’usage. La suite a échoué avec le message de rejet exact.

<figure class="fd-fig vt-flag" aria-labelledby="vt2-cap" data-desc="Premier bug, un flag documenté mais jamais enregistré. La sortie de issue-check --help listait --no-changelog, tout comme le tableau des options du README, mais le parseur ne l’avait jamais enregistré : l’utiliser se terminait par Unknown option '--no-changelog'. Le parseArgs de Node ne reconnaît un préfixe --no- qu’avec allowNegative, qui demande un Node plus récent que le minimum de 20.10.0 annoncé par le paquet, et le flag est donc devenu un simple --skip-changelog. Le nouveau test extrait chaque flag de la sortie --help de chaque commande et vérifie que le parseur l’accepte. Un faux --phantom-flag glissé dans le texte d’usage a fait échouer la suite avec exactement le message de rejet.">
<ol class="vt-said">
<li><p class="vt-k">issue-check --help</p><p class="vt-fl">--no-changelog</p><p class="vt-said-v">le listait</p></li>
<li><p class="vt-k">Options du README</p><p class="vt-fl">--no-changelog</p><p class="vt-said-v">le listaient</p></li>
<li class="vt-said-bad"><p class="vt-k">Le parseur</p><p class="vt-fl">Unknown option '--no-changelog'</p><p class="vt-said-v">ne l’a jamais enregistré</p></li>
</ol>
<ol class="vt-why">
<li><code>parseArgs</code> ne reconnaît <code>--no-</code> qu’avec <code>allowNegative</code></li>
<li>Il faut pour ça un Node plus récent que <code>&gt;=20.10.0</code></li>
<li>Il est donc devenu <code>--skip-changelog</code></li>
</ol>
<div class="vt-test">
<p class="vt-test-h">Le nouveau test : chaque flag du --help de chaque commande passe par le parseur</p>
<p class="vt-row"><code>--skip-changelog</code><span class="vt-pass">accepté</span></p>
<p class="vt-row vt-row-plant"><code>--phantom-flag</code><span class="vt-fail">is documented but rejected</span></p>
<p class="vt-row-note">Glissé exprès dans le texte d’usage : la suite a échoué.</p>
</div>
<figcaption id="vt2-cap">Deux endroits documentaient le flag, et le parseur n’en avait jamais entendu parler. Le test confronte désormais chaque flag documenté au parseur : toute la classe de bugs d’un coup.</figcaption>
</figure>

## Deuxième bug : les tests n’ont jamais tourné sur Node 20

Ouvrir cette PR a fait passer la CI au rouge sur `main` : 5 jobs sur 9.

`npm test` lançait `node --test "test/**/*.test.js"`. Les globs ne sont arrivés dans `node --test` qu’avec Node 21. Sur Node 20, le motif entre guillemets est pris pour un chemin littéral, donc chaque job 20.x échouait avec `Could not find .../test/**/*.test.js`. La suite n’avait jamais tourné sur la version de Node promise par `engines`.

Le correctif est presque drôle : un simple `node --test`, sans aucun motif. La découverte par défaut fonctionne depuis Node 18. La CI est passée au vert sur les neuf jobs (Ubuntu, macOS et Windows, croisés avec Node 20, 22 et 24).

Même classe que le premier bug : quelque chose d’annoncé qui ne marchait pas. C’est grâce à la matrice de 9 jobs que j’ai trouvé les deux.

<figure class="fd-fig vt-ci" aria-labelledby="vt3-cap" data-desc="Deuxième bug, les tests n’ont jamais tourné sur Node 20. npm test lançait node --test avec le glob entre guillemets test/**/*.test.js. Les globs ne sont arrivés dans node --test qu’avec Node 21 : sur Node 20, le motif est pris pour un chemin littéral, et chaque job 20.x échouait avec Could not find …/test/**/*.test.js. Un simple node --test, sans aucun motif, trouve les tests par la découverte par défaut, qui fonctionne depuis Node 18. Ouvrir la PR du premier bug avait fait passer la CI au rouge sur main, 5 jobs sur 9. Après le correctif, la CI est passée au vert sur les neuf jobs : Ubuntu, macOS et Windows, croisés avec Node 20, 22 et 24.">
<div class="vt-tl-wrap">
<p class="vt-k">Comment chaque version de Node lit la commande de test</p>
<div class="vt-tl">
<p class="vt-tl-v">18</p><p class="vt-tl-v">19</p><p class="vt-tl-v vt-tl-leg vt-tl-red">20</p><p class="vt-tl-v">21</p><p class="vt-tl-v vt-tl-leg">22</p><p class="vt-tl-v">23</p><p class="vt-tl-v vt-tl-leg">24</p>
<p class="vt-tl-bar vt-tl-plain">Un simple <code>node --test</code> trouve les tests</p>
<p class="vt-tl-miss">Glob lu comme un chemin littéral</p>
<p class="vt-tl-bar vt-tl-glob">Le glob marche</p>
</div>
<p class="vt-tl-key"><i></i>un job de CI</p>
</div>
<div class="vt-ba">
<div class="vt-ba-col">
<p class="vt-k">Avant, sur main</p>
<p class="vt-cmd"><code>node --test "test/**/*.test.js"</code></p>
<p class="vt-legs" aria-hidden="true"><i class="bad"></i><i class="bad"></i><i class="bad"></i><i class="bad"></i><i class="bad"></i><i></i><i></i><i></i><i></i></p>
<p class="vt-ba-n"><b>5 sur 9</b> jobs au rouge</p>
<p class="vt-err">Chaque job 20.x : <code>Could not find .../test/**/*.test.js</code></p>
</div>
<p class="vt-to" aria-hidden="true">→</p>
<div class="vt-ba-col vt-ba-after">
<p class="vt-k">Après</p>
<p class="vt-cmd"><code>node --test</code></p>
<div class="vt-grid" role="img" aria-label="Ubuntu, macOS et Windows sur Node 20, 22 et 24 : les neuf jobs au vert">
<span></span><span class="vt-gh">20</span><span class="vt-gh">22</span><span class="vt-gh">24</span>
<span class="vt-gr">Ubuntu</span><i></i><i></i><i></i>
<span class="vt-gr">macOS</span><i></i><i></i><i></i>
<span class="vt-gr">Windows</span><i></i><i></i><i></i>
</div>
<p class="vt-ba-n"><b>9 sur 9</b> jobs au vert</p>
</div>
</div>
<figcaption id="vt3-cap">Pourquoi le glob cassait sur Node 20, et le correctif. Les carrés rouges comptent les jobs en échec sur main ; ils ne les placent pas dans la matrice.</figcaption>
</figure>

## npm a refusé un nom qui semblait libre

L’outil s’appelait d’abord `tidyrepo`. `npm view tidyrepo` renvoyait une 404, donc le nom semblait disponible.

Puis `npm publish` a échoué avec une 403. Le contrôle de similarité de npm retire la ponctuation, donc `tidyrepo` entrait en collision avec un `tidy-repo` existant. Une 404 ne prouve pas qu’un nom est libre : vérifiez aussi les variantes avec tiret et avec underscore.

Il s’appelle `vibetidy` maintenant, et franchement, ça lui va mieux.

<figure class="fd-fig vt-npm" aria-labelledby="vt4-cap" data-desc="Pourquoi npm a refusé un nom qui semblait libre. L’outil s’appelait d’abord tidyrepo. npm view tidyrepo renvoyait une 404, donc le nom semblait disponible. Puis npm publish a échoué avec une 403. Le contrôle de similarité de npm retire la ponctuation, donc tidyrepo entrait en collision avec un paquet existant, tidy-repo. Une 404 ne prouve pas qu’un nom est libre : vérifiez aussi les variantes avec tiret et avec underscore. L’outil s’appelle désormais vibetidy.">
<ol class="vt-steps">
<li><p class="vt-cmd"><code>npm view tidyrepo</code></p><p class="vt-status">404</p><p class="vt-say">Semble libre</p></li>
<li class="vt-steps-bad"><p class="vt-cmd"><code>npm publish</code></p><p class="vt-status">403</p><p class="vt-say">Refusé</p></li>
</ol>
<div class="vt-sim">
<p class="vt-k">Le contrôle de similarité de npm retire la ponctuation</p>
<div class="vt-sim-grid">
<p class="vt-sim-who">Déjà sur npm</p><p class="vt-name">tidy<b class="vt-dash">-</b>repo</p><p class="vt-strip" aria-hidden="true">→</p><p class="vt-name vt-name-hit">tidyrepo</p>
<p class="vt-sim-who">Le mien</p><p class="vt-name">tidyrepo</p><p class="vt-strip" aria-hidden="true">→</p><p class="vt-name vt-name-hit">tidyrepo</p>
</div>
<p class="vt-same">Même nom sans ponctuation</p>
</div>
<div class="vt-checks">
<p class="vt-k">Une 404 ne prouve rien. Vérifiez aussi</p>
<ul class="vt-chips">
<li>tidyrepo</li>
<li>tidy-repo</li>
<li>tidy_repo</li>
</ul>
</div>
<p class="mp-strip"><b>Désormais</b><span>vibetidy</span></p>
<figcaption id="vt4-cap">La vérification du nom, étape par étape. Un nom libre doit le rester une fois sa ponctuation retirée.</figcaption>
</figure>

## Essayez-le

C’est sous licence MIT, sans aucune dépendance à l’exécution, et ça marche avec OpenAI, OpenRouter, Anthropic, Groq, DeepSeek, Together ou un Ollama local :

```bash
npx vibetidy readme --print-context      # see the facts, no key needed
npx vibetidy readme                      # generate, review the diff, confirm
npx vibetidy issue-check --install-hook  # nag on feature commits with no issue
```

<figure class="fd-fig vt-try" aria-labelledby="vt5-cap" data-desc="Comment essayer vibetidy. L’outil est sous licence MIT, n’a aucune dépendance à l’exécution et marche avec OpenAI, OpenRouter, Anthropic, Groq, DeepSeek, Together ou un Ollama local. Il y a trois commandes. npx vibetidy readme --print-context montre les faits et ne demande aucune clé. npx vibetidy readme génère le README, puis vous relisez le diff et confirmez. npx vibetidy issue-check --install-hook ajoute un hook qui vous rappelle à l’ordre sur les commits de fonctionnalité sans issue.">
<ol class="vt-cmds">
<li>
<div class="vt-cmd-t"><p class="vt-n"><b>1</b>Voir les faits</p><p class="vt-cmd"><code>npx vibetidy readme --print-context</code></p></div>
<div class="vt-mini vt-mini-ctx" aria-hidden="true"><span>faits scannés</span><span>prompt complet</span><s>clé d’API</s></div>
</li>
<li>
<div class="vt-cmd-t"><p class="vt-n"><b>2</b>Générer, relire le diff, confirmer</p><p class="vt-cmd"><code>npx vibetidy readme</code></p></div>
<div class="vt-mini vt-mini-diff" aria-hidden="true"><i></i><i></i><i></i><span class="vt-ok-btn">confirmer</span></div>
</li>
<li>
<div class="vt-cmd-t"><p class="vt-n"><b>3</b>Rappel à l’ordre sur les commits de fonctionnalité sans issue</p><p class="vt-cmd"><code>npx vibetidy issue-check --install-hook</code></p></div>
<div class="vt-mini vt-mini-hook" aria-hidden="true"><span>commit de fonctionnalité</span><span class="vt-noissue">pas d’issue</span><span class="vt-nag">rappel</span></div>
</li>
</ol>
<div class="vt-prov">
<p class="vt-k">Marche avec</p>
<ul class="vt-chips">
<li>OpenAI</li>
<li>OpenRouter</li>
<li>Anthropic</li>
<li>Groq</li>
<li>DeepSeek</li>
<li>Together</li>
<li class="vt-chip-local">Ollama, en local</li>
</ul>
</div>
<div class="mp-stats vt-meta">
<p><b>MIT</b><span>licence</span></p>
<p><b>0</b><span>dépendance à l’exécution</span></p>
</div>
<figcaption id="vt5-cap">Les trois commandes de la note, dans l’ordre où vous les lanceriez. Les petits dessins sont schématiques.</figcaption>
</figure>

S’il écrit quelque chose que votre dépôt ne confirme pas, c’est un bug. Ouvrez une issue sur [GitHub](https://github.com/brbousnguar/vibetidy).
