george-andrikopoulos/domain-rust

Discipline d'ingénierie pour travailler en Rust. Couvre : concevoir d'abord les types ; emprunter dans les signatures ; la visibilité des modules est délibérée ; utiliser #[must_use] sur les retours conséquents ; utiliser libéralement les newtypes ; analyser large puis vérifier les plages ; sceller les ensembles de traits fermés ; builder typestate pour les champs obligatoires ; typestate pour les protocoles ; vérifier que l'abstraction a été compilée ; async tout le long ; les erreurs nomment ce qui a échoué ; justifier chaque clone ; pas de let _ sur une variable locale ; analyser, ne pas valider ; champs privés uniquement ; fixtures multiplateformes ; pas de...

Qu'est-ce que domain-rust ?

Ce skill définit une discipline d'ingénierie rigoureuse pour le développement Rust, applicable dans des assistants de codage IA comme Claude Code, Cursor, Codex, etc. Il fournit un ensemble de règles et de directives qui guident la conception, l'écriture et la révision du code Rust, en mettant l'accent sur la sécurité, la maintenabilité et l'exploitation du système de types. Le skill combine des vérifications automatiques (via des hooks) avec des normes de codage humaines, couvrant des sujets allant de la gestion de l'asynchrone et de la propriété à la conception des erreurs et la visibilité des modules. Chaque règle est conçue pour être exécutable par l'IA lors de la révision de code, garantissant ainsi une application cohérente sur l'ensemble d'une base de code. La force de cette discipline réside dans sa capacité à faire respecter les bonnes pratiques au moment de l'écriture du code, plutôt que de s'appuyer sur des révisions manuelles ultérieures.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/george-andrikopoulos/relearn/tree/HEAD/claude-pack/skills/domain-rust

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

domaine : règles rust

Async, c'est async jusqu'en bas [R:async-all-the-way]

Partiellement appliqué par le hook : no-block-on-in-async ; toute autre façon dont un thread d'exécution est bloqué dans async -- entrées/sorties fichier et réseau synchrones, un verrou std::sync maintenu pendant un await, une longue section CPU jamais confiée à spawn_blocking -- et tout fichier Rust en dehors d'une arborescence src/ est concerné par cette seule instruction.

Pas de block_on dans du code async. Un runtime async multiplexe plusieurs tâches sur peu de threads, donc un appel bloquant ne retarde pas une seule tâche -- il retire un worker du pool pour toute sa durée et retarde chaque tâche qui aurait dû s'y exécuter. Les victimes ne sont pas liées au code qui a bloqué, ce qui explique pourquoi le symptôme est une latence de queue inexpliquée ailleurs, et pourquoi il est quasiment impossible à attribuer après coup.

block_on n'est correct qu'à la frontière où le code synchrone entre dans l'async : main, un test, un callback d'une bibliothèque C. Une fois à l'intérieur, restez à l'intérieur -- E/S async, un verrou compatible async partout où une garde doit survivre à un await, et spawn_blocking pour le travail qui bloque réellement, comme le calcul intensif sur CPU ou un client tiers synchrone.

La même chose s'applique à une garde Mutex de std::sync maintenue pendant un await. Rien ne la signale comme bloquante, mais elle maintient un verrou pendant que la tâche est désynchronisée, donc la tâche en contention bloque son propre thread de travail et l'échec se présente comme un interblocage plutôt qu'un verrou.

Un block_on imbriqué sur un runtime mono-thread ne se dégrade pas -- il provoque un interblocage immédiat. C'est l'échec honnête ; le cas multi-threadé cache simplement la même erreur derrière un nombre de threads, jusqu'à ce que la charge supprime la cachette.

Prenez un emprunt dans la signature sauf si la fonction doit posséder [R:borrow-in-signatures]

Prenez &str plutôt que String, &[T] plutôt que Vec<T>, &Path plutôt que PathBuf. Une signature est une déclaration sur ce dont la fonction a besoin, et un paramètre possédé dit "je vais le garder" -- donc quand le corps ne fait que lire, la signature est mensongère et chaque appelant paie pour cela par une allocation.

Possédez le paramètre exactement là où la fonction le stocke. Là, impl Into<String> est la forme courtoise : un appelant qui a une String la déplace gratuitement, et un appelant qui a un &str alloue une fois, en connaissance de cause. En sortie, renvoyez &str plutôt que &String -- l'indirection supplémentaire n'apporte rien à l'appelant et verrouille la représentation du champ dans l'API publique.

Le coût est rarement l'allocation unique. C'est qu'un paramètre possédé se propage : l'appelant clone pour le satisfaire, son appelant clone pour satisfaire cela, et une signature choisie sans réflexion devient une colonne de clones qui semblent tous localement nécessaires. R:justify-every-clone est là où ces clones apparaissent ; cette règle explique comment ils ne sont jamais créés.

