Recherche/Faire écrire du code système avec soin par l’IA, jusqu’aux types et à la gestion des erreurs

Faire écrire du code système avec soin par l’IA, jusqu’aux types et à la gestion des erreurs

Quand une IA aide sur du code système exigeant. Elle relit son code au moins trois fois plus longtemps que l’écriture, couvre tous les cas d’erreur, met les règles dans les types, garde compatibles anciens et nouveaux formats et fait de petits commits qui compilent.

Pour utiliser ces règles d'IA
## Relire le code écrit par l’IA au moins trois fois plus longtemps que son écriture Traiter l'assistant comme un multiplicateur de qualité, pas comme un multiplicateur de vitesse. Investir le temps qu'il fait gagner dans une rigueur supérieure à celle qu'atteindrait un travail non assisté. - Viser l'excellence : couvrir davantage de cas limites, refactoriser pour la compréhensibilité, solder les TODO en suspens, faire les choses fastidieuses et viser zéro bug. - Relire bien plus longtemps que l'on ne génère. Consacrer à la relecture du code généré au moins trois fois le temps qu'il a fallu pour le produire, en pesant chaque ligne et chaque décision de conception, et en cherchant activement les moyens de le mettre en défaut. - L'auteur est responsable du résultat. Ne pas remettre un premier jet comme un travail fini, sauf si l'on demande explicitement un simple avis sur une orientation. - Un travail qui montre des signes d'avoir été produit sans soin, par exemple des cas manquants qu'une relecture humaine attentive aurait détectés, peut être refusé d'emblée plutôt que révisé. - Énoncer ce niveau d'exigence au début de toute conversation impliquant des modifications de code, puis à nouveau avant d'ouvrir une pull request, afin que l'auteur et le relecteur partagent la même barre. ## Confier la correction aux types et couvrir tous les cas limites Préférer l'implémentation correcte à l'implémentation commode, et pousser autant de correction que possible dans le système de types, afin que les erreurs échouent à la compilation plutôt qu'en production. - Modéliser tout l'espace des erreurs. Aucun raccourci et aucune gestion d'erreurs simplifiée. - Traiter tous les cas limites, y compris les situations de compétition, la temporisation des signaux et les différences de comportement entre systèmes d'exploitation. - Encoder les contraintes de correction dans les types : types enveloppes pour les valeurs du domaine, patrons builder pour les constructions complexes, états typés dans les génériques quand les transitions d'état comptent, et emprunt plutôt que clonage. - Préférer les garanties à la compilation aux vérifications à l'exécution partout où le choix existe. - Garder la visibilité aussi étroite que possible. N'élargir un élément que lorsque quelque chose hors de son module en a réellement besoin. - Marquer les types publics des paquets stables et consommés en externe comme extensibles pour la compatibilité ascendante. Les paquets internes qui ne promettent aucune stabilité n'en ont pas besoin, même si les types d'erreur peuvent tout de même le vouloir. - Utiliser le passage de messages ou le modèle acteur au lieu d'un état mutable partagé, afin qu'aucune situation de compétition sur les données ne puisse survenir. - Bien traiter les détails compte davantage que finir vite. ## Donner des messages d’erreur utiles et le même comportement sur chaque système d’exploitation Le comportement visible par l'utilisateur est une donnée d'entrée de la conception, pas une touche finale. - Produire des diagnostics structurés et utiles plutôt que de simples chaînes d'erreur, et utiliser une bibliothèque de diagnostics riche partout où le message atteint une personne. - Rendre le rapport de progression réactif et informatif. - Garder un comportement cohérent entre les plateformes même là où les capacités sous-jacentes diffèrent, et implémenter chaque plateforme avec ses propres mécanismes natifs au lieu d'émuler une plateforme sur une autre. - Rédiger les messages destinés à l'utilisateur au présent, de façon claire : « l'outil prend désormais en charge », et non « l'outil prenait désormais en charge ». - Centraliser la mise au pluriel dans un module d'aide partagé dont les fonctions renvoient la forme singulière ou plurielle pour un nombre donné. Y ajouter les nouvelles fonctions d'aide plutôt que d'insérer la logique de pluriel directement aux points d'appel. ## Faire évoluer la conception pas à pas et noter pourquoi elle a été choisie Garder une conception spécifique et composable plutôt qu'abstraite et trop générique. - Faire évoluer la conception de façon incrémentale au lieu de viser une architecture parfaite dès le départ. - Consigner les décisions de conception et leurs compromis dans des documents de conception conservés avec le projet, afin que les lecteurs ultérieurs voient pourquoi telle forme a été retenue. - En cas d'incertitude, explorer et itérer plutôt que figer une supposition dans une abstraction. ## Séparer les erreurs de l’utilisateur des bugs internes et joindre un contexte détaillé Les erreurs font partie de la surface que voit l'utilisateur et méritent la même attention de conception que le chemin nominal. - Utiliser des types d'erreur générés par dérivation plutôt que des erreurs sous forme de chaînes, et regrouper les erreurs apparentées sous une énumération de genre dès que l'ensemble grandit. - Porter sur les erreurs un contexte riche et structuré plutôt que d'aplatir le détail en un message unique. - Utiliser un modèle à deux niveaux. Les erreurs causées par l'utilisateur et les erreurs externes forment un type distinct qui se traduit en codes de sortie sémantiques ; les erreurs de programmation internes sont séparées et peuvent provoquer une panique. - Rédiger le texte d'affichage des erreurs comme un fragment de phrase en minuscules qui se lit correctement une fois intégré dans une formule englobante telle que « échec de… ». ## Signaler les clés de configuration inconnues et rendre les erreurs de configuration claires Les décisions de désérialisation déterminent la qualité de chaque erreur de configuration que l'utilisateur verra un jour. - Suivre et signaler les chemins ignorés ou inconnus dans la configuration, afin qu'une faute de frappe apparaisse comme un avertissement au lieu de rester sans effet silencieusement. - Ne jamais aplatir les structures imbriquées pendant la désérialisation. La mise en tampon interne que cela exige dégrade les avertissements sur les champs inconnus ; copier plutôt explicitement les champs dans la structure. - Ne jamais s'appuyer sur la désérialisation de variantes non étiquetées, qui produit de mauvais messages d'erreur. Écrire à la place un visiteur personnalisé avec une chaîne d'attente précise. ## Avant de changer un format enregistré, vérifier que les anciennes et les nouvelles versions peuvent lire les données les unes des autres Toute modification d'une structure écrite sur disque ou envoyée sur le réseau doit être suivie à travers l'ensemble de la matrice de compatibilité avant d'être intégrée. - Ancien lecteur avec de nouvelles données : peut-il encore désérialiser, et perd-il de l'information ? - Nouveau lecteur avec d'anciennes données : les valeurs par défaut déclarées produisent-elles des valeurs correctes, et pas seulement valides ? - Ancien écrivain avec de nouvelles données : peut-il faire l'aller-retour sans perdre de champs ? C'est le cas le plus souvent oublié. - Incrémenter la version du format de façon proactive. Si un nouveau champ va être sémantiquement important, augmenter la version au moment où le champ est ajouté, et non lorsqu'une valeur non par défaut y est écrite pour la première fois ; sinon, les anciennes versions du logiciel corrompent silencieusement les données lorsqu'elles les réécrivent. - Considérer les valeurs par défaut comme nécessaires mais non suffisantes. Elles permettent à un ancien lecteur d'analyser de nouvelles données, mais un ancien écrivain perd tout de même les champs qu'il ne connaît pas. ## N’utiliser async que là où il faut et ne pas lancer de tâches sans limite L'asynchronie est un outil pour les entrées-sorties et pour la concurrence, pas un style par défaut. - Utiliser un runtime async multithread, mais confiner l'async au code qui en a besoin et garder tout le reste synchrone. - Utiliser la concurrence structurée afin que le travail concurrent n'ait pas à recevoir une durée de vie artificiellement longue. - Ordonnancer le travail mis en file avec une conscience de la contre-pression au lieu de lancer des tâches sans limite. - Fournir des primitives de temporisation pouvant être mises en pause là où le contrôle des tâches peut suspendre et reprendre le processus, afin que le temps de suspension ne soit pas imputé aux délais d'expiration. ## Ne pas mettre de logique dans les modules d’agrégation et placer les imports en haut Les frontières entre modules sont imposées par la structure et la visibilité, non par la seule convention. - N'utiliser les fichiers de module agrégateurs que pour réexporter des éléments publics. La logique non triviale appartient à un sous-module d'implémentation ou à un sous-module plus spécifique. - Garder le code spécifique à une plateforme dans des fichiers séparés par plateforme, derrière une compilation conditionnelle, avec une interface partagée entre eux. - Garder les utilitaires de test dans des modules ou des fichiers dédiés. - Importer les types et les fonctions tout en haut du module. Ne jamais importer à l'intérieur du corps d'une fonction ; la seule exception est une fonction compilée conditionnellement qui a besoin d'un import lui-même conditionné. - Préférer importer le type lui-même, et n'importer un module à la place que là où c'est l'écriture conventionnelle. Éviter les chemins pleinement qualifiés dans les signatures : importer le module parent et qualifier uniquement avec celui-ci. - Importer les variantes d'une énumération pour le filtrage par motif est acceptable. ## Emprunter ou partager les données au lieu de les copier Choisir la représentation qui évite le travail plutôt que d'optimiser après coup. - Partager les données immuables par comptage de références ou par emprunt au lieu de les copier. - Utiliser un type optimisé pour les petites chaînes là où le code manipule beaucoup de chaînes courtes. - Prêter attention au clonage et au référencement. Éviter le clonage là où les données ont une structure naturelle d'arbre ou de propriété que l'emprunt exprime déjà. - Faire circuler les données en flux à travers des itérateurs quand c'est possible, au lieu de tout mettre en tampon en mémoire. ## Formater avant chaque commit et gérer les versions des dépendances en un seul endroit La cohérence mécanique est peu coûteuse à imposer et coûteuse à rattraper après coup. - Commencer chaque fichier source par la ligne de copyright du projet et l'identifiant de licence. - Figer l'édition du langage, et exécuter la commande de formatage du projet avant chaque commit. Le formatage est vérifié en intégration continue, donc un commit non formaté y échoue. - Supprimer un lint avec la forme « attendu » plutôt qu'avec la forme « autorisé », afin que la suppression elle-même émette un avertissement dès que le lint ne se déclenche plus et que les suppressions mortes ne s'accumulent pas. - Là où un lint interdit d'ajouter du texte formaté à une chaîne, utiliser la macro d'écriture du projet au lieu d'ajouter une chaîne formatée. - Gérer toutes les versions de dépendances de façon centralisée à la racine de l'espace de travail, et figer les paquets internes à des versions exactes. - Commenter les choix non évidents de dépendances et de fonctionnalités, en particulier là où une fonctionnalité est désactivée pour une raison de sécurité ou de taille. ## Écrire des commentaires qui expliquent pourquoi, pas seulement quoi - Les commentaires expliquent le pourquoi, pas seulement le quoi. - Ne pas raconter le déroulement à l'intérieur du corps des fonctions. N'ajouter un commentaire que là où quelque chose est non évident ou particulier, ou là où l'intention demande une explication plus approfondie. - La documentation au niveau du module énonce l'objet et les responsabilités du module. - Toujours terminer les commentaires de code par un point. - Ne jamais mettre une majuscule à chaque mot dans les en-têtes et les titres ; toujours utiliser la capitalisation de phrase, avec une majuscule au premier mot seulement. - Toujours utiliser la virgule sérielle, dite virgule d'Oxford. - Ne pas omettre les articles. Écrire « le fichier a une version plus récente », et non « fichier a version plus récente ». ## Lancer les tests avec le runner du projet et réutiliser les fixtures existantes Tester de la façon dont le projet exécute ses tests, et réutiliser le modèle de test qui existe déjà. - Utiliser le lanceur de tests désigné par le projet pour les tests unitaires et d'intégration. Ne pas se rabattre sur le lanceur par défaut du langage lorsque la suite dépend du modèle d'exécution du lanceur désigné. - N'utiliser une copie de l'outil compilée localement que pour vérifier des modifications du comportement propre à l'outil, comme une nouvelle option ou un nouveau format de sortie. Exécuter la suite elle-même avec le lanceur standard, puisque les tests d'intégration lancent leurs propres processus internes à partir des artefacts de compilation. - Exécuter les tests de documentation avec la commande qui les prend en charge lorsque le lanceur principal ne le fait pas. - Garder les tests unitaires à côté du code qu'ils testent, les tests d'intégration dans un paquet dédié, les fixtures partagées dans un paquet de fixtures, et les utilitaires de test dans un paquet de support. - Préférer le modèle de résultats attendus du paquet de fixtures aux vérifications ponctuelles écrites à la main. - Recourir à l'outillage qui existe déjà : cas paramétrés, tests par propriétés, assertions par instantané, harnais personnalisés et sorties d'assertion améliorées. Vérifier ce qui est disponible avant d'ajouter un nouveau mécanisme. ## Préfixer le sujet du commit par sa portée et garder chaque commit compilable L'historique est un artefact relisible : il faut donc traiter chaque commit comme une unité de relecture. - Préfixer le sujet de chaque commit par la portée qu'il touche, entre crochets, suivie d'une brève description. Utiliser une portée méta pour le travail transversal, comme les versions minimales de la chaîne d'outils, les publications et les modifications d'intégration continue. - Les commits d'incrément de version et de préparation de publication suivent leurs propres sujets fixes et sont normalement produits par l'outil de publication. - Rédiger les corps de message au passé simple et au présent : ce qui se produisait avant, ce qui se produit maintenant, et quels tests ont été ajoutés. - Les messages de commit sont en Markdown. Ne pas utiliser d'accents graves dans la ligne de sujet, mais en utiliser dans le corps. - Rendre les commits atomiques. Chaque commit est une unité logique de changement. - Garder un historique bissectable. Chaque commit doit compiler et passer toutes les vérifications. - Séparer les préoccupations. Les corrections de formatage et les refactorisations appartiennent à leurs propres commits, à part des changements de comportement. ## Diviser un runner concurrent en répartiteur et exécuteur qui communiquent par messages Un lanceur concurrent est le plus facile à raisonner lorsqu'un composant possède le monde extérieur et qu'un autre possède l'exécution. - Un répartiteur interagit avec le monde extérieur, linéarise les événements de toutes les sources, multiplexe ces sources au moyen d'une construction select, et détient l'état faisant autorité sur ce qui est en cours d'exécution. Des points de synchronisation gardent le rapport d'état exempt de situations de compétition. - Un exécuteur ordonnance et exécute les unités de travail sous forme de machines à états async, en gérant les reprises, les délais d'expiration, le lancement de processus et la capture de la sortie. Chaque unité fait autorité sur son propre état et dispose d'un canal dédié vers le répartiteur plutôt que d'une diffusion générale. - Ne partager aucun état directement ; tout circule entre les composants sous forme de messages. - Modéliser tout l'espace des défaillances, et utiliser des minuteurs pouvant être mis en pause afin que les signaux de suspension et de reprise ne faussent pas la temporisation. - Utiliser les mécanismes natifs de confinement des processus et de gestion des signaux de chaque plateforme, les protéger par compilation conditionnelle et par des modules propres à chaque plateforme derrière une interface partagée, et documenter dans les commentaires les différences entre plateformes et leurs compromis.