


Low-Level-Debugger-Code zuerst mit Tests ändern, ohne andere Plattformen kaputt zu machen
Für eine KI, die einen Debugger einer kompilierten Sprache für viele Systeme und Backends ändert. Sie schreibt zuerst einen fehlschlagenden Test, legt OS-spezifischen Code in eigene Dateien, prüft beim Ändern gemeinsamer Schnittstellen alle Backends und beginnt Commits mit dem Subsystem.
Diese KI-Regeln verwenden
## Vor dem Ändern des Debuggers die Schicht lesen, der das Verhalten gehört
Ein Debugger für eine kompilierte Sprache ist ein komplexes, mehrschichtiges System. Um effektiv darin zu arbeiten, braucht man Verständnis für die internen Abläufe des Debuggings, binäre Debug-Info-Formate, betriebssystemspezifische Prozesssteuerung und die internen Abläufe der Sprach-Laufzeitumgebung.
Behandle jede Änderung so, als würde sie ein System berühren, bei dem Korrektheit über Plattformen und Backends hinweg wichtiger ist als reine Geschwindigkeit. Ein subtiler Fehler beim Speicherzugriff, bei der Registerbehandlung oder im Backend-Verhalten zeigt sich als schwer reproduzierbarer Fehler, der nur auf einer einzigen Plattform auftritt.
Lies vor dem Bearbeiten die umgebende Schicht, um zu verstehen, welche Abstraktion für das Verhalten zuständig ist, das du änderst. Änderungen an gemeinsam genutzten Schnittstellen wirken sich auf jedes Backend aus, das sie implementiert, also grenze den Wirkungsbereich ab, bevor du beginnst.
## Vor jeder Codeänderung einen fehlschlagenden Test schreiben und keine Tests überspringen
Alle Codeänderungen müssen der testgetriebenen Entwicklung folgen. Das ist nicht optional.
Befolge den Rot-Grün-Refaktorieren-Verifizieren-Zyklus:
1. **ROT** - Schreibe zuerst einen fehlschlagenden Test; er muss aus dem richtigen Grund fehlschlagen.
2. **GRÜN** - Schreibe den minimal notwendigen Code, damit der Test besteht.
3. **REFAKTORIEREN** - Räume auf, während die Tests grün bleiben.
4. **VERIFIZIEREN** - Führe die vollständige Testsuite aus, bevor du committest.
Verpflichtende Regeln:
1. Schreibe niemals Implementierungscode, ohne zuvor einen fehlschlagenden Test zu haben.
2. Committe niemals Code ohne Tests.
3. Überspringe niemals Tests oder markiere sie als übersprungen, nur damit die CI durchläuft.
4. Deaktiviere niemals bestehende Tests - korrigiere entweder den Test oder den Code.
5. Jeder Test überprüft genau ein bestimmtes Verhalten.
6. Testnamen beschreiben das getestete Szenario.
Wenn es sich schwierig anfühlt, zuerst einen Test zu schreiben, ist diese Schwierigkeit ein Signal, keine Ausrede. Meist bedeutet es, dass eine Funktion zu viel macht, Abhängigkeiten zu eng gekoppelt sind oder die Anforderungen unklar sind. Überspringe TDD nicht - behebe stattdessen das Designproblem, das dadurch sichtbar wurde.
## Festlegen, wohin jede Testart gehört, und Testergebnisse wiederholbar halten
Wo Tests hingehören:
- **Unit-Tests**: gemeinsam mit dem Code ablegen, den sie abdecken.
- **Integrationstests**: in einem eigenen Verzeichnis für End-to-End-Tests.
- **Plattformspezifische Tests**: über Build-Tags/bedingte Kompilierung absichern.
- **Backend-spezifische Tests**: jedes Backend separat prüfen.
- **Fixtures**: als Quellcode halten, der während des Testlaufs kompiliert wird, nicht als vorgefertigte Binärdateien; den gemeinsam genutzten Fixture-Locator-Helper verwenden statt eine eigene Pfadsuche zu bauen.
Qualitätsstandards:
- Teste Verhalten, nicht Implementierung.
- Bevorzuge eine Zusicherung (Assertion) pro Test, damit ein Fehlschlag auf genau eine Ursache verweist.
- Benenne klar, mit einem durch Unterstriche getrennten Muster aus dem, was getestet wird, dem Szenario und dem erwarteten Ergebnis.
- Nutze die vorhandenen Test-Hilfsfunktionen wieder, statt sie neu zu erfinden.
- Tests müssen deterministisch sein - keine Zufallswerte, keine Race Conditions.
- Prüfe bei Tests für Ausgabeformate die vollständige erwartete Ausgabe statt nur eine Teilzeichenfolge. Der Vergleich der vollständigen Ausgabe erkennt Regressionen - zusätzliche Leerzeichen, vertauschte Felder, Formatänderungen -, die bei einer Teilstring-Prüfung stillschweigend übersehen werden.
## Lesende und zustandsändernde Schnittstellen zwischen den Schichten trennen
Das System ist in Schichten mit klarer Trennung der Zuständigkeiten organisiert: oben eine CLI, darunter eine Service-/Protokollschicht, darunter Operationen auf hoher Ebene, darunter eine Prozess-/Ziel-Abstraktion und ganz unten austauschbare Backend-Implementierungen (direkte Prozesssteuerung auf Betriebssystemebene, ein Remote-Protokoll-Client, Offline-Dump-Analyse, Tracing auf Kernel-Ebene).
Verwende an der Abstraktionsgrenze ein zweistufiges Schnittstellendesign:
- Eine schreibgeschützte öffentliche Schnittstelle, über die Aufrufer den Zustand beobachten.
- Eine separate interne Schnittstelle, die alle zustandsverändernden Operationen trägt.
- Ein zustandsbehaftetes Wrapper-Objekt um die interne Schnittstelle sowie eine Gruppenabstraktion zur gleichzeitigen Verwaltung mehrerer Ziele.
Backends sind hinter der internen Schnittstelle austauschbar. Wenn du diese Schnittstelle änderst, betrifft die Änderung in der Regel ALLE Implementierungen - berücksichtige jedes Backend, nicht nur dasjenige, das gerade vor dir liegt.
## Betriebssystem- und architekturspezifischen Code in eigene Dateien legen
Isoliere betriebssystem- und architekturspezifischen Code durch dateinamenbasierte bedingte Kompilierung - eigene Quelldateien pro Betriebssystem und pro Architektur, die vom Build-System ausgewählt werden statt durch Verzweigungen zur Laufzeit.
- Platziere niemals plattformspezifischen Code in generischen Dateien.
- Beschränke plattformspezifischen Code auf die Backends, die ihn tatsächlich benötigen (das Backend für die direkte Betriebssystemsteuerung und das Remote-Protokoll-Backend), nicht auf die gemeinsam genutzte Kernlogik.
- Halte Details zu Registern, ABI und Aufrufkonventionen in architekturspezifischen Dateien, wenn viele Architekturen unterstützt werden.
Gut umgesetzt bleibt der gemeinsame Kern portabel, und das Hinzufügen einer neuen Plattform beschränkt sich auf das Hinzufügen von Dateien, statt generischen Code umzuschreiben.
## Häufige Fehler beim Lesen von Prozessspeicher und Setzen von Breakpoints vermeiden
1. **Ändere gemeinsam genutzten Zustand nicht direkt** - nutze die Methoden der internen Schnittstelle, die Sperrung (Locking) und Zustandskonsistenz gewährleisten.
2. **Backend-spezifische Operationen unterscheiden sich** - eine konzeptionelle Aktion (zum Beispiel das Setzen eines Breakpoints) wird pro Backend unterschiedlich implementiert; gehe niemals von einem einzigen Mechanismus aus.
3. **Unterscheide ähnliche Abstraktionen** - ein Betriebssystem-Thread und eine Nebenläufigkeitseinheit auf Sprachebene sind nicht dasselbe; wisse, auf welche der beiden eine Operation abzielt. Die meisten Operationen wirken auf die Einheit der Sprachebene, nicht auf den rohen Thread.
4. **Speichersicherheit** - behandle beim Lesen des Speichers des Zielprozesses immer mögliche Fehler. Der Prozess kann beendet werden, Speicher kann nicht mehr zugeordnet (unmapped) sein, oder eine Adresse kann ungültig sein.
5. **Füge keine redundanten Durchläufe hinzu** - wenn ein bestehender Durchlauf bereits alle Einheiten (Kompilierungseinheiten, Symbole usw.) durchläuft, füge deine einheitsbezogene Prüfung dort hinzu, statt separat zu scannen.
6. **Platziere Instrumentierung nach dem Prolog** - setze Breakpoints/Probes nach dem Funktionsprolog statt am rohen Einstiegspunkt, da der Stack-Check-Prolog einer Laufzeitumgebung erneut auslösen kann, wenn am Einstiegspunkt instrumentiert wird.
## Commit-Nachrichten mit dem betroffenen Subsystem beginnen und das Warum erklären
Verwende ein subsystembasiertes Commit-Format: eine Betreffzeile, die das betroffene Subsystem benennt, gefolgt von einem Doppelpunkt und einer kurzen Zusammenfassung der Änderung, dann eine Leerzeile, dann einen Textabsatz, der erklärt, warum die Änderung vorgenommen wurde, und schließlich eine abschließende Zeile mit einem Verweis auf das behobene Issue.
- Stelle der Betreffzeile das betroffene Subsystem voran, damit die Historie nach Bereichen durchsuchbar bleibt.
- Halte die Betreffzeile knapp und begrenzt (etwa 70 Zeichen).
- Nutze den Textkörper, um zu erklären, warum die Änderung vorgenommen wurde, mit Zeilenumbruch (etwa 80 Zeichen).
- Verweise auf das auslösende Issue, damit sich jede Änderung auf ihre Ursache zurückführen lässt.
Strukturierte Nachrichten halten das Änderungsprotokoll übersichtlich und machen es leicht nachvollziehbar, warum eine bestimmte Änderung vorgenommen wurde.
