Mnémosyne appelait un modèle mort depuis seize jours.

Le modèle, deepseek-v3.2, avait été retiré par Ollama le 15 juillet. Sans préavis — un jour il répond, le lendemain il renvoie « was retired at… ». Mnémosyne extrait des fiches de lore par IA ; l’extraction ne marchait plus. Personne ne s’en était aperçu, parce que rien ne criait.

Ce qui rend l’histoire intéressante, ce n’est pas la panne. C’est que j’avais déjà documenté cet incident. Le fichier d’infrastructure décrit précisément le retrait du modèle, liste les projets touchés, et conclut : « les trois premiers sont corrigés ». Mnémosyne n’était pas dans la liste. Il avait été oublié à l’écriture, et l’oubli était devenu la vérité officielle.

Le remède ne peut pas être un document

J’ai passé la journée à construire un registre de décisions d’architecture. Des règles qu’on se donne : quels modèles LLM sont autorisés, tout service HTTP expose une sonde de santé, aucun credential dans un dépôt, deux projets ne partagent pas un port.

La tentation, c’est d’écrire ces règles quelque part et de s’y référer. C’est exactement ce qui avait produit le problème.

Alors chaque décision porte un vérificateur exécutable. Pas une description de la règle : un bout de code qui va voir. Pour les modèles, ça veut dire appeler réellement chaque identifiant trouvé dans le code — parce que ollama list continue d’afficher les modèles morts. Ce ne sont que des alias locaux ; le refus vient du cloud, et seul un appel le révèle.

Premier passage, sept secondes. Quatre choses cassées :

Mnémosyne sur son modèle fantôme. Cinq modèles injoignables dans la configuration de LibreChat, dont les deux retirés. Un projet appelant un modèle qui n’a jamais été installé, alors que la doc et la config l’annonçaient tous les deux. Et l’Ollama d’une seconde machine en panne de CUDA — listé dans /api/tags, incapable de charger quoi que ce soit.

Le point commun : aucune de ces pannes n’était détectable en lisant. Toutes l’étaient en essayant.

Les alertes n’étaient jamais parties

En redémarrant le service, une ligne dans le journal :

Ntfy alert failed: 'latin-1' codec can't encode characters in position 0-1

Les en-têtes HTTP sont en latin-1. Le titre de mes alertes commençait par un emoji — « ⚠️ Changement santé projet ». Toutes les notifications échouaient, depuis toujours, et l’échec était avalé par un except qui imprimait dans un journal que personne ne lit.

Un système d’alerte qui n’alerte pas est pire que pas d’alerte : il produit la sensation d’être couvert.

Le même schéma est réapparu deux fois dans la journée. Une unité systemd qui régénère le glossaire du monde était morte depuis vingt-trois heures — elle surveillait le journal git d’un dépôt dans lequel elle écrit elle-même, se redéclenchait en boucle, et systemd l’avait coupée après cinq démarrages en dix secondes. Et une sauvegarde vers le NAS sortait en erreur toutes les nuits depuis deux jours, alors qu’elle transférait ses 18 gigaoctets sans problème : trois fichiers appartenant à root la faisaient finir en « transfert partiel ».

Cette dernière est la plus vicieuse. Le job échouait pour une raison bénigne, chaque nuit. Le jour où le NAS sera vraiment injoignable, l’alerte ressemblera aux précédentes.

Le moment où j’ai eu tort

J’ai voulu que chaque projet raconte ce qu’il est et ce qui lui manque. Vingt-sept projets, un modèle de langue, une question : que pourrais-tu ajouter pour t’améliorer ?

Le résultat avait l’air riche. Cinquante-trois propositions. En les lisant, j’ai trouvé que BANG ! — mon séquenceur MIDI — réclamait un « mode Song » pour enchaîner ses motifs.

Le mode Song existait depuis six semaines. Le fichier qui l’implémente fait 400 lignes.

Le modèle n’avait pas halluciné. Il avait lu la feuille de route du projet, où la case n’était pas cochée, et en avait conclu que le travail restait à faire. La feuille de route datait du 25 mai. Cent quarante-cinq commits avaient été faits depuis sans y toucher.

Quatre tâches y étaient marquées « à faire » alors qu’elles étaient livrées. Je l’ai vérifié en ouvrant les fichiers, pas en lisant les messages de commit.

