Code-Intelligenz
Control Center gibt einem Agenten vier Wege, Code zu verstehen, die über das Lesen hinausgehen. Diese Wege beantworten jeweils andere Fragen, und es sind vier statt einer, weil kein einzelner Mechanismus alle beantwortet.
| Mechanismus | Beantwortet | Aufwand |
|---|---|---|
| Code-Graph | Was ist hier, und was hängt davon ab | Ein Indexlauf; Abfragen sind günstig |
| Sprachserver | Kompiliert das wirklich, und welchen Typ hat das | Ein Serverprozess pro Projekt |
| Strukturelle Suche | Wo taucht diese FORM auf | Ein Parse pro Datei |
| Debugger | Was in diesem Moment in diesem Frame galt | Ein angehaltener Prozess |
Die Lücke, die der Sprachserver schließt
Abschnitt betitelt „Die Lücke, die der Sprachserver schließt“Der Code-Graph entsteht aus tree-sitter: Symbole, Aufrufkanten, ein Auswirkungsradius. Er ist schnell, er ist günstig, und er weiß nichts über Typen. Er kann dir nicht sagen, ob der Aufruf, den du gerade geschrieben hast, kompiliert, ob dieses Feld auf dem Objekt existiert oder ob dein Umbenennen einen Re-Export verpasst hat.
Ohne Sprachserver merkt ein Agent das zur Testzeit, oder nie. Er schreibt etwas Plausibles, macht weiter, und der Fehler taucht drei Züge später an einer anderen Änderung auf.
Der Gewinn ist nicht das lsp-Tool. Er ist, dass write, edit und apply_patch umhüllt sind: Nach jedem Schreiben geht die Datei durch den Sprachserver, und neu hinzugekommene Diagnosen fließen ins Tool-Ergebnis ein. Der Agent erfährt in dem Moment, in dem er den Build bricht, dass er ihn gebrochen hat, ob er danach gefragt hat oder nicht.
Nur neue Diagnosen
Abschnitt betitelt „Nur neue Diagnosen“Zu wiederholen, was der Agent schon erfahren hat, ist schlimmer, als nichts zu sagen: Das Ergebnis füllt sich mit Rauschen, das der Agent überfliegen lernt, und der eine neue Fehler geht mit unter. Ein Protokoll pro Lauf hält fest, was schon gemeldet wurde, nach Schweregrad, Code und Meldung – nicht nach Position, damit eine Diagnose, die bloß vier Zeilen nach unten gerutscht ist, nicht noch einmal als neu gemeldet wird.
Faul, und ehrlich, wenn er fehlt
Abschnitt betitelt „Faul, und ehrlich, wenn er fehlt“Beim Start läuft kein Server. Einer startet bei der ersten Anfrage, die ihn braucht, fast immer die erste Bearbeitung einer Datei, für die er zuständig ist, und untätige werden weggeräumt. Ein Host, der Dart nie anfasst, startet nie einen Analyzer.
Die Erkennung ist eine Schnittmenge: Im Checkout muss ein Wurzelmarker liegen, und die Binärdatei muss sich auflösen lassen. Ein Marker allein sagt „dieses Projekt ist Python“ und nichts darüber, ob der Server installiert ist. Eine auflösbare Binärdatei allein sagt, dass der Host eine Toolchain hat, und nichts über diesen Checkout. Nur beides zusammen heißt, dass eine Abfrage gelingen kann, und ein Tool, das angeboten wird und immer fehlschlägt, ist schlimmer als eines, das fehlt.
Warum strukturelle Suche, wenn es grep gibt
Abschnitt betitelt „Warum strukturelle Suche, wenn es grep gibt“grep trifft Zeichen. Das ist das richtige Werkzeug für einen Namen und das falsche für eine Form.
Ein strukturelles Muster wird mit derselben Grammatik wie die Datei geparst, deshalb findet dispose($X) den Aufruf, egal wie er gesetzt, umbrochen oder über Zeilen gebrochen ist, und trifft dieselben Zeichen nie in einem Zeichenkettenliteral oder einem Kommentar.
Was es über einen Regex mit Extra-Schritten hebt: Eine Metavariable, die in einem Muster wiederholt wird, muss denselben Text erfassen:
if ($X != null) $X.dispose()Es trifft eine Absicherung, die genau das freigibt, was sie geprüft hat, und keine, die etwas anderes freigibt. Das ist eine Fehlerklasse, die sich textuell überhaupt nicht ausdrücken lässt.
Es läuft auf dem tree-sitter, der schon eingebunden ist
Abschnitt betitelt „Es läuft auf dem tree-sitter, der schon eingebunden ist“Eine zweite Engine gibt es nicht. cc_natives lädt tree-sitter und seine Grammatiken schon für den Code-Graph, mit einem Cache für kompilierte Abfragen und einer Sprachtabelle. Der Matcher ist reines Dart über Bäumen, die sowieso geparst wurden. Eine zweite strukturelle Engine hieße einen zweiten Parser, einen zweiten Grammatiksatz, den man auf jeder Plattform bauen und bereitstellen müsste, und zwei Antworten auf „was ein Aufrufausdruck ist“.
Ein Fragment braucht einen Ort
Abschnitt betitelt „Ein Fragment braucht einen Ort“Eine Grammatik legt nur fest, was eine ganze Datei ist. Nackt auf Darts oberster Ebene geparst, ist dispose(x) eine völlig gültige Funktionssignatur – eine Deklaration namens dispose mit einem Parameter x – und nirgends ein Fehler, der das verrät. Ein Matcher auf diesem Parse sucht Deklarationen, findet keine Aufrufe und meldet „keine Treffer“ für ein Muster, das nie so gelesen wurde, wie es aussieht.
Ein Muster durchläuft deshalb eine Leiter von Gerüsten – Anweisung, Ausdruck, Klassenmember, oberste Ebene – und das erste, das es annimmt, gewinnt. Ein Fragment, das kein Kontext annimmt, wird als nicht parsebar gemeldet, statt gesucht und stillschweigend nicht gefunden zu werden.
Warum eine Umschreibung erst ins Staging geht, statt zu schreiben
Abschnitt betitelt „Warum eine Umschreibung erst ins Staging geht, statt zu schreiben“Eine strukturelle Umschreibung ist der weitreichendste Eingriff, den ein Agent machen kann: ein Muster, vierzig Dateien, und niemand liest das Ergebnis Zeile für Zeile, weder das Modell noch die Person, die danach gefragt hat. Sie zu schreiben und „fertig“ zu melden heißt, dass der erste ehrliche Blick auf die Änderung hinterher ein git diff ist.
Deshalb meldet ast_edit, was es tun würde – die Anzahl, die Dateien, die Fundstellen – und die Änderung landet erst, wenn resolve sie übernimmt. Genau das lässt einen Diff und die Wahl Übernehmen/Verwerfen vor einem Menschen stehen, bevor irgendetwas auf die Platte kommt.
Der Commit ist alles oder nichts, und jede Datei wird gegen den Inhalt geprüft, der beim Staging festgehalten wurde. Zwischen Staging und Commit kann der Agent eine dieser Dateien von Hand geändert haben, ein Diagnoselauf kann sie umgeschrieben haben, oder ein Formatter kann sie angefasst haben. Trotzdem zu committen würde diese Arbeit verwerfen, und der Diff sähe aus, als wäre er so gewollt. Jede Abweichung lehnt die ganze Änderung ab, weil eine nur teilweise angewendete strukturelle Umschreibung einen Baum hinterlässt, der unter keiner der beiden Formen kompiliert.
Was ein Debugger beantwortet, eine Print-Anweisung aber nicht
Abschnitt betitelt „Was ein Debugger beantwortet, eine Print-Anweisung aber nicht“Die Schleife mit der Print-Anweisung ist: eine Ausgabe einfügen, das Ganze noch einmal laufen lassen, die Ausgabe lesen, die Anweisung wieder löschen. Sie kostet einen vollen Testlauf pro Frage und beantwortet nur die Frage, die du dir vor dem Lauf gestellt hast.
Ein angehaltener Frame beantwortet jede Frage zu diesem Moment auf einmal: jede lokale Variable, jeden Aufrufer und einen Ausdruck, der im eigenen Scope des Frames ausgewertet wird. Bei einem festgefahrenen Prozess oder einem Fehler, der sich nur einmal in zehn Läufen reproduziert, ist das der Unterschied zwischen einem Nachmittag und einer Minute.
Begrenzt wie eine Kapsel, und aus demselben Grund
Abschnitt betitelt „Begrenzt wie eine Kapsel, und aus demselben Grund“Ein Debug-Adapter besitzt einen angehaltenen Prozess, der festhält, was dieser Prozess festhält: einen Port, eine Sperre, eine Datenbankverbindung. Eine Sitzung, die niemand steuert, ist ein Leck, das den Zug überlebt, der sie gestartet hat. Deshalb gibt es eine harte TTL, genau eine Sitzung pro Unterhaltung, und ein zweites launch wird abgelehnt, statt das erste stillschweigend zu ersetzen: Ein Agent, der ein zweites startet, ohne das erste zu beenden, hat das erste mit ziemlicher Sicherheit aus den Augen verloren.
Ein Transport, zwei Protokolle
Abschnitt betitelt „Ein Transport, zwei Protokolle“Das Debug Adapter Protocol teilt mit dem Language Server Protocol das Content-Length-Framing und nichts darüber: andere Korrelationsschlüssel, eine andere Fehlerform und ein dritter Nachrichtentyp (events) ohne Entsprechung im LSP. Das Framing liegt an einer Stelle; die Protokolle sitzen getrennt darauf.
Das Framing zweimal zu schreiben hieße zwei Antworten auf „eine Nachricht reicht über zwei Chunks“, „ein Chunk enthält drei Nachrichten“ und „der Server hat vor seinem ersten Frame ein Banner nach stdout geschrieben“, und nur eine davon bekäme die Korrektur.
Verwandt
Abschnitt betitelt „Verwandt“- Code mit dem Code-Graph suchen – die Seite für Symbole und Auswirkungen
- Einen fehlgeschlagenen Test mit einem Agenten debuggen – der Debugger, von vorn bis hinten
- Code strukturell refaktorieren –
ast_grepundast_edit - Eingebaute Agenten-Tools – jede Operation und jedes Argument