Machines d'état sans-IO lisibles
Une bibliothèque sans-IO est une machine d'état dont les entrées/sorties sont externalisées. L'appelant lui fournit des entrées (handle_*, chacune avec now), récupère ses sorties (poll_transmit, poll_event, poll_timeout) et possède les sockets et les temporisateurs. Un tel code ne vaut la peine d'être conservé que si les gens peuvent raisonner dessus : le prochain mainteneur, l'auteur d'un pilote, un relecteur qui le vérifie par rapport à une RFC. Chacun d'eux doit pouvoir répondre rapidement à quatre questions :
- États : dans quels états chaque entité de longue durée (connexion, requête, flux, session) peut-elle se trouver, et quelles données existent dans chacun ?
- Transitions : quelles entrées la font passer d'un état à un autre, et où dans le code chaque transition se produit-elle ?
- Sorties : que met en file d'attente chaque transition (transmissions, événements, écritures), et dans quel ordre ?
- Temps : quand a-t-elle besoin d'être réveillée, et quelle règle fixe chaque échéance ?
Votre travail consiste à rendre ces réponses visibles dans les types et le code, pendant que la machine continue de faire exactement ce qu'elle faisait. Vous éditez l'arbre de travail et terminez par un résumé.
<skill> ci-dessous désigne le répertoire de base de cette compétence, le dossier qui contient ce SKILL.md.
Règles de base
-
Le comportement reste identique. Dans une crate sans-IO, le comportement couvre des éléments qui semblent cosmétiques :
- l'ordre dans lequel les sorties sont mises en file d'attente
- l'ordre d'itération des maps (un
BTreeMapou unVecordonné par insertion décide qui passe en premier) - l'instant renvoyé par
poll_timeout - le fait qu'une écriture vide ou un événement sans opération soit émis ou non
- l'erreur avec laquelle une entité échoue ou se ferme
- la sortie d'un type
#[derive(Debug)]dont vous modifiez les champs, si un instantané, une assertion de journalisation ou un message d'erreur l'affiche
Les tests qui passent par l'API publique, et chaque instantané, doivent rester inchangés. Si l'un d'eux doit être modifié, cette étape a changé le comportement : annulez-la. Les tests unitaires à l'intérieur de la crate qui nomment des éléments privés peuvent nécessiter la même modification mécanique que le code (un drapeau devenu une variante, une aide renommée). Effectuez uniquement cette modification, conservez ce que chaque assertion vérifie, et listez le test dans le résumé.
-
Laissez l'API publique tranquille. Conservez les noms, signatures, variantes et champs des éléments
pubtels quels, car les pilotes et les crates en aval en dépendent (et de la semver). Les élémentspub(crate)et privés sont modifiables. Placez les améliorations de l'API dans le résumé comme suggestions. Comment vérifier cela est expliqué à l'étape 5. -
Ne sacrifiez pas les performances pour la lisibilité. Pas de nouvelles clones, allocations ou passages supplémentaires sur les maps sur les chemins qui s'exécutent par entrée (par paquet, message ou requête). Un
mem::replaceoumem::takeest gratuit ; cloner unVecpour contourner le vérificateur d'emprunt ne l'est pas. -
Travaillez par petites étapes. Chaque étape compile et réussit les vérifications, donc un échec de vérification pointe vers un seul changement et le diff peut être révisé morceau par morceau.
-
Suivez le projet. Avant d'éditer, lisez ses documents d'agent et de contributeur (AGENTS.md, CLAUDE.md, CONTRIBUTING), la configuration du linter et le workflow CI. Adaptez-vous à la densité de commentaires et au style environnants. Là où le projet conserve la documentation en phase avec le code (un tableau de cycle de vie, un commentaire faisant correspondre les lignes aux fonctions, un tableau de couverture de spécification), mettez-les à jour dans la même étape.
Dimensionner le travail au changement
Le filet de sécurité existe pour détecter les changements de comportement que les tests existants manqueraient, alors dimensionnez-le en fonction du changement que vous prévoyez :
- Petit. Un handler ou quelques aides, aucun changement sur les champs qui contiennent l'état. Exécutez les vérifications du projet une fois avant et une fois après, et ignorez tout ce qui est marqué grand ci-dessous. Dans le résumé, cartographiez uniquement l'entité que vous avez touchée.
- Moyen. Plusieurs transitions ou aides de sortie dans un module. Exécutez les vérifications avant chaque étape et à la fin. Construisez un harnais différentiel uniquement si aucun test existant ne fige les sorties exactes des chemins que vous modifiez, et uniquement sur ces chemins. Pas de fuzzing, pas de cas de test de propriété supplémentaires, pas de benchmarks : les propres vérifications du projet à leurs paramètres normaux sont le poids approprié ici.
- Grand. Des drapeaux remplacés par une énumération de phase, plusieurs rédacteurs d'un champ d'état fusionnés en transitions, la plupart des handlers remodelés. Utilisez l'ensemble du workflow.
Décidez de la taille après la carte (étape 2). Si le changement s'agrandit en cours de route, passez à une taille supérieure et dites-le dans le résumé.
Workflow
1. Mettre en place un filet de sécurité
Faites-le avant la première modification, afin que tout échec ultérieur soit le vôtre et non préexistant.
- Notez la base. Lancez
git statuset notez le commit de base : toute comparaison ultérieure se fera par rapport à lui. Si les fichiers que vous prévoyez de modifier ont déjà des modifications non commitées, informez l'utilisateur avant de mélanger vos modifications aux leurs. Si vous continuez par-dessus, utilisezgit stash createcomme base : il les enregistre comme un commit sans toucher à l'arbre. - Corrigez une commande de vérification. Utilisez celle du projet si elle existe (un workflow CI, un justfile, un xtask, les commandes dans ses documents d'agent). Sinon, utilisez la porte intégrée :
<skill>/scripts/check.sh <repo> default all. Listez les ensembles de fonctionnalités utilisés par CI, par exempledefault all none tls. La porte vérifie le formatage sans le modifier, puis exécute clippy et les tests pour chaque ensemble de fonctionnalités, et se termine avec un code non nul si quelque chose échoue. Limitez-la au paquet que vous modifiez et aux membres qui en dépendent avecCHECK_PACKAGES="-p <crate> -p <dependent>", et exécutez l'ensemble du workspace une fois à la fin. Si CI exécute également autre chose (une vérification MSRV, des drapeaux de lint supplémentaires), exécutez-le en parallèle. - Si la référence n'est pas verte, ne la corrigez pas. Enregistrez quelles vérifications échouent et comment, informez l'utilisateur, et attendez-vous ensuite aux mêmes échecs et à aucun nouveau. Formatez uniquement les fichiers que vous modifiez. Un échec provenant de l'environnement plutôt que du code (une configuration cargo globale avec des drapeaux de nightly seulement, une chaîne d'outils ou un outil manquant) va sous Vérification comme "n'a pas pu être exécuté", avec la raison. Si le dépôt n'a pas de CI, prenez le niveau minimum de chaîne d'outils dans
rust-version, l'édition et les fonctionnalités de langage utilisées, et dites ce que vous avez pu et n'avez pas pu exécuter. - Pour les changements importants : construisez un harnais différentiel lorsque les tests existants ne figent pas les sorties exactes des chemins que vous modifierez. Fige signifie des tests de trace ou d'instantané qui affirment la séquence de sortie et les échéances textuellement ; les assertions sur l'état final seul ne comptent pas. Le harnais exécute la base et l'arbre de travail côte à côte sur les mêmes appels aléatoires et compare chaque sortie.
<skill>/scripts/difftest_scaffold.pycrée la paire de crates et un modèle en dehors du dépôt. Remplir le modèle est le vrai travail, alors prévoyez du temps : lisez d'abord references/differential.md. Vérifiez qu'il attrape un mutant d'une ligne planté avant de vous y fier ; un seul suffit. - Gardez le harnais moins cher que le refactoring. Pilotez les chemins que vous restructurez, pas toute la crate. Si couvrir un chemin coûterait plus cher que le changement qui lui est apporté, signalez l'écart sous Vérification et fiez-vous aux tests existants de ce chemin. Exemples : un chemin derrière TLS ou le chiffrement, un non-déterminisme que vous devriez normaliser, un pair que vous devriez simuler.
- Uniquement pour les changements importants : exécutez les vérifications lentes une fois avec plus de cas. Exécutez les tests de propriété, dimensionnés pour se terminer en quelques minutes (dix fois la valeur par défaut, ou
PROPTEST_CASES=10000lorsque les cas sont bon marché), et à nouveau à la fin. Exécutez une cible de fuzzing uniquement si elle atteint le code que vous modifierez, limitée dans le temps à 30–60 s. Exécutez des benchmarks uniquement si un changement ajoutera du travail sur un chemin par entrée ; la restructuration du flux de contrôle n'en a pas besoin. - Remplissez les lacunes de couverture qui devraient survivre au changement. Si un chemin que vous restructurerez n'a pas de couverture et que vous ne construisez pas de harnais, ajoutez d'abord des tests qui figent ses sorties actuelles. Si vous en construisez un, il couvre la lacune pour le refactoring ; ajoutez un test uniquement pour un comportement qu'il révèle et qui mérite d'être conservé (une bizarrerie que vous avez gardée intentionnellement). Placez les nouveaux tests dans un nouveau fichier ou module, en tant qu'étape séparée, afin que les tests existants restent inchangés. L'exception est la ligne
modunique dont une disposition de test monocrate telle quetests/main.rsa besoin.
2. Cartographier la machine
Cartographiez les entités concernées par la demande, et toute entité qu'un changement prévu toucherait. Dans une grande crate où la demande ne précise pas quelle partie est importante, choisissez l'entité nommée par l'utilisateur et dites ce que vous avez laissé de côté. Écrivez la carte avant de changer quoi que ce soit (un fichier brouillon convient). Elle deviendra la carte d'état du résumé, et souvent la documentation du module.
- Entités. Pour chaque entité dans le périmètre, listez ses champs et regroupez-les par la phase dans laquelle ils signifient quelque chose. Les combinaisons de
booletOptionqui encodent une phase entre elles sont la cible principale. - Entrées. Chaque
pub fnqui prend des données externes ounow, et ce que chacune peut déclencher. - Rédacteurs. Pour chaque champ d'état, chaque endroit qui lui assigne une valeur (cherchez
.field =,mem::replace,.take(),+= 1). Un champ avec de nombreux rédacteurs est une transition dispersée dans le fichier. - Sorties. Chaque push sur une file d'attente de sortie, et chaque échéance que
poll_timeoutlit. - Invariants. Ce qui doit toujours être vrai : un ID dans exactement une map, une échéance fixée chaque fois que quelque chose attend, une connexion référencée qui existe.
- Bizarreries. Comportement qui semble accidentel : analyse tolérante, précédence étrange entre deux erreurs, une réponse envoyée en réponse à des déchets, une entrée encore acceptée après une échéance. Vous la conservez (règle 1), mais listez-la pour l'utilisateur.
Ensuite, écrivez la carte sous forme de tableau : état, entrée, état suivant, sorties. Là où vous ne pouvez pas la remplir à partir du code sans tracer à travers plusieurs fonctions, le code n'est pas encore lisible, et c'est là que le travail se fait.
3. Choisir les changements
references/patterns.md est le catalogue, avec des exemples avant/après et les cas où chaque motif ne paie pas. Lisez son index, puis les sections pour ce que votre carte a révélé, y compris leurs paragraphes "Ça ne vaut pas le coup". Lisez tout le fichier pour un changement important. Classez les changements candidats par rapport à combien chacun raccourcit la réponse à l'une des quatre questions, par rapport à la taille du diff. Les mouvements qui paient généralement le plus :
- une énumération dont les variantes portent les données de chaque phase, à la place des drapeaux et options qui encodent la phase (motif 1)
- une fonction nommée par transition, de sorte que chaque champ d'état ait peu de rédacteurs (motif 2)
- des handlers qui correspondent à l'entrée, décident, appliquent la transition et mettent en file d'attente les sorties, le long d'un seul axe de répartition cohérent (motif 3)
- une aide par type de sortie, nommée d'après son intention (motif 4)
- chaque règle d'échéance écrite une fois, avec
poll_timeoutdérivé de l'état (motif 5) - le cycle de vie écrit là où un lecteur atterrit, et les invariants sur les champs (motif 9)
Si l'utilisateur a nommé une fonction ou une partie, ne changez que cela et ce dont elle a besoin. Une aide que vous extrayez peut être réutilisée par ses frères naturels, mais ne retravaillez pas le code en dehors de la demande simplement parce que l'aide y correspond aussi. Listez ces améliorations sous Suggestions à la place : un relecteur qui a demandé à propos d'une fonction ne devrait pas avoir à en vérifier une deuxième.
Laissez de côté les changements qui ne font que déplacer du code, renommer des variables locales pour un goût personnel, et tout ce qui rend le code plus long sans faciliter la réponse à une question. Lorsque l'utilisateur demande le déplacement lui-même (diviser un long fichier en modules), voyez le motif 11. Le code qui se lit déjà bien peut rester tel quel : dites-le dans le résumé plutôt que d'inventer du travail.
4. Appliquer par étapes
Ordonnez les étapes de sorte que chacune soit révisable et qu'aucune n'ajoute du code qu'une étape ultérieure supprime :
- Ajouts purs et extractions : aides de sortie, prédicats nommés, aides d'échéance, étapes marquées par des fonctionnalités déplacées dans des aides, encodage et analyse de filaire déplacés dans des fonctions (motifs 4, 5, 6, 10).
- Le changement central : une énumération de phase, avec les transitions et formes de handler qu'elle impose (motifs 1–3, 7, 12). Cela doit souvent atterrir en une seule étape. Si c'est le cas, dites pourquoi.
- Noms (motif 8).
- Documentation (motif 9).
- Réorganisation des méthodes, en dernier et seul, si jamais (motif 11).
Entre les modifications, utilisez une boucle rapide : clippy et les tests du paquet que vous modifiez, dans l'ensemble de fonctionnalités par défaut, plus le harnais si vous en avez construit un. À la fin de chaque étape, exécutez la commande de vérification de l'étape 1. Ensuite, transformez l'étape en commit si l'utilisateur veut des commits (il a demandé, ou le workflow du projet les attend) ; sinon, laissez le travail non commité et listez les étapes dans le résumé. Ne réduisez pas un lint avec une échafaudage que l'étape suivante supprime (une porte cfg ou un #[allow] ajouté uniquement pour rendre une étape verte). Réordonnez plutôt les étapes.
Abandonnez un motif à tout endroit où le vérificateur d'emprunt le force à des contorsions, car un code contorsionné se lit moins bien que ce dont vous avez commencé. Les signes habituels :
- une méthode qui prend un champ frère de
self(une file d'attente de sortie telle que&mut VecDeque<Transmit>) ou sa propre clé de map comme argument, uniquement pour passer outre un emprunt - une méthode
&selfqui prend une sous-structure uniquement pour pouvoir lire la configuration - des clés clonées ou une recherche supplémentaire
Les correctifs habituels :
- renvoyez la sortie et laissez l'appelant la mettre en file d'attente
- mettez l'aide sur l'entité et passez la valeur
Copyde configuration dont elle a besoin - gardez le code en ligne
5. Vérifier et résumer
Exécutez la commande de vérification une dernière fois, sur l'ensemble du workspace, plus tout ce que vous avez exécuté au début : les vérifications lentes et le harnais.
Ensuite, vérifiez l'API publique, si le diff touche un élément pub, un derive, une implémentation de trait, un type de champ (les traits automatiques tels que Send suivent les champs privés) ou un cfg sur un élément :
- Utilisez
cargo semver-checks --baseline-rev <base>une fois par ensemble de fonctionnalités, avec des drapeaux explicites :--default-features,--all-features,--only-explicit-features(aucun), et--only-explicit-features --features x. - Il ne détecte que les changements cassants, alors confirmez également que rien de nouveau n'est devenu public : utilisez
cargo public-api diffsi installé, ou lisezgit diff <base> | grep -E '^\+.*\bpub\b'. - Si aucun outil n'est installé, demandez avant d'en installer un, et sinon signalez la vérification de l'API comme non exécutée.
- Si le diff ne touche que des corps de fonction et des éléments privés, dites-le explicitement dans le résumé.
Enfin, écrivez un résumé de l'étape 2 au niveau de détails du résumé, des suggestions (s'ils ont été laissés de côté), et un compte rendu de vérification. Le format exact est dans references/summary.md. Terminez l'arbre dans l'état où il compile et passe toutes les vérifications, et si l'utilisateur voulait des commits, avec chaque étape dans un commit séparé.