CommunityCodierung & Entwicklunggithub.com

george-andrikopoulos/domain-rust

Entwicklungsdisziplin für die Arbeit mit Rust. Deckt ab: Typen zuerst entwerfen; Ausleihen in Signaturen; Sichtbarkeit von Modulen ist bewusst; #[must_use] für folgenreiche Rückgaben; Newtypes großzügig verwenden; breit parsen, dann Bereich prüfen; geschlossene Traitsets versiegeln; Typzustands-Builder für Pflichtfelder; Typzustand für Protokolle; überprüfen, ob die Abstraktion wegkompiliert wurde; durchgängig asynchron; Fehler benennen, was fehlgeschlagen ist; jeden Clone rechtfertigen; kein let _ bei lokalen Variablen; parsen, nicht validieren; nur private Felder; plattformübergreifende Fixtures; kein...

Was ist domain-rust?

Diese Skill-Definition beschreibt eine umfassende Engineering-Disziplin für die Arbeit mit Rust, die als Entwicklungsleitfaden für KI-Agenten wie Claude Code, Cursor oder Codex dient. Sie umfasst eine Reihe strenger Regeln und Best Practices, die von Typsicherheit über Nebenläufigkeit bis hin zu Fehlerbehandlung reichen. Die Disziplin betont das Entwerfen durch Typen vor der Logik, die durchgängige Verwendung von async/await ohne block_on innerhalb async Code, das bewusste Übernehmen von Ausleihen in Signaturen, die großzügige Verwendung von Newtypes für Domänenkonzepte und das Parsen statt Validieren an Systemgrenzen. Fehler werden als typisierte Enums mit identifizierenden Werten modelliert, jeder Clone muss dokumentiert und gerechtfertigt werden, und die Sichtbarkeit von Modulen wird als wichtige Architekturentscheidung behandelt. Die Regeln sind teilweise durch Hooks automatisiert, was eine konsistente Durchsetzung in der gesamten Codebasis ermöglicht. Diese Disziplin ist plattformunabhängig und für jedes Rust-Projekt anwendbar, das hohe Qualitätsstandards anstrebt.

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

Installed? Explore more Codierung & Entwicklung skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

domain: Rust-Regeln

Async ist durchgängig async [R:async-all-the-way]

Teilweise erzwungen durch Hook:no-block-on-in-async; jeder andere Weg, bei dem ein Runtime-Thread innerhalb von Async blockiert wird – synchrone Datei- und Netzwerk-I/O, eine std::sync-Sperre, die über ein await hinweg gehalten wird, eine lange CPU-Sektion, die nie an spawn_blocking übergeben wurde – und jede Rust-Datei außerhalb eines src/-Baums wird allein durch diese Anweisung gehalten.

Kein block_on innerhalb von Async-Code. Eine Async-Runtime multiplexiert viele Aufgaben auf wenige Threads, daher verzögert ein blockierender Aufruf nicht nur eine Aufgabe – er entfernt einen Worker für die Dauer der Blockade aus dem Pool und verzögert jede Aufgabe, die dort gelaufen wäre. Die Opfer stehen in keinem Zusammenhang mit dem Code, der blockiert hat, weshalb das Symptom eine unerklärliche Tail-Latenz an ganz anderer Stelle ist und warum es im Nachhinein kaum zuzuordnen ist.

block_on ist nur an der Grenze korrekt, an der synchroner Code in Async eintritt: main, ein Test, ein Callback aus einer C-Bibliothek. Einmal drinnen, bleib drinnen – Async-I/O, eine async-fähige Sperre, wenn ein Guard ein await überleben muss, und spawn_blocking für Arbeit, die tatsächlich blockiert, wie CPU-intensive Berechnungen oder ein synchroner Drittanbieter-Client.

Das Gleiche gilt für einen std::sync::Mutex-Guard, der über ein await hinweg gehalten wird. Nichts kennzeichnet ihn als blockierend, aber er hält eine Sperre, während die Aufgabe descheduled ist, sodass die konkurrierende Aufgabe ihren eigenen Worker-Thread blockiert und der Fehler sich als Deadlock und nicht als Sperre präsentiert.