Un document qui ne suit pas le code ment deux fois : il fait croire qu’il reste à faire ce qui est fait, et il perd ce qui a été appris en chemin.

Réécrire quatorze feuilles de route

Quatorze projets sur vingt-sept n’avaient qu’un gabarit : « Définir les prochaines étapes ». Je les ai reprises une par une, en m’interdisant une chose — ne rien écrire qui ne soit adossé à un fait constatable.

La méthode s’est révélée d’elle-même. Chaque trouvaille est venue de la confrontation de deux sources qui devraient s’accorder et ne s’accordaient pas.

Un service qui se présente comme « lecture seule » et qui porte trois routes d’écriture. Une chronologie censée lire les relations datées d’un graphe, dont la requête SQL ne sélectionne jamais la colonne de date : sept cent soixante-seize relations disponibles, deux événements affichés, tous deux des tests de juin. Un générateur de portraits qui en a produit quarante et un pour soixante-neuf personnages — invisible, parce que ses fichiers sont nommés d’après le nom d’affichage quand le reste du monde utilise des identifiants. Une page d’accueil dont cinq liens sur dix-sept ne mènent nulle part.

Et une note d’infrastructure qui affirmait qu’une démo publique avait été retirée, avec la consigne « ne plus recréer sans demande explicite ». La démo était en ligne : elle avait été restaurée dix jours plus tard, à ma demande, après identification du vrai coupable de la lenteur. Une session appliquant docilement la consigne aurait supprimé quelque chose que je voulais.

C’est la première fois que je voyais une note périmée ne pas se contenter d’être fausse : elle donnait un ordre.

L’outil qui truque son propre thermomètre

J’avais ajouté une règle : pas plus de quinze commits sans toucher à la feuille de route. Premier passage, dix projets hors cadre.

Puis j’ai commité les vingt-deux feuilles de route modifiées, et le compteur est passé à un seul écart.

Sauf que rien ne s’était amélioré. Le compteur mesure les commits depuis la dernière modification du fichier — et je venais de modifier les vingt-deux fichiers pour y poser un bloc automatique. L’outil avait fait passer sa propre métrique au vert en écrivant dedans.

C’est exactement le genre d’indicateur qui rassure à tort. Je l’ai noté comme défaut à corriger plutôt que de m’en féliciter : le vérificateur devrait ignorer les commits qui ne touchent que son propre bloc.

Un test rouge depuis trois mois

Le meilleur signal de la journée n’a rien coûté. Un projet avait un test en échec depuis le 3 mai — découvert le 1ᵉʳ août. Le fichier d’exemple avait été rethématisé dans le même commit que le test qui l’attendait sous son ancien nom. Personne ne relance les tests d’un projet en sommeil, et c’est justement là que la régression s’installe.

J’ai donc écrit un vérificateur qui lance les suites de tous les projets. Et j’ai fabriqué trois faux positifs en le construisant.

D’abord j’ai fait confiance à l’environnement virtuel de chaque projet — l’un d’eux n’a pas pytest, et « module introuvable » se lisait comme une suite en panne. Puis j’ai traité toute erreur d’import comme un problème d’environnement, ce qui masquait le vrai échec du premier projet, dont la sortie contient les deux. Puis j’ai découvert l’inverse : ailleurs, l’unique ligne « FAILED » était une erreur d’import déguisée.

Un vérificateur qui crie au loup une fois sur trois est pire qu’aucun vérificateur. On apprend à l’ignorer — précisément comme la sauvegarde qui échouait chaque nuit.

La sortie honnête n’était pas de deviner mieux. C’était d’arrêter de deviner : chaque projet déclare désormais sa commande de test dans un fichier d’une ligne. Le projet déclare, l’outil vérifie.

Ce que je retiens

Une règle qu’on ne peut pas mesurer n’est pas une règle, c’est une note. Elle sera oubliée, puis contredite, puis retrouvée un an plus tard dans un fichier que personne ne relit.

Et le corollaire, plus désagréable : un document qui se déclare à jour est plus dangereux qu’un document visiblement périmé. On ne relit pas ce qu’on croit juste. J’en ai trouvé un aujourd’hui qui affirmait avoir corrigé un autre fichier — la correction n’avait jamais eu lieu.

Rien de tout ça n’a été trouvé en réfléchissant. Tout a été trouvé en confrontant deux sources qui auraient dû s’accorder.