Concevez en écrivant d'abord les types, avant toute logique [R:design-types-first]

S'est répété 2 fois depuis sa rédaction ; la dernière fois le 2026-11-01.

Esquissez les types jusqu'à ce que la conception émerge, puis écrivez la logique. L'esquisse au niveau des types est la spécification exécutable que la phase rouge d'un test-first recherche, et c'est la plus forte : un test échantillonne des points de l'espace de comportement, un type contraint tout l'espace et le compilateur le prouve partout, à la compilation, aussi longtemps que le code existe. Quand les types sont corrects, une grande partie de l'implémentation s'écrit d'elle-même, et de nombreuses implémentations erronées cessent de compiler. En Rust, cet ordre remplace toute valeur par défaut test-first -- les tests ne sont pas supprimés, ils sont rétrogradés à la couche où ils sont le bon outil : les tests de propriété pour les lois comportementales que les types ne peuvent pas encoder, les tests unitaires comme points de repère de régression pour les bugs passés. Appliquez chaque garantie à la couche la plus forte qui peut la contenir, et lors de la révision, demandez d'abord non pas "est-ce que ça passe" mais "laquelle de ces garanties pourrait monter d'une couche ?"

Les enums d'erreur portent les valeurs qui identifient l'échec [R:errors-name-what-failed]

Partiellement appliqué par le hook : no-string-error ; les variantes sans champ, les variantes omettant la valeur d'identification, et un fourre-tout nommé Unknown(String), qu'une ligne ne peut pas distinguer d'une variante portant la valeur non reconnue, sont concernés par cette seule instruction.

Chaque échec distinct est une variante d'un enum thiserror, et chaque variante porte les valeurs qui identifient l'instance :

#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
    #[error("Échec de la connexion TCP à {addr}")]
    Tcp { addr: SocketAddr, #[source] source: io::Error },
    #[error("Délai d'attente dépassé après {elapsed:?}")]
    Timeout { elapsed: Duration },
}

Deux choses en découlent qu'une chaîne ne peut pas donner. L'appelant peut faire un match -- réessayer un Timeout, remonter un Tcp -- plutôt que d'analyser une prose qui change la prochaine fois que quelqu'un édite un message. Et le message nomme l'adresse réelle, donc une seule ligne de log suffit pour agir plutôt que le début d'une enquête.

Gardez la cause dans #[source] au lieu de l'interpoler dans le texte. La chaîne s'imprime alors une fois, à la frontière, sans que chaque couche ne répète la couche en dessous.

Une variante par condition qu'un appelant pourrait raisonnablement traiter différemment. Regrouper quatre causes dans Other(String) recrée l'erreur de chaîne à l'intérieur d'un enum : cela se lit comme un type et se comporte comme de la prose, et c'est la forme que cette règle existe pour attraper.

Chaque clone() porte sa raison, ou la conception est erronée [R:justify-every-clone]

Partiellement appliqué par le hook : no-clone-without-comment ; le commentaire indique-t-il une raison réelle plutôt que de reformuler l'appel, et tout fichier Rust en dehors d'une arborescence src/ est concerné par cette seule instruction.

Une erreur de vérificateur d'emprunt est une question sur la propriété. clone() n'y répond pas ; il paie pour éviter d'y répondre. Essayez d'abord les réponses : restructurez pour qu'un propriétaire soit évident, prenez un emprunt avec une durée de vie nommée, divisez l'emprunt sur des champs plus petits, ou partagez avec Arc/Rc là où la valeur est vraiment partagée plutôt que copiée.

Là où un clone survit à cet examen, écrivez la raison à côté, sous la forme standard utilisée dans toute cette base de code :

let sources: Vec<RuleTag> = rules.iter().map(|r| r.tag().clone()).collect(); // allow:clone: le OutputFile possède sa provenance, survivant à l'emprunt &Library

Le commentaire est ce qui rend la règle révisable. Les clones non annotés sont indiscernables les uns des autres, donc un relecteur doit redériver l'argument de propriété pour chacun et en pratique ne le fait pour aucun. Un clone annoté énonce une affirmation qui peut être vérifiée, et qui peut être trouvée et supprimée le jour où la conception autour change.

La raison doit concerner la propriété ou la durée de vie. "Pour le faire compiler" est la classe d'erreur reformulée, pas une justification. Deux copies d'une valeur que le code croit être une seule valeur est un bogue de correction en attente de la personne qui mute la mauvaise copie.

La plupart des clones qui survivent à la révision ont été créés par une signature, pas par un site d'appel : R:borrow-in-signatures est d'où vient la pression, et corriger le paramètre supprime généralement le clone plutôt que de l'annoter.

Les modules sont la frontière d'encapsulation ; pub est une exportation délibérée [R:module-visibility-is-deliberate]

Utilisez pub(crate) et pub(super) librement ; réservez pub nu pour ce que la crate exporte délibérément. La visibilité n'est pas de la paperasse. C'est la déclaration de ce qui peut encore être modifié librement, et c'est la seule déclaration qu'un compilateur peut vérifier.

La valeur par défaut est importante car pub est bon marché à ajouter et coûteux à supprimer. Une fois qu'un élément est public, un appelant externe peut en dépendre, donc le module ne peut plus être réorganisé, le champ ne peut plus être renommé, et l'assistant écrit pour un seul site d'appel est désormais une API prise en charge. Rien de tout cela n'a été décidé ; c'était par défaut.

Un module contient un invariant de la même manière qu'un type. Un assistant qui ne peut être appelé qu'après une vérification appartient à côté de cette vérification, privé au module, donc "seulement après" est appliqué par le fait que personne d'autre ne peut l'appeler du tout. Le rendre public convertit cette garantie en commentaire de documentation.

Réexportez explicitement la surface prévue à la racine de la crate avec pub use, et laissez tout ce qui se trouve derrière être aussi privé que possible. L'API publique est alors une liste que quelqu'un a écrite, plutôt que le résidu de l'endroit où le code se trouvait.

R:private-fields-only est le même argument un niveau plus bas, au niveau du champ plutôt que de l'élément. Les deux sont séparables -- un champ privé sur un type pub fuit la structure, un type pub(crate) avec des champs publics ne fuit rien -- donc satisfaire l'un ne dit rien sur l'autre.

Marquez les valeurs de retour conséquentes avec #[must_use] [R:must-use-on-consequential-returns]

Mettez #[must_use] sur chaque fonction qui retourne un Result, et sur chaque fonction dont la valeur de retour est le but de l'appel. Le compilateur signale alors un résultat ignoré à chaque site d'appel, présent et futur, au lieu de laisser un relecteur remarquer une seule déclaration nue parmi cent.

Préférez l'attribut sur le type à l'attribut sur la fonction. #[must_use] struct Reçu; voyage vers chaque fonction qui retourne un Reçu, y compris celles écrites plus tard par quelqu'un qui n'a jamais lu cette règle ; l'attribut au niveau de la fonction doit être rappelé à chaque fois. Attachez-le au type chaque fois que le type est toujours conséquent, et revenez à la fonction uniquement pour le cas plus étroit où le même type vaut parfois la peine d'être ignoré.

Élevez l'avertissement résultant en une erreur dans le CI. Un #[must_use] dont la violation imprime une note que personne ne lit est un commentaire avec une syntaxe supplémentaire, et la garantie qu'il prétend n'est tenue nulle part.

Newtype libéralement : les concepts distincts obtiennent des types distincts [R:newtype-liberally]

Donnez à chaque concept du domaine son propre type, même lorsque la représentation sous-jacente est identique. Miles(f64) et Kilometers(f64), Host(String) et Port(u16) -- jamais deux f64 nus ou un String et un u16 dont l'ordre seul un humain se souvient. Les newtypes sont généralement sans coût : ils compilent en le même code machine que le primitif, donc la seule chose qu'ils ajoutent est l'erreur de compilation que vous voulez -- mais là où ce coût est important, R:verify-the-abstraction-compiled-away exige que l'affirmation soit vérifiée plutôt que répétée. La confusion d'unités qui ne peut pas être écrite est moins chère que celle attrapée en révision, et bien moins chère que celle qui ne l'est pas. R:parse-dont-validate est d'où vient le newtype -- la frontière le crée comme un témoin ; cette règle consiste à le porter partout ensuite au lieu de le déballer vers le primitif.

Pas de anyhow dans les types de retour des bibliothèques [R:no-anyhow-in-libraries]

Également appliqué par le hook : no-anyhow-in-lib.

Les crates de bibliothèque retournent des enums d'erreur typés (thiserror), donc un Result dit exactement ce qui peut mal tourner et les appelants peuvent faire un match dessus. anyhow n'appartient qu'à la frontière binaire la plus externe. Cette règle a été promue : le hook no-anyhow-in-lib l'applique désormais de manière déterministe au moment de l'écriture.

N'étouffez pas une variable locale inutilisée avec let _ = ; utilisez-la, signalez-la ou supprimez-la [R:no-let-underscore-on-a-local]

Partiellement appliqué par le hook : no-let-underscore-local ; les liaisons préfixées par un souligné sont concernées par cette seule instruction.

L'avertissement du compilateur pour une liaison inutilisée est un lecteur. Il dit qu'une valeur que vous avez pris la peine de calculer n'atteint rien. let _ = x; existe pour éteindre ce lecteur, et sur une variable locale, il l'éteint presque toujours au moment où c'était juste.

Il prend deux formes, et les deux compilent proprement avec -D warnings tout en se lisant pour un relecteur comme une décision que quelqu'un a prise :

  • Une distinction calculée et abandonnée. Un indicateur est analysé, un mode est décodé, une valeur de configuration est lue -- puis ignorée, donc les cas qu'elle était censée séparer sont traités silencieusement comme un seul.
  • Un échec attrapé et abandonné. if let Err(e) = op() { let _ = e; continue; } ne laisse aucun événement nulle part ; l'échec s'est produit, et rien dans le système ne peut le dire par la suite.

Lorsque l'avertissement se déclenche, faites l'une des trois choses suivantes. Utilisez la valeur -- une distinction inutilisée signifie généralement qu'une branche manque, et l'avertissement la pointe du doigt. Signalez-la ou propagez-la -- une erreur qui est vraiment sûre à ignorer reçoit toujours un événement nommé, ou un commentaire disant pourquoi personne en aval n'a besoin de le savoir. Ou supprimez le code qui l'a calculée. Là où une liaison est morte par conception, comme la dernière affectation à une variable de boucle, réécrivez la boucle pour que l'affectation morte ne se produise jamais plutôt que de faire taire l'avertissement dessus.

let _ = f(); sur un appel est un cas différent : il ignore une valeur de retour, ce que R:must-use-on-consequential-returns régit. Cette règle concerne une variable locale que vous détenez déjà. R:guarantee-needs-a-reader s'appuie sur le compilateur pour attraper les liaisons non lues ; let _ = est l'orthographe qu'il ne peut pas voir.

Vérification en mode échec, chaque fois que vous êtes sur le point d'écrire let _ = nom; : à quoi cette valeur a-t-elle été calculée pour décider, et où cette décision a-t-elle lieu maintenant ? Si la réponse est "nulle part", l'avertissement avait raison.

Pas de unwrap() dans le code de production [R:no-unwrap-in-production]

Également appliqué par le hook : no-unwrap-in-src + no-expect-empty-msg.

Pas de unwrap() dans le code de production. Utilisez expect() uniquement avec un message de panique significatif qui nomme la ressource et l'invariant, ou retournez l'erreur avec ? et laissez l'appelant décider comment la remonter. Cette règle a été promue : le hook no-unwrap-in-src l'applique désormais de manière déterministe au moment de l'écriture, donc la couche d'instruction n'a plus à le faire.

Analysez, ne validez pas [R:parse-dont-validate]

Partiellement appliqué par le hook : no-free-validator ; revérifier les données analysées aux sites d'appel internes est concerné par cette seule instruction.

Transformez l'entrée brute en un type de domaine riche à la frontière la plus externe, produisant un newtype témoin dont l'existence prouve que la vérification a eu lieu. Le code intérieur prend le témoin et ne revérifie jamais -- un périmètre, une vérification, appliquée partout après par le compilateur. R:parse-wide-then-range-check affine ceci : la frontière doit être capable de voir la valeur illégale afin de la nommer.

Analysez large, puis vérifiez la plage [R:parse-wide-then-range-check]

Analysez en un type suffisamment large pour représenter la valeur hors plage, puis vérifiez la plage pour...

[Suite tronquée pour la traduction]

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

george-andrikopoulos/domain-java

Engineering discipline for working in java. Covers: a speculated path deoptimises when the input changes; a view is not a copy; a wrapper type is not free here; close what you open; design for inheritance or forbid it; equality is one contract; exceptions name what failed; identity is not equality for boxes; no reference to internals escapes; null is not a value; publish safely or not at all; seal the alternatives; serializable is a second constructor

george-andrikopoulos/domain-low-latency

Engineering discipline for working in low-latency. Covers: attack the design in a second pass; a measurement matches the regime it reports; a queue without a bound has no overload behaviour; a structure keeps the regime it was proved under; allocated is not resident; answer the requirement at its layer; no allocation on the hot path; no coordinated omission; no false sharing on a hot line; no retry loop on a contended path; no stall inside a publication window; no syscall on a bounded path;...

george-andrikopoulos/global

Engineering discipline that applies to every project and language. Covers: guarantee needs a reader; verdict survives the channel; check the claim you inherit; doc currency; measure the claim not a subset; no silent spend; prefer by construction; repair the lying artefact; signal needs a consequence; verify through production path; detector excludes own definitions; an ambiguous write target is a question; anchor outside the checked partys reach; delegate a fan out; diff against the known...

Skills associés