Verschachteltes block_on auf einer Current-Thread-Runtime degradiert nicht – es führt sofort zu einem Deadlock. Das ist der ehrliche Fehler; der Multithread-Fall verbirgt denselben Fehler lediglich hinter einer Thread-Anzahl, bis die Last das Versteck wegnimmt.

Nimm eine Ausleihe in der Signatur, es sei denn, die Funktion muss besitzen [R:borrow-in-signatures]

Nimm &str anstelle von String, &[T] anstelle von Vec<T>, &Path anstelle von PathBuf. Eine Signatur ist eine Aussage darüber, was die Funktion benötigt, und ein eigener Parameter sagt: "Ich werde dies behalten" – wenn der Körper also nur liest, ist die Signatur nicht wahr, und jeder Aufrufer zahlt dafür mit einer Allokation.

Besitze den Parameter genau dort, wo die Funktion ihn speichert. Dort ist impl Into<String> die höfliche Form: Ein Aufrufer, der einen String hält, verschiebt ihn kostenlos hinein, und ein Aufrufer, der ein &str hält, allokiert einmal, wissentlich. Auf dem Rückweg gib &str statt &String zurück – die zusätzliche Indirektion bringt dem Aufrufer nichts und nagelt die Darstellung des Feldes in der öffentlichen API fest.

Die Kosten sind selten die einzelne Allokation. Es ist, dass ein eigener Parameter sich fortpflanzt: Der Aufrufer klont, um ihn zu erfüllen, dessen Aufrufer klont, um das zu erfüllen, und eine gedankenlos gewählte Signatur wird zu einer Säule von Klonen, die jeweils lokal notwendig erscheinen. R:justify-every-clone ist, wo diese Klone auftauchen; diese Regel ist, wie sie nie erzeugt werden.

Entwerfe, indem du zuerst die Typen schreibst, vor jeder Logik [R:design-types-first]

Ist seit seiner Niederschrift 2 Mal wiederholt worden; zuletzt am 2026-11-01.

Skizziere die Typen, bis sich das Design ergibt, dann schreibe die Logik. Die Typskizze auf Typebene ist die ausführbare Spezifikation, die eine Test-First-Rotphase anstrebt, und sie ist die stärkere: Ein Test sampelt Punkte des Verhaltensraums, ein Typ beschränkt den gesamten Raum und der Compiler beweist es überall, zur Compile-Zeit, solange der Code existiert. Wenn die Typen richtig sind, schreibt sich ein Großteil der Implementierung von selbst, und viele falsche Implementierungen hören auf zu kompilieren. In Rust ersetzt diese Reihenfolge jede Test-First-Standardvorgabe – Tests werden nicht entfernt, sondern auf die Ebene zurückgestuft, auf der sie das richtige Werkzeug sind: Eigenschaftstests für Verhaltensgesetze, die die Typen nicht kodieren können, Unit-Tests als Regression-Pins für vergangene Fehler. Wende jede Garantie auf der stärksten Ebene an, die sie halten kann, und frage beim Review zuerst nicht "besteht es?", sondern "welche dieser Garantien könnte eine Ebene nach oben verschoben werden?"

Fehler-Enums tragen die Werte, die den Fehler identifizieren [R:errors-name-what-failed]

Teilweise erzwungen durch Hook:no-string-error; felderlose Varianten, Varianten, die den identifizierenden Wert auslassen, und ein Sammelbecken namens Unknown(String), das eine Zeile nicht von einer Variante unterscheiden kann, die den unerkannten Wert trägt, wird allein durch diese Anweisung gehalten.

Jeder eindeutige Fehler ist eine Variante eines thiserror-Enums, und jede Variante trägt die Werte, die die Instanz identifizieren:

#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
    #[error("TCP-Verbindung zu {addr} fehlgeschlagen")]
    Tcp { addr: SocketAddr, #[source] source: io::Error },
    #[error("Zeitüberschreitung nach {elapsed:?}")]
    Timeout { elapsed: Duration },
}

Daraus ergeben sich zwei Dinge, die ein String nicht bieten kann. Der Aufrufer kann match – einen Timeout wiederholen, ein Tcp nach oben reichen – anstatt Prosa zu parsen, die sich ändert, wenn jemand das nächste Mal eine Nachricht bearbeitet. Und die Nachricht nennt die tatsächliche Adresse, sodass eine einzelne Logzeile ausreicht, um zu handeln, anstatt den Beginn einer Untersuchung darzustellen.

Behalte die Ursache in #[source], anstatt sie in den Text zu interpolieren. Die Kette wird dann einmal am Rand ausgegeben, ohne dass jede Schicht die darunterliegende Schicht wiederholt.

Eine Variante pro Bedingung, die ein Aufrufer plausibel unterschiedlich behandeln könnte. Vier Ursachen in Other(String) zusammenzufassen, erzeugt den String-Fehler innerhalb eines Enums neu: Es liest sich wie ein Typ und verhält sich wie Prosa, und es ist die Form, die diese Regel zu fangen bestimmt ist.

Jeder clone() trägt seinen Grund, sonst ist das Design falsch [R:justify-every-clone]

Teilweise erzwungen durch Hook:no-clone-without-comment; ob der Kommentar einen echten Grund angibt, anstatt den Aufruf zu wiederholen, und jede Rust-Datei außerhalb eines src/-Baums wird allein durch diese Anweisung gehalten.

Ein Borrow-Checker-Fehler ist eine Frage nach dem Besitz. clone() beantwortet sie nicht; es bezahlt, um sie zu vermeiden. Probiere zuerst die Antworten aus: Restrukturiere so, dass ein Besitzer offensichtlich ist, nimm eine Ausleihe mit einem benannten Lifetime, teile die Ausleihe über kleinere Felder auf oder teile mit Arc/Rc, wo der Wert wirklich gemeinsam genutzt und nicht kopiert wird.

Wo ein Clone diese Prüfung überlebt, schreibe den Grund daneben, in der im gesamten Codebase verwendeten Hausform:

let sources: Vec<RuleTag> = rules.iter().map(|r| r.tag().clone()).collect(); // allow:clone: die OutputFile besitzt ihre Herkunft, überlebt die &Library-Ausleihe

Der Kommentar macht die Regel überprüfbar. Nicht annotierte Klone sind voneinander nicht zu unterscheiden, daher muss ein Reviewer das Besitzargument für jeden einzeln neu ableiten und tut es in der Praxis für keinen. Ein annotierter Clone stellt eine Behauptung auf, die überprüft werden kann und die gefunden und entfernt werden kann, sobald sich das Design darum herum ändert.

Der Grund muss etwas mit Besitz oder Lifetime zu tun haben. "Damit es kompiliert" ist die Fehlerklasse, die wiederholt wird, keine Rechtfertigung. Zwei Kopien eines Werts, von dem der Code glaubt, es sei ein Wert, ist ein Korrektheitsfehler, der darauf wartet, dass jemand die falsche mutiert.

Die meisten Klone, die eine Überprüfung überleben, wurden durch eine Signatur erzeugt, nicht durch eine Aufrufstelle: R:borrow-in-signatures ist, wo der Druck herkommt, und das Beheben des Parameters löscht normalerweise den Clone, anstatt ihn zu annotieren.

Module sind die Kapselungsgrenze; pub ist ein bewusster Export [R:module-visibility-is-deliberate]

Verwende pub(crate) und pub(super) frei; reserviere das bloße pub für das, was die Crate bewusst exportiert. Sichtbarkeit ist kein Papierkram. Sie ist die Aussage darüber, was noch frei geändert werden kann, und sie ist die einzige solche Aussage, die ein Compiler überprüfen kann.

Die Voreinstellung ist wichtig, weil pub billig hinzuzufügen und teuer zu entfernen ist. Sobald ein Element öffentlich ist, kann ein externer Aufrufer davon abhängen, sodass das Modul nicht mehr neu organisiert werden kann, das Feld nicht mehr umbenannt werden kann und der Helfer, der für eine einzige Aufrufstelle geschrieben wurde, jetzt eine unterstützte API ist. Nichts davon wurde entschieden; es wurde als Standard gesetzt.

Ein Modul hält eine Invariante auf die gleiche Weise wie ein Typ. Ein Helfer, der nur nach einer Prüfung aufgerufen werden darf, gehört neben diese Prüfung, privat zum Modul, sodass "nur nach" dadurch erzwungen wird, dass niemand anders ihn überhaupt aufrufen kann. Ihn öffentlich zu machen, wandelt diese Garantie in einen Doc-Kommentar um.

Exportiere die beabsichtigte Oberfläche explizit an der Crate-Wurzel mit pub use neu, und lass alles dahinter so privat sein, wie es nur sein kann. Die öffentliche API ist dann eine Liste, die jemand geschrieben hat, und nicht der Rückstand davon, wo der Code zufällig gelandet ist.

R:private-fields-only ist dasselbe Argument eine Ebene tiefer, auf Feldebene statt auf Elementebene. Die beiden sind trennbar – ein privates Feld auf einem pub-Typ gibt Struktur preis, ein pub(crate)-Typ mit öffentlichen Feldern gibt keine preis – daher sagt das Erfüllen des einen nichts über das andere aus.

Markiere folgenreiche Rückgabewerte mit #[must_use] [R:must-use-on-consequential-returns]

Setze #[must_use] auf jede Funktion, die ein Result zurückgibt, und auf jede Funktion, deren Rückgabewert der Grund für den Aufruf ist. Der Compiler kennzeichnet dann ein verworfenes Ergebnis an jeder Aufrufstelle, gegenwärtig und zukünftig, anstatt es einem Reviewer zu überlassen, eine einzelne nackte Anweisung unter hundert zu bemerken.

Bevorzuge das Attribut auf dem Typ gegenüber dem Attribut auf der Funktion. #[must_use] struct Receipt; wandert zu jeder Funktion, die ein Receipt zurückgibt, einschließlich derer, die später von jemandem geschrieben werden, der diese Regel nie gelesen hat; das Attribut auf Funktionsebene muss jedes Mal bedacht werden. Füge es dem Typ hinzu, wann immer der Typ immer folgenreich ist, und greife nur für den engeren Fall auf die Funktion zurück, in dem derselbe Typ manchmal wegwerfbar ist.

Stufe die resultierende Warnung in CI zu einem Fehler hoch. Ein #[must_use], dessen Verletzung eine Notiz ausgibt, die niemand liest, ist ein Kommentar mit zusätzlicher Syntax, und die beanspruchte Garantie wird nirgendwo gehalten.

Newtype großzügig: Unterschiedliche Konzepte erhalten unterschiedliche Typen [R:newtype-liberally]

Gib jedem Domänenkonzept seinen eigenen Typ, selbst wenn die zugrunde liegende Darstellung identisch ist. Miles(f64) und Kilometers(f64), Host(String) und Port(u16) – niemals zwei bloße f64s oder ein String und ein u16, deren Reihenfolge sich nur ein Mensch merkt. Newtypes sind normalerweise kostenlos: Sie kompilieren zum gleichen Maschinencode wie der primitive Typ, also ist das Einzige, was sie hinzufügen, der Compiler-Fehler, den du haben willst – aber wo diese Kosten tragend sind, erfordert R:verify-the-abstraction-compiled-away, dass die Behauptung überprüft und nicht wiederholt wird. Die Einheitenverwechslung, die nicht geschrieben werden kann, ist billiger als die, die in der Überprüfung erwischt wird, und weitaus billiger als die, die nicht erwischt wird. R:parse-dont-validate ist, wo der Newtype herkommt – die Grenze prägt ihn als Zeugen; diese Regel handelt davon, ihn überall danach mitzunehmen, anstatt zurück zum primitiven Typ zu entpacken.

Kein anyhow in Bibliotheks-Rückgabetypen [R:no-anyhow-in-libraries]

Auch erzwungen durch Hook:no-anyhow-in-lib.

Bibliotheks-Crates geben typisierte Fehler-Enums (thiserror) zurück, sodass ein Result genau sagt, was schiefgehen kann, und Aufrufer darauf match können. anyhow gehört nur an die äußerste Binär-Grenze. Diese Regel ist abgeschlossen: Der Hook no-anyhow-in-lib erzwingt sie jetzt deterministisch zur Schreibzeit.

Unterdrücke keine ungenutzte lokale Variable mit let _ =; verwende sie, melde sie oder lösche sie [R:no-let-underscore-on-a-local]

Teilweise erzwungen durch Hook:no-let-underscore-local; Bindungen mit Unterstrich-Präfix werden allein durch diese Anweisung gehalten.

Die Compiler-Warnung für ungenutzte Bindungen ist ein Leser. Sie sagt, dass ein Wert, den du dir die Mühe gemacht hast zu berechnen, bei nichts ankommt. let _ = x; existiert, um diesen Leser auszuschalten, und bei einer lokalen Variable schaltet es ihn fast immer in dem Moment aus, in dem es richtig war.

Es hat zwei Formen, und beide kompilieren sauber unter -D warnings, während sie für einen Reviewer wie eine getroffene Entscheidung aussehen:

  • Eine Unterscheidung berechnet und verworfen. Ein Flag wird geparst, ein Modus wird decodiert, ein Konfigurationswert wird gelesen – und dann verworfen, sodass die Fälle, die es trennen sollte, stillschweigend als einer behandelt werden.
  • Ein Fehler gefangen und verworfen. if let Err(e) = op() { let _ = e; continue; } hinterlässt kein Ereignis irgendwo; der Fehler ist passiert, und nichts im System kann später davon sagen.

Wenn die Warnung ausgelöst wird, tue eines von drei Dingen. Verwende den Wert – eine ungenutzte Unterscheidung bedeutet normalerweise, dass ein Zweig fehlt, und die Warnung zeigt darauf. Melde oder gib ihn weiter – ein Fehler, der wirklich sicher zu ignorieren ist, erhält trotzdem ein benanntes Ereignis oder einen Kommentar, der sagt, warum niemand downstream davon wissen muss. Oder lösche den Code, der ihn berechnet hat. Wo eine Bindung tot von Design ist, wie die letzte Zuweisung an eine Schleifenvariable, schreibe die Schleife um, sodass die tote Zuweisung nie passiert, anstatt die Warnung darüber stumm zu schalten.

let _ = f(); bei einem Aufruf ist ein anderer Fall: Es verwirft einen Rückgabewert, was R:must-use-on-consequential-returns regelt. Diese Regel handelt von einer lokalen Variable, die du bereits hältst. R:guarantee-needs-a-reader verlässt sich darauf, dass der Compiler ungelesene Bindungen erwischt; let _ = ist die eine Schreibweise, die er nicht sehen kann.

Fehlerfall-Prüfung, immer wenn du kurz davor bist, let _ = name; zu schreiben: Wozu wurde dieser Wert berechnet, um eine Entscheidung zu treffen, und wo passiert diese Entscheidung jetzt? Wenn die Antwort "nirgendwo" ist, hatte die Warnung recht.

Kein unwrap() im Produktionscode [R:no-unwrap-in-production]

Auch erzwungen durch Hook:no-unwrap-in-src + Hook:no-expect-empty-msg.

Kein unwrap() im Produktionscode. Verwende expect() nur mit einer aussagekräftigen Panik-Nachricht, die die Ressource und die Invariante nennt, oder gib den Fehler mit ? zurück und lass den Aufrufer entscheiden, wie er ihn anzeigt. Diese Regel ist abgeschlossen: Der Hook no-unwrap-in-src erzwingt sie jetzt deterministisch zur Schreibzeit, sodass die Anweisungsebene dies nicht mehr tun muss.

Parse, nicht validiere [R:parse-dont-validate]

Teilweise erzwungen durch Hook:no-free-validator; das erneute Überprüfen geparster Daten an internen Aufrufstellen wird allein durch diese Anweisung gehalten.

Wandle rohe Eingaben an der äußersten Grenze in einen reichen Domänentyp um und erzeuge einen Zeugen-Newtype, dessen Existenz beweist, dass die Prüfung stattgefunden hat. Innerer Code nimmt den Zeugen und überprüft nie erneut – ein Perimeter, eine Prüfung, danach überall durch den Compiler erzwungen. R:parse-wide-then-range-check schärft dies: Die Grenze muss in der Lage sein, den ungültigen Wert zu sehen, um ihn zu benennen.

Parse breit, dann Bereich prüfen [R:parse-wide-then-range-check]

Parse in einen Typ, der breit genug ist, um den Wert außerhalb des Bereichs darzustellen, und prüfe dann den Bereich, um ...

[... für die Übersetzung gekürzt]

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...

Verwandte Skills