Zum Inhalt springen

Code als signatur-basierten Kopplungsgraphen erkunden

Die Codebasis als signatur-basierten Kopplungsgraphen erkunden, für Menschen auf der Kommandozeile und für KI-Coding-Agenten über MCP.

codecharter graph overview [pfad] [optionen]
codecharter graph neighborhood <target> [pfad] [optionen]
codecharter graph impact <target> [pfad] [optionen]
codecharter graph cycles [pfad] [optionen]
codecharter graph hubs [pfad] [optionen]
codecharter graph paths <from> --to <to> [pfad] [optionen]
codecharter graph diff --base <ref> [pfad] [optionen]

codecharter graph projiziert Ihre analysierte Solution auf einen Kopplungsgraphen und beantwortet darauf sieben Fragen: Wie sieht die Solution von oben aus (overview), womit ist ein bestimmter Typ verbunden (neighborhood), was bricht, wenn Sie diesen Typ ändern (impact), wo ist der Graph kein DAG (cycles), welche Knoten sind tragend (hubs), führt jeder Pfad zwischen zwei Knoten über einen bestimmten Engpass (paths), und hat ein Branch im Vergleich zweier Git-Referenzen die Architektur verschlechtert (diff). Das Kommando richtet sich an zwei Zielgruppen: an Entwickler auf der Kommandozeile, die sich schnell orientieren wollen, und an KI-Coding-Agenten, die eine Codebasis verstehen müssen, ohne vorher jede Datei zu lesen. Dieselben sieben Fragen stehen auch als MCP-Tools zur Verfügung, siehe Mit einem KI-Assistenten.

Knoten sind Solution-Typen (auf der aggregierten overview-Ebene zusätzlich Projekte oder Namespaces). Kanten stammen aus deklarierter Vererbung, Interface-Implementierung, Member-, Parameter-, Rückgabe-, Feld- und Event-Typen, aufgelösten Methodenrumpf-Aufrufen, angewendeten Attributen und Objekterzeugungsausdrücken. Was das genau abdeckt und was nicht, steht unter Kantenbedeutung und Grenzen.

Der Agenten-Workflow: orientieren, hineinzoomen, Auswirkung einschätzen, Zyklen finden, tragende Knoten finden, Engpässe prüfen, Branches vergleichen

Die sieben Subcommands sind zur Verwendung nacheinander gedacht. Ein Agent, der ein unbekanntes Repository übernimmt, arbeitet die meisten davon typischerweise durch, bevor er etwas ändert. hubs steht dabei sinnvollerweise am Anfang, denn es beantwortet, wo diese Codebasis trägt, und leistet damit etwas, was die Top-Kanten-Sicht von overview nicht leistet: Ein Typ mit Fan-in 60, verteilt auf 60 verschiedene Aufrufer, taucht nie als schwere Kante auf, ist aber genau der Typ, den man nicht beiläufig ändern sollte.

1. Orientieren: overview

Starten Sie mit der Form der Solution: welche Projekte oder Namespaces existieren, und welche sind am stärksten gekoppelt.

codecharter graph overview --level projects --top 10
Totals: 4 project(s), 31 namespace(s), 268 type(s)

Nodes (4):
  Acme.Domain  52 type(s)
  Acme.Infrastructure  74 type(s)
  Acme.Web  96 type(s)
  Acme.Tests  46 type(s)

Edges (6, weight desc):
  Acme.Web -> Acme.Infrastructure  (weight 58)
  Acme.Infrastructure -> Acme.Domain  (weight 47)
  Acme.Web -> Acme.Domain  (weight 22)
  Acme.Tests -> Acme.Web  (weight 19)
  Acme.Tests -> Acme.Infrastructure  (weight 11)
  Acme.Tests -> Acme.Domain  (weight 6)

Der Hinweis zur Kantenbedeutung und, bei gekürzter Kantenliste, ein Kürzungshinweis gehen bei jedem Aufruf nach stderr; das Beispiel oben zeigt nur stdout. --level namespaces (der Standard) aggregiert eine Ebene tiefer: Jede Knoten-Id wird zu <Assembly>|<vollständiger Namespace-Name>, weil derselbe Namespace-Name in mehr als einer Assembly vorkommen kann.

2. Hineinzoomen: neighborhood

Sobald klar ist, wo die Kopplung liegt, lösen Sie einen Typ auf und sehen sich seine unmittelbare Umgebung an: wovon er erbt, was er implementiert, was er nutzt, von wem er genutzt wird, was er aufruft und von wem er aufgerufen wird.

codecharter graph neighborhood OrderService --depth 1
Focus: Acme.Domain.OrderService (class)  src/Acme.Domain/OrderService.cs:12
  public OrderService(IOrderRepository repository, IClock clock)
  public async Task<Order> PlaceAsync(OrderRequest request, CancellationToken cancellationToken)
  public async Task CancelAsync(int orderId, CancellationToken cancellationToken)

Implements (1):
  Acme.Domain.IOrderService (interface)  src/Acme.Domain/IOrderService.cs:8  distance 1  [Implements]

Uses (2):
  Acme.Domain.IOrderRepository (interface)  src/Acme.Domain/IOrderRepository.cs:6  distance 1  [Uses]
  Acme.Domain.IClock (interface)  src/Acme.Domain/IClock.cs:4  distance 1  [Uses]

Used by (3):
  Acme.Web.OrdersController (class)  src/Acme.Web/OrdersController.cs:15  distance 1  [Uses]
  Acme.Tests.OrderServiceTests (class)  tests/Acme.Tests/OrderServiceTests.cs:9  distance 1  [Uses]
  Acme.Infrastructure.OrderServiceRegistration (class)  src/Acme.Infrastructure/OrderServiceRegistration.cs:5  distance 1  [Uses]

target wird gegen einen exakten oder groß-/kleinschreibungsunabhängigen vollständigen Typnamen, ein Namenssuffix (OrderService trifft Acme.Domain.OrderService) oder einen Quelldateipfad aufgelöst. Der Pfad ist nützlich für einen Agenten, der die bearbeitete Datei kennt, aber nicht den vollständigen Typnamen. Ein mehrdeutiges oder nicht aufgelöstes Target rät nie: Es gibt die gefundenen Kandidaten aus und beendet sich mit einem Aufruffehler.

3. Auswirkung einschätzen: impact

Bevor Sie einen Typ ändern oder entfernen, fragen Sie, wer von ihm abhängt.

codecharter graph impact OrderService
Dependents by distance (2 level(s)):
  Distance 1 (3):
    Acme.Web.OrdersController (class)  src/Acme.Web/OrdersController.cs:15
    Acme.Tests.OrderServiceTests (class)  tests/Acme.Tests/OrderServiceTests.cs:9
    Acme.Infrastructure.OrderServiceRegistration (class)  src/Acme.Infrastructure/OrderServiceRegistration.cs:5
  Distance 2 (1):
    Acme.Web.Program (class)  src/Acme.Web/Program.cs:1

Mit --source wechseln Sie von "wer hängt hiervon ab" zu "wie erreicht A B":

codecharter graph impact IClock --source OrdersController
Paths (1):
  Path 1:
    Acme.Web.OrdersController (class)  src/Acme.Web/OrdersController.cs:15
    Acme.Domain.OrderService (class)  src/Acme.Domain/OrderService.cs:12  [Uses]
    Acme.Domain.IClock (interface)  src/Acme.Domain/IClock.cs:4  [Uses]

4. Zyklen finden: cycles

Ein Abhängigkeitszyklus fällt bei der Betrachtung einzelner Kanten kaum auf. Ob nur zwei Namespaces zirkulär sind oder vier, lässt sich so nicht zuverlässig unterscheiden. cycles berechnet es exakt: jede starke Zusammenhangskomponente im Graphen, mit einem konkreten Zyklus zum Handeln und, pro Kante, ob ihr Entfernen allein den Zyklus bricht.

codecharter graph cycles --level namespaces
Level: Namespaces. Totals: 4 node(s), 6 edge(s), 1 component(s), 2 node(s) in cycles.

Component 1 (size 2):
  Members (2):
    Acme.Domain (Acme|Acme.Domain)  52 type(s)
    Acme.Infrastructure (Acme|Acme.Infrastructure)  74 type(s)
  Shortest cycle: Acme|Acme.Domain -> Acme|Acme.Infrastructure -> Acme|Acme.Domain
  Closing edges (2):
    Acme|Acme.Infrastructure -> Acme|Acme.Domain  (weight 47, breaks)
    Acme|Acme.Domain -> Acme|Acme.Infrastructure  (weight 3, breaks)

Eine Komponente der Größe 1 wird nie gemeldet, ein Knoten kann in diesem Graphen nicht von sich selbst abhängen. --level namespaces (der Standard) eignet sich am besten für die Architektur-Review: klein genug zum Lesen, und ein Zyklus auf Namespace-Ebene ist fast immer ein echtes Problem. --level types findet mehr, meldet aber viel Rauschen, da Begleit- und geschachtelte Typen sich routinemäßig gegenseitig referenzieren. Verwenden Sie types nur, wenn Sie ein konkretes Refactoring verfolgen. --level projects ist das stärkste Signal und meist leer, da .NET zirkuläre Projektreferenzen verbietet; ein nicht leeres Ergebnis lohnt dort einen sofortigen Blick.

breaksComponent: false bedeutet nicht, dass eine Kante gefahrlos bleiben kann, sondern nur, dass das alleinige Entfernen dieser einen Kante den Zyklus nicht bricht. Die Closing Edges sind so sortiert, dass die brechenden Kanten zuerst kommen, danach nach Gewicht aufsteigend. Die erste Zeile ist damit der günstigste wirksame Schnitt, sofern es einen gibt. Gibt es keinen, etwa bei einer dichten, gegenseitig redundanten Komponente, ist der kürzeste Zyklus darüber die konkrete Einheit zum Handeln: Wählen Sie eine Kante auf dieser Kette zum Brechen.

Ist der fragliche Typ bereits bekannt, führt --target direkt dorthin, statt jede Komponente zu durchsuchen:

codecharter graph cycles --target OrderService

Das beschränkt das Ergebnis auf die einzelne Komponente, die OrderService enthält (--max-components wird dann ignoriert), oder meldet, dass der Typ nicht Teil eines Zyklus ist. Die Auflösung erfolgt auf dieselbe Art wie bei neighborhood.

5. Tragende Knoten finden: hubs

Die Frage, was hier trägt, treibt drei konkrete Entscheidungen: was beim Einarbeiten zuerst zu lesen ist, was als stabiler Vertrag gilt (hoher Fan-in, niedriger Fan-out) im Gegensatz zu einem Blatt, das sich gefahrlos umschreiben lässt, und wo nachzusehen ist, wenn eine Änderung weiter ausstrahlt als erwartet.

codecharter graph hubs --top 5
Level: Types. SortBy: FanIn. Totals: 268 node(s), 812 edge(s), 214 ranked.

Hubs (5):
  Acme.Domain.OrderService                                     fanIn=74   fanOut=9    weightedFanIn=318   weightedFanOut=14   instability=0.108
  Acme.Domain.IOrderRepository                                 fanIn=41   fanOut=1    weightedFanIn=52    weightedFanOut=1    instability=0.024
  Acme.Domain.IClock                                           fanIn=38   fanOut=0    weightedFanIn=61    weightedFanOut=0    instability=0
  ...

fanIn/fanOut zählen verschiedene Nachbarknoten, entsprechend der klassischen Definition afferenter und efferenter Kopplung. weightedFanIn/weightedFanOut summieren stattdessen die Kanten-Gewichte: Ein Typ, den 60 Aufrufer je einmal berühren, hat fanIn: 60, aber ein deutlich kleineres weightedFanIn als ein Typ, den ein Aufrufer 60-mal trifft. instability = fanOut / (fanIn + fanOut): 0 bedeutet, jede Abhängigkeit zeigt nach innen (ein stabiler, gefahrlos nutzbarer Vertrag), 1 bedeutet, jede Abhängigkeit zeigt nach außen (ein Blatt, gefahrlos änderbar). Ein Knoten ohne jede Kante hat keine definierte Instabilität und meldet n/a, nie 0, denn 0 bedeutet maximal stabil, und ein isolierter Knoten ist das nicht.

Diese Grade stimmen nicht mit CouplingAfferent/CouplingEfferent aus einer .ccr-Regel überein. Sie werden über dieselbe Kantenmenge berechnet wie die übrigen Graph-Befehle, sodass sich jedes hubs-Ergebnis gegen graph neighborhood gegenprüfen lässt. Jedes Ergebnis trägt einen entsprechenden Hinweis.

codecharter graph hubs --sort-by instability --min-degree 5

--sort-by akzeptiert fan_in (Standard), fan_out oder instability. total, weightedFanIn und weightedFanOut bleiben unabhängig davon als Felder auf jeder Zeile verfügbar. Sortierung nach instability allein hebt vor allem kaum berührte Knoten hervor, die trivial nahe an 1 liegen; ein höheres --min-degree (Standard 1) sieht daran vorbei. Bei --sort-by instability trägt jedes Ergebnis einen entsprechenden Hinweis.

codecharter graph hubs --scope Acme.Web

--scope (ein Namespace- oder Assembly-Präfix, Groß-/Kleinschreibung egal) beschränkt auf einen Teilbaum, welche Knoten in die Rangliste aufgenommen werden. Das ist nützlich, sobald die globale Top-20 aus einer Handvoll Kern-Domänentypen besteht und Sie wissen wollen, was innerhalb des gerade bearbeiteten Moduls trägt. Die Grade bleiben über den gesamten Graphen berechnet, sodass fanIn/fanOut einer eingeschränkten Zeile die globalen Zahlen bleiben statt einer nach innen gerichteten Zählung; jedes eingeschränkte Ergebnis trägt einen entsprechenden Hinweis. Ein --scope, der auf nichts passt, liefert eine leere Rangliste plus einen Hinweis, der den Scope nennt, nie eine stillschweigend leere Liste.

--level namespaces/--level projects aggregieren auf dieselbe Art wie bei cycles. Kanten innerhalb eines Aggregats entfallen dabei, sodass der Fan-out eines Namespace nur Abhängigkeiten zu anderen Namespaces zählt.

6. Einen Engpass prüfen: paths

impact --source findet einen kürzesten Pfad von A nach B. Das beantwortet "kann A B erreichen", nicht "muss jede Route von A nach B über diesen einen Typ laufen", also genau die Frage, die entscheidet, ob es gefahrlos ist, einen Typ zu löschen, oder ob eine vorgeschlagene Naht ein Modul wirklich von einem anderen isoliert. paths beantwortet diese Frage direkt, ohne jede Route zwischen A und B aufzuzählen (was in einer echten Codebasis exponentiell wäre):

codecharter graph paths OrdersController --to IClock --must-pass-through OrderService
allPathsIntercepted: true
Explored 14 node(s). Exhaustive: true.

Jeder Pfad von OrdersController zu IClock läuft in diesem Fall über OrderService, ein Entfernen oder Absichern von OrderService würde also jede Route kappen. Gilt das nicht, meldet das Ergebnis statt eines bloßen "false" ein konkretes Gegenbeispiel:

codecharter graph paths OrdersController --to IClock --must-pass-through PaymentGateway
allPathsIntercepted: false
Counterexample path:
  OrdersController
  --Uses--> OrderService
  --Uses--> IClock
Explored 9 node(s). Exhaustive: true.

Das Gegenbeispiel ist ein konkreter A-nach-B-Pfad, der PaymentGateway nie berührt, der Beweis, dass die Behauptung nicht gilt. Ein Agent muss dem Ergebnis nicht vertrauen, er kann den Pfad lesen.

Ohne --must-pass-through beantwortet paths stattdessen die reine Erreichbarkeitsfrage ("kann A B überhaupt erreichen"):

codecharter graph paths OrdersController --to LegacyBillingService
reachable: false
Explored 42 node(s). Exhaustive: true.

Die Antwort ist immer einer von drei Zuständen, nie eine gekürzte Vermutung. Eine gekürzte Suche meldet allPathsIntercepted: null (im Text als undecided gerendert) statt eines möglicherweise falschen true, denn "galt für jeden Pfad, den ich gesehen habe" ist nicht dieselbe Aussage wie "gilt für jeden Pfad". isExhaustive sagt, welche der beiden Sie bekommen haben, und nur isExhaustive: true darf je zusammen mit einem true/false-Ergebnis auftreten. Entspricht from dem --must-pass-through-Ziel oder to dem --must-pass-through-Ziel, beginnt oder endet jeder Pfad trivial am Engpass, die Antwort ist also true ohne Suche. (from gleich to nimmt den regulären Weg: die Suche findet das Ziel sofort, mit einem davon verschiedenen Engpass ist die Antwort also false, mit dem Ein-Knoten-Pfad als Gegenbeispiel.) Kann A B unabhängig vom Engpass gar nicht erreichen, ist allPathsIntercepted ebenfalls true (vakuos wahr: es gibt null Pfade, also läuft jeder dieser null Pfade über X); ein Hinweis nennt das explizit, damit es nie mit einer echten Absicherung verwechselt wird.

Auf --level methods (Standard types) durchläuft paths einen bei Bedarf gebildeten induzierten Teilgraphen aus Methodenaufruf-Kanten, siehe paths und Methodenebenen-Ehrlichkeit unten für die Auswirkung auf die Verlässlichkeit der Antwort.

7. Hat der Branch es verschlechtert: diff

Ein git diff zeigt, welche Zeilen sich geändert haben. Er zeigt nicht, dass der Branch eine Abhängigkeit von Domain zu Infrastructure eingeführt hat, dass ein Typ elf zusätzliche Aufrufer bekommen hat, oder dass ein Blatt-Namespace jetzt auf vier andere zeigt, also genau die Befunde, die ein Mensch im Review benennt und die ein Zeilen-Diff verbirgt.

codecharter graph diff --base main
Building the base graph for 'main' (a cache miss costs a restore plus a full analysis; may take a while)...
Base: main @ a1b2c3d (cached). Head: (working tree) @ ?.
Nodes: 1402 -> 1420 (+22/-4). Edges: 5744 -> 5811 (+91/-24/~13).

Added nodes (22):
  + MyApp.Services.OrderReconciler (Class)
  ...
Removed nodes (4):
  - MyApp.Services.LegacyReconciler (Class)
  ...
Added edges (91):
  + MyApp|MyApp.Domain -Calls-> MyApp|MyApp.Infrastructure (weight=6)
  ...
Removed edges (24):
  ...
Changed edges (13):
  ~ MyApp|MyApp.Api -Uses-> MyApp|MyApp.Services (weight 31 -> 47)
  ...
Coupling changes (18):
  MyApp.Domain                                                fanIn   6 -> 6    fanOut   0 -> 1
  ...

--base ist Pflicht, --head ist standardmäßig der Arbeitsbaum. Der häufige Fall, hat mein Branch gegenüber main etwas verschlechtert, kostet damit nichts Zusätzliches jenseits der Base-Seite. Die Knotenidentität ist allein die Id, eine Umbenennungserkennung findet nicht statt: Eine Umbenennung erscheint als ein entfernter plus ein hinzugefügter Knoten, nie als ein einzelner Eintrag "umbenannt". Die Kantenidentität ist (sourceId, targetId, kind). Eine reine Gewichtsänderung, etwa ein Typ, der vierzig zusätzliche Aufrufstellen bei einem anderen bekommt, ist ein echtes Signal und wird als changedEdges-Eintrag gemeldet statt in "unverändert" verschmolzen zu werden.

Ein Cache-Miss auf --base ist nicht kostenlos. Der Vergleich gegen einen Commit, der nicht Ihr aktueller Checkout ist, erfordert, ihn als separaten Arbeitsbaum zu materialisieren, seine NuGet-Pakete wiederherzustellen und eine vollständige Solution-Analyse durchzuführen. Das dauert je nach Solution-Größe Sekunden bis Minuten, weshalb die CLI vorher einen Fortschrittshinweis auf stderr ausgibt. Wiederholte Aufrufe gegen dieselbe --base werden aus einem Cache unter .codecharter/cache/graph/ bedient (bereits durch den bestehenden .codecharter/cache/-Eintrag in .gitignore abgedeckt) und sind schnell. --no-cache umgeht den Cache zu Diagnosezwecken.

codecharter graph diff --base main --level namespaces --top 10

--level (Standard types) steuert die Granularität der Kopplungsänderungs-Zusammenfassung, dasselbe dreiwertige Enum wie bei cycles/hubs. Hinzugefügte und entfernte Knoten bleiben immer auf Typ-Ebene, dort liegt die Evidenz. Bei namespaces/projects werden zusätzlich die Listen hinzugefügter, entfernter und geänderter Kanten auf dieselbe Granularität hochgerechnet: Hundert Ergänzungen auf Typ-Ebene zwischen zwei Namespaces sind ein architektonischer Fakt, nicht hundert. Die Gewichte einer geänderten Kante sind die aufsummierten Gewichte beider Seiten, sodass das gemeldete Delta genau der Summe der zugrunde liegenden Einzeldeltas entspricht. couplingChanges ist nach absoluter Änderung des Gesamtgrads absteigend sortiert und auf --top (Standard 20) begrenzt.

Optionen

[pfad] gilt für alle sieben Subcommands: der Pfad zu einer .sln-, .slnx- oder .csproj-Datei, oder zu einem zu durchsuchenden Verzeichnis. Ohne Angabe, oder wenn ein Verzeichnis angegeben wird, wird automatisch in diesem Verzeichnis gesucht (ohne Angabe im aktuellen).

--format und --output-file gelten ebenfalls für alle sieben. --format akzeptiert text (Standard), json, mermaid und dot, Groß-/Kleinschreibung egal; cycles, hubs, paths und diff akzeptieren nur text/json, da keiner der vier einen Diagramm-Writer hat. Mit --format json verwendet die Ausgabe dieselbe Property-Schreibweise, dieselbe Enum-Schreibweise und denselben Umgang mit Null-Werten wie die MCP-Tools (camelCase-Properties, PascalCase-Enum-Werte, Null-Properties werden weggelassen), sodass ein Skript beide Ausgaben für jedes gemeinsame Feld gleich verarbeiten kann. Die MCP-Ausgabe ist eine Obermenge und liefert zusätzlich durationMs und isWorkspaceWarm, was die CLI-JSON-Ausgabe nicht hat. --format mermaid/dot rendert dasselbe Ergebnis stattdessen als Diagramm, siehe Ein Diagramm rendern: --format mermaid/dot.

--max-tokens gilt ebenfalls für alle sieben, siehe Ergebnisgröße begrenzen: --max-tokens. Dasselbe gilt für --edge-kinds: eine kommagetrennte Liste aus inherits/implements/uses/calls/attributes/creates (Groß-/Kleinschreibung egal, standardmäßig alle sechs), die einschränkt, welche Kanten enthalten sind, siehe Kantenbedeutung und Grenzen. paths nutzt --edge-kinds auf --level methods gar nicht, siehe paths und Methodenebenen-Ehrlichkeit.

overview

Option Standard Beschreibung
--level <projects\|namespaces> namespaces Aggregationsebene.
--top <n> 50 Maximale Anzahl aggregierter Kanten, absteigend nach Gewicht (Referenzanzahl) sortiert.
--max-tokens <n> nicht gesetzt Ungefähres Größenbudget der Ausgabe in Tokens, zusätzlich zu --top.
--edge-kinds <liste> alle sechs Kommagetrennt: inherits/implements/uses/calls/attributes/creates.
--format <text\|json\|mermaid\|dot> text Ausgabeformat. mermaid/dot rendern stattdessen ein Diagramm.
--output-file <pfad> stdout Ausgabe in diese Datei schreiben statt nach stdout.

neighborhood

Option Standard Beschreibung
<target> Erforderlich. Der aufzulösende Typ: vollständiger Name, Namenssuffix oder Quelldateipfad.
--depth <1-3> 1 Breitensuche-Schritte ausgehend vom Fokus-Typ. Auf 1 bis 3 begrenzt.
--direction <out\|in\|both> both Welche Kantenrichtungen expandiert werden.
--signatures <focus\|all\|none> focus Für welche Knoten vollständige Member-Signaturen gerendert werden: nur der Fokus-Typ, der Fokus-Typ und jeder direkte Nachbar in Distanz 1, oder keiner.
--max-types <n> 50 Maximale Anzahl Nachbar-Typen, bevor gekürzt wird.
--max-tokens <n> nicht gesetzt Ungefähres Größenbudget der Ausgabe in Tokens, zusätzlich zu --max-types.
--edge-kinds <liste> alle sechs Kommagetrennt: inherits/implements/uses/calls/attributes/creates.
--format <text\|json\|mermaid\|dot> text Ausgabeformat. mermaid/dot rendern stattdessen ein Diagramm.
--output-file <pfad> stdout Ausgabe in diese Datei schreiben statt nach stdout.

impact

Option Standard Beschreibung
<target> Erforderlich. Der Typ, dessen eingehende Auswirkung (oder, mit --source, dessen Abhängigkeitspfade) berechnet wird.
--source <typ> keine Wenn angegeben, wechselt die Abfrage vom eingehenden transitiven Abschluss zu den kürzesten Abhängigkeitspfaden von diesem Typ zu target.
--depth <n> 10 Maximale Anzahl an Schritten für die Suche: begrenzt die Distanz des Abschlusses und den Suchradius der Pfadsuche.
--max-nodes <n> 200 Maximale Anzahl besuchter Knoten, bevor gekürzt wird.
--max-paths <n> 5 Maximale Anzahl zurückgegebener kürzester Pfade, wenn --source angegeben ist.
--no-through-implementations aus Durchläuft nur deklarierte Kantenrichtungen (eine Implements-Kante von einem Typ zu seinem Interface, nie umgekehrt). Siehe unten.
--max-tokens <n> nicht gesetzt Ungefähres Größenbudget der Ausgabe in Tokens, zusätzlich zu --max-nodes/--max-paths.
--edge-kinds <liste> alle sechs Kommagetrennt: inherits/implements/uses/calls/attributes/creates.
--format <text\|json\|mermaid\|dot> text Ausgabeformat. mermaid/dot rendern stattdessen ein Diagramm.
--output-file <pfad> stdout Ausgabe in diese Datei schreiben statt nach stdout.

DI-Code hängt vom Interface ab, nicht von dessen Implementierung: ChatService implementiert IChatService, also verläuft die Kante ChatService --Implements--> IChatService. Wird sie nur in ihrer deklarierten Richtung verfolgt, erreicht ein Pfad von einem Konsumenten von IChatService nie ChatService (oder das, wovon ChatService selbst abhängt), und der eingehende Closure von ChatService übersieht jeden Konsumenten, der ausschließlich IChatService referenziert. Deshalb ist das rückwärts gerichtete Durchlaufen dieser Kante, vom Interface zu jedem Typ, der es implementiert, standardmäßig eingeschaltet, zusätzlich zu jeder Kante, die die Abfrage ohnehin schon verfolgt. Jeder Schritt über eine rückwärts gelaufene Kante trägt das Label implemented by, das sich vom vorwärts gerichteten Implements unterscheidet. Die Ausgabe verbirgt damit nie, dass die Suche einen Schritt genommen hat, den die Kanten für sich genommen nicht deklarieren, und meldet zusätzlich, wie viele Knoten nur über einen rückwärts gelaufenen Schritt erreichbar waren:

codecharter graph impact ChatService --source OrdersController
Paths (1):
  Path 1:
    Acme.Web.OrdersController (class)  src/Acme.Web/OrdersController.cs:15
    Acme.Domain.IChatService (interface)  src/Acme.Domain/IChatService.cs:4  [Uses]
    Acme.Domain.ChatService (class)  src/Acme.Domain/ChatService.cs:9  [implemented by]

Mit --no-through-implementations durchläuft die Suche stattdessen nur deklarierte Kantenrichtungen. neighborhood und overview kennen diese Option nicht, sie rendern Implements/ImplementedBy bereits als getrennte Richtungen.

Beim Umstieg von einer älteren Version: Jedes Skript und jeder CI-Job, der sich darauf verlässt, dass graph impact an der Interface-Grenze stehen bleibt, braucht jetzt --no-through-implementations. Closures können wachsen, ein mit --max-nodes begrenzter Closure kann jetzt kürzen, wo er vorher vollständig war, und die Pfadsuche kann einen anderen kürzesten Pfad liefern. Die alte Option --through-implementations wird für eine Version lang weiterhin akzeptiert, wirkt sich aber nicht mehr aus.

target und --source werden in neighborhood und impact auf dieselbe Art aufgelöst. codecharter graph respektiert dieselbe exclude-Liste aus .codecharter/config.yml, die auch codecharter analyze verwendet: Eine Datei, die Sie von der Regelanalyse ausnehmen, bleibt auch im Graphen außen vor.

cycles

Option Standard Beschreibung
--level <types\|namespaces\|projects> namespaces Aggregationsebene. types ist hier gültig, bei overview nicht.
--target <typ> keiner Beschränkt das Ergebnis auf die einzelne Komponente, die diesen Typ enthält. --max-components wird dann ignoriert.
--max-components <n> 20 Maximale Anzahl zurückgegebener Komponenten, größte zuerst. Ignoriert, wenn --target angegeben ist.
--max-members-per-component <n> 25 Maximale Anzahl Mitglieder (und Closing Edges) pro Komponente, bevor gekürzt wird.
--min-size <n> 2 Minimale zu meldende Komponentengröße.
--max-tokens <n> nicht gesetzt Ungefähres Größenbudget der Ausgabe in Tokens. Die Akkumulationseinheit ist eine ganze Komponente, nie eine halb befüllte.
--edge-kinds <liste> alle sechs Kommagetrennt: inherits/implements/uses/calls/attributes/creates.
--format <text\|json> text Ausgabeformat. Noch kein mermaid/dot, siehe Optionen oben.
--output-file <pfad> stdout Ausgabe in diese Datei schreiben statt nach stdout.

Anders als bei neighborhood/impact ist ein nicht aufgelöstes oder mehrdeutiges --target hier kein Aufruffehler. Es wird auf dieselbe Art gemeldet wie ein Target, das zu keinem Zyklus gehört: als Hinweis, bei Mehrdeutigkeit zusammen mit den Kandidaten, und mit Exit-Code 0. Beide Fälle bedeuten, dass es für dieses Target nichts zu zeigen gibt.

hubs

Option Standard Beschreibung
--level <types\|namespaces\|projects> types Aggregationsebene. Anders als bei cycles ist types der Standard, denn welcher Typ trägt ist die Frage, die ein Agent zuerst stellt.
--sort-by <fan_in\|fan_out\|instability> fan_in Sortierschlüssel. total, weightedFanIn und weightedFanOut bleiben unabhängig von --sort-by als Felder auf jeder Zeile verfügbar.
--scope <präfix> keiner Namespace- oder Assembly-Präfix (Groß-/Kleinschreibung egal), das einschränkt, welche Knoten in die Rangliste kommen. Die Grade bleiben über den gesamten Graphen berechnet.
--top <n> 20 Maximale Anzahl zurückgegebener Hub-Einträge, sortiert nach --sort-by, bei Gleichstand nach Gesamtgrad absteigend, dann nach Knoten-Id.
--min-degree <n> 1 Minimaler fanIn + fanOut, den ein Knoten braucht, um gelistet zu werden.
--max-tokens <n> nicht gesetzt Ungefähres Größenbudget der Ausgabe in Tokens. Die Akkumulationseinheit ist ein ganzer Hub-Eintrag, nie ein halb befüllter.
--edge-kinds <liste> alle sechs Kommagetrennt: inherits/implements/uses/calls/attributes/creates. Eingeschränkt auf calls beantwortet es, welcher Typ von den meisten Stellen aufgerufen wird; eingeschränkt auf inherits,implements beantwortet es, welche Abstraktion die meisten Implementierer hat.
--format <text\|json> text Ausgabeformat. Kein mermaid/dot, eine Rangliste ist kein Graph.
--output-file <pfad> stdout Ausgabe in diese Datei schreiben statt nach stdout.

paths

Option Standard Beschreibung
<from> Erforderlich. Der aufzulösende Start-Typ oder die Start-Methode.
--to <target> Erforderlich. Der aufzulösende Ziel-Typ oder die Ziel-Methode.
--must-pass-through <target> keiner Wenn angegeben, wird gefragt, ob jeder Pfad von from zu --to über diesen Knoten läuft, statt der reinen Erreichbarkeit.
--level <types\|methods> types Traversierungsgranularität. methods baut bei Bedarf einen induzierten Teilgraphen aus Methodenaufruf-Kanten, siehe paths und Methodenebenen-Ehrlichkeit.
--max-tokens <n> nicht gesetzt Ungefähres Größenbudget der Ausgabe in Tokens; begrenzt zusätzlich, wie viele Knoten die Suche erkunden darf, bevor die Antwort undecided wird.
--edge-kinds <liste> alle sechs Kommagetrennt: inherits/implements/uses/calls/attributes/creates. Wird auf --level methods ignoriert.
--format <text\|json> text Ausgabeformat. Kein mermaid/dot, ein Dreizustands-Ergebnis plus eine Gegenbeispiel-Kette ist kein Diagramm.
--output-file <pfad> stdout Ausgabe in diese Datei schreiben statt nach stdout.

from, --to und --must-pass-through werden auf --level types auf dieselbe Art aufgelöst wie das target von neighborhood (vollständiger Name, Namenssuffix oder Quelldateipfad). Auf --level methods werden sie gegen die kanonische Signatur einer Methode, ihren um den deklarierenden Typ ergänzten Namen oder ein bloßes Namenssuffix aufgelöst, in derselben Reihenfolge exakt-dann-suffix, die nie rät; ein mehrdeutiger oder unbekannter Name gibt jede Kandidaten-Signatur (falls vorhanden) nach stderr aus und beendet sich mit Exit-Code 0 und dem Ergebnis undecided, wie cycles, nicht mit dem Aufruffehler von neighborhood/impact.

paths und Methodenebenen-Ehrlichkeit

Auf --level methods liest paths keine zweite, persistierte Graph-Ebene. Es baut den Methodenaufruf-Teilgraphen bei Bedarf aus demselben analysierten Modell und durchläuft dabei calls-, implements-, implemented by- und overridden by-Kanten, sodass ein Aufruf über ein Interface (die übliche DI-Form: ein Aufrufer hält IOrderService, nicht OrderService) nicht an der Interface-Methode endet. Das hält den persistierten Graphen bei genau einer Granularität (Typen), weshalb --level methods allein zu diesem Subcommand gehört.

Ein true-Ergebnis auf Methodenebene trägt einen Vorbehalt, den ein Ergebnis auf Typebene nicht hat. calls-Kanten stammen aus aufgelösten Methodenrumpf-Aufrufen, derselben Direktaufruf-Analyse, die overview/neighborhood/impact für ihre calls-Kanten verwenden, was eine Unterapproximation ist: ein Aufruf, der nur über ein Delegate, Reflection oder einen von einem Source-Generator erzeugten Rumpf erfolgt, ist für diese Analyse unsichtbar. Das bedeutet, ein false auf Methodenebene (ein Gegenbeispiel-Pfad wurde gefunden) ist immer belastbar, der gezeigte Pfad existiert wirklich, aber ein true auf Methodenebene (jeder gefundene Pfad läuft über den Engpass) sagt nur "jeder Pfad, den diese Analyse sehen kann, tut das"; eine Route über eine unsichtbare Kante könnte weiterhin existieren. Das Ergebnis trägt bei --level methods mit einem true-Ergebnis immer einen entsprechenden Hinweis; die strukturellen Kanten auf Typebene haben diese Lücke nicht, ihre Ergebnisse tragen keinen solchen Vorbehalt.

diff

Option Standard Beschreibung
--base <ref> Pflicht Referenz, gegen die verglichen wird.
--head <ref> Arbeitsbaum Referenz, die verglichen wird.
--level <types\|namespaces\|projects> types Granularität der Kopplungsänderungs-Zusammenfassung und des Kanten-Rollups. Knotenlisten bleiben immer auf Typ-Ebene; Kantenlisten werden bei namespaces/projects mit hochgerechnet.
--max-nodes <n> 50 Maximale Einträge pro Bucket hinzugefügter/entfernter Knoten.
--max-edges <n> 100 Maximale Einträge pro Bucket hinzugefügter/entfernter/geänderter Kanten.
--top <n> 20 Maximale Anzahl Einträge der Kopplungsänderungs-Zusammenfassung, sortiert nach absoluter Änderung des Gesamtgrads absteigend.
--no-cache aus Umgeht den Disk-Cache unter .codecharter/cache/graph/ (Diagnosezwecke).
--max-tokens <n> nicht gesetzt Ungefähres Größenbudget der Ausgabe in Tokens.
--edge-kinds <liste> alle sechs Kommagetrennt: inherits/implements/uses/calls/attributes/creates.
--format <text\|json> text Ausgabeformat. Kein mermaid/dot, ein Diff zwischen zwei Graphen ist selbst kein Diagramm.
--output-file <pfad> stdout Ausgabe in diese Datei schreiben statt nach stdout.

Ein Diagramm rendern: --format mermaid/dot

--format mermaid und --format dot sind auf cycles, hubs, paths und diff ein Aufruffehler: Für cycles ist der Diagramm-Writer auf einen Folgeschritt verschoben, für hubs ist eine Rangliste kein Graph, ein Dreizustands-Ergebnis plus eine Gegenbeispiel-Kette ist kein Diagramm, und ein Diff zwischen zwei Graphen ist selbst kein Diagramm. Alles Folgende gilt für overview, neighborhood und impact.

Beide Formate rendern dieselbe Abfrage als Diagramm statt als Text oder JSON, sodass Sie es direkt in einen PR-Kommentar, ein Design-Dokument oder eine Graphviz-Pipeline einfügen können. Das gibt es nur in der CLI, siehe Mit einem KI-Assistenten für den Grund, warum die MCP-Tools immer JSON liefern.

codecharter graph overview --level namespaces --top 6 --format mermaid
%% CodeCharter graph overview — namespaces
%% Edge styles: thick = inherits, dashed = implements, solid = uses
graph TD
  subgraph c0["Acme.Domain"]
    n0["Orders"]
  end
  subgraph c1["Acme.Infrastructure"]
    n1["Persistence"]
  end
  n0 -->|58| n1
codecharter graph neighborhood OrderService --format dot
// CodeCharter graph neighborhood — focus Acme.Domain.OrderService
digraph codecharter {
  rankdir=TB;
  n0 [label="OrderService", shape=box, style=bold];
  n1 [label="IOrderService", shape=box];
  n0 -> n1 [style=dashed, arrowhead=onormal];
}

Für jedes Diagramm gilt:

  • Knoten-Ids sind synthetisch (n0, n1, ...) und werden in der Reihenfolge des Ergebnisses vergeben, nicht aus dem Typnamen abgeleitet. Zwei Namen, die auf dieselbe Kennung zusammenfielen (etwa zwei generische Instanzen), verschmelzen so nie zu einem Knoten. Das Label ist der kurze Typname, ergänzt um den Namespace in Klammern, wenn ein anderer Knoten im selben Diagramm denselben kurzen Namen trägt.
  • Die Kantenart bestimmt den Linienstil, damit das Diagramm ohne Legende lesbar ist: Inherits ist dick (mermaid ==>) oder mit hohlem Dreieckskopf (DOT arrowhead=onormal), Implements ist gestrichelt, Uses ist einfach, Calls ist ein beschrifteter |calls|-Pfeil (mermaid) oder eine dickere Linie (DOT penwidth=2). Ein rückwärts gelaufener Implements-Schritt (siehe oben) bekommt zusätzlich zum gestrichelten Implements-Stil das Label implemented by und sieht damit nie wie eine gewöhnliche vorwärts gerichtete Implements-Kante aus.
  • Ein Gewicht über 1 wird als Kantenlabel gerendert. Nur die aggregierten Kanten von overview tragen ein Gewicht, neighborhood- und impact-Diagramme zeigen daher nie eines.
  • Die Legende ist immer ein Kommentar (%%///), nie ein Knoten des Graphen, damit ein nachgelagerter Parser sie nicht für einen Teil des Graphen halten kann.
  • Ein leeres Ergebnis rendert weiterhin ein syntaktisch gültiges, leeres Diagramm (graph TD / digraph codecharter {}) statt eines leeren Strings.
  • overview --level namespaces gruppiert Knoten in ein Subgraph pro Assembly. --level projects gruppiert nicht, dort ist ein Knoten pro Assembly bereits die Gruppe.
  • Das Dependents-Diagramm von impact (ohne --source) zieht eine Kante von einem Distanz-1-Dependent direkt zum Ziel, weil das ein bekannter, echter Schritt ist. Tiefere Distanzen werden als nach Distanz gruppierte Knoten ohne Kante zu einem bestimmten Vorgänger gerendert, weil dieses Ergebnis nur meldet, dass ein Knoten das Ziel in N Schritten erreicht, nicht über welchen Typ in Distanz N-1. Für eine vollständig gezeichnete Kette verwenden Sie --source.

--output-file und die Anzahl-Budgets (--top, --max-types, --max-nodes/--max-paths) funktionieren genau wie bei text/json. Verwenden Sie ein niedrigeres --top/--max-types/--max-nodes als bei JSON, denn ein Diagramm mit hunderten Knoten ist unabhängig vom Format nicht lesbar. --max-tokens wird bei einem Diagramm-Format nicht unterstützt, siehe den nächsten Abschnitt.

Ergebnisgröße begrenzen: --max-tokens

Jedes Anzahl-Budget (--top, --max-types, --max-nodes/--max-paths) begrenzt, wie viele Elemente ein Ergebnis enthält. Das ist die falsche Einheit für das, was das Kontextfenster eines Agenten tatsächlich begrenzt: Zwei Aufrufe mit identischer Anzahl können sich um eine Größenordnung in der Größe unterscheiden. neighborhood --signatures all rendert vollständige Member-Signaturen für jeden Nachbarn, dasselbe --max-types mit --signatures none ist winzig. --max-tokens begrenzt stattdessen die ungefähre Größe der Ausgabe, zusätzlich zu einem bereits geltenden Anzahl-Budget: Es zählt, welche Grenze zuerst greift.

Die Schätzung wird aus einer kompakten JSON-Serialisierung mit rund 4 Bytes pro Token berechnet. Sie ist eine Schätzung, kein echter Tokenizer-Wert, und die --format json-Ausgabe der CLI ist eingerückt und daher etwas größer als die Schätzung; ein Skript, das CLI-JSON in ein Kontextfenster pipet, sollte diesen Unterschied einkalkulieren. Jedes Ergebnis meldet estimatedTokens, unabhängig davon, ob gekürzt wurde oder nicht, damit ein Aufrufer den nächsten Aufruf anhand des letzten kalibrieren kann.

Wurde ein Ergebnis gekürzt, meldet es außerdem boundBy: eine geordnete Liste der Optionsnamen, die das Ergebnis tatsächlich begrenzt haben, etwa ["max_types", "max_tokens"], sowie einen gröberen truncationReason (None/CountBudget/TokenBudget/Both). Das ist relevant, weil ein Anzahl-Budget schon während der zugrunde liegenden Suche greifen kann, bevor --max-tokens überhaupt zum Tragen kommt: Nennt boundBy eine Anzahl-Option, bringt ein höheres --max-tokens allein nichts mehr, und der begleitende Hinweis sagt das explizit.

--max-tokens kürzt immer ganze Elemente, nie teilweise:

  • overview verwirft die Kanten mit dem niedrigsten Gewicht.
  • neighborhood sammelt Nachbarn in der Reihenfolge der Breitensuche, also in der Reihenfolge, die die entferntesten Nachbarn zuerst preisgibt, und gruppiert die Überlebenden erst danach nach Relation. Ein gekürztes Ergebnis verliert also entfernte Nachbarn, nie eine ganze Relations-Kategorie. Ist --signatures auf all oder focus gesetzt und hat das Token-Budget das Ergebnis begrenzt, nennt der Hinweis zusätzlich den günstigeren Hebel: Ein engerer --signatures-Bereich liefert innerhalb desselben Budgets deutlich mehr Nachbarn.
  • impact verwirft ganze Distanz-Gruppen nach Knoten im Closure-Modus, oder ganze kürzeste Pfade im Pfad-Modus. Ein zurückgegebener Pfad ist immer die vollständige Kette von Quelle zu Ziel, nie mittendrin abgeschnitten.
  • cycles verwirft ganze Komponenten, größte zuerst, nie eine halb befüllte.
  • hubs verwirft nach der --top-Kürzung ganze Hub-Einträge, in Rangfolge, die niedrigst-platzierten Überlebenden zuerst.
  • paths liefert nie ein gekürztes true/false: Ist das Knoten-Budget erschöpft, bevor die Suche abschließen kann, wird die Antwort stattdessen undecided/null mit isExhaustive: false.

--max-tokens ist standardmäßig nicht gesetzt, es gilt dann kein Größenlimit. Ein Wert von 0 oder darunter ist ein Aufruffehler, nicht "unbegrenzt". Die Option wird nicht zusammen mit --format mermaid/dot unterstützt, da ein mitten im Graphen abgeschnittenes Diagramm syntaktisch kaputt wäre; ein Anzahl-Budget bleibt dort der richtige Hebel für die Diagrammgröße.

Exit-Codes

Code Bedeutung
0 Erfolg.
2 Aufruffehler: ein nicht auflösbarer Pfad, ein nicht erkannter Optionswert, --max-tokens ≤ 0 oder in Kombination mit --format mermaid/dot, oder (nur bei neighborhood/impact) ein target/--source, das auf keinen oder mehr als einen Knoten aufgelöst wurde. Bei einem mehrdeutigen oder nicht aufgelösten Target werden die gefundenen Kandidaten (falls vorhanden) nach stderr ausgegeben. cycles und paths nutzen diesen Code bei einem nicht aufgelösten oder mehrdeutigen Target nie: sie geben die Kandidaten nach stderr aus und beenden sich mit 0 und einem undecided-/No-op-Ergebnis, siehe cycles oben. hubs hat kein Target. Bei diff auch für ein fehlendes --base. Bei paths auch für ein fehlendes --to.
3 Das aufgelöste Analyseziel konnte nicht analysiert werden, bei diff auch eine nicht auflösbare --base/--head-Referenz oder eine fehlgeschlagene Materialisierung der Base-Seite (etwa dotnet restore offline).

Die Ausgabe geht immer nach stdout (oder in --output-file), jeder Hinweis nach stderr, einschließlich des Erinnerungssatzes zur Kantenbedeutung, den jedes Ergebnis trägt. Das ist dieselbe Trennung wie bei codecharter coverage. Wie das zu den übrigen Befehlen passt, steht unter Exit-Codes.

Mit einem KI-Assistenten

Dieselben sieben Abfragen stehen KI-Coding-Assistenten als MCP-Tools zur Verfügung, neben den Analyse- und Coverage-Tools: graph_overview, graph_neighborhood, graph_impact, graph_cycles, graph_hubs, graph_paths und graph_diff. Die Parameter sind das snake_case-Gegenstück der obigen CLI-Optionen (max_types, max_nodes, max_paths, max_components, max_members_per_component, min_size, sort_by, min_degree, max_tokens, source, base, head, max_edges, must_pass_through, level), mit denselben Standardwerten, und target/source/scope/from/to/must_pass_through werden auf dieselbe Art aufgelöst. Die ersten sechs teilen sich den warmen Per-Workspace-Cache der Analyse-Tools: Der erste Aufruf nach dem Öffnen oder Ändern eines Workspace ist ein Cold Start und liefert statt eines Ergebnisses einen Retry-Hinweis, die folgenden Aufrufe sind schnell.

graph_diff verhält sich anders. Es hat keinen no_cache-Parameter, dieser Diagnose-Notausgang bleibt CLI-exklusiv, weil er einem Client erlauben würde, bei jedem Aufruf eine unbegrenzte Neuanalyse auszulösen. Ein Cache-Miss auf der Base-Seite blockiert den Tool-Aufruf zudem nicht: Das Materialisieren und Analysieren einer Revision läuft im Hintergrund, und der erste Aufruf für ein gegebenes base/head-Paar liefert sofort isPending: true statt eines Retry-Hinweises. Rufen Sie mit denselben base/head/path-Werten erneut auf, sobald Sie annehmen, dass es fertig ist. Ein wiederholter Aufruf für eine bereits materialisierte base wird aus einem Per-Commit-Disk-Cache bedient und ist schnell, wie die warmen Aufrufe der anderen fünf Tools.

Die MCP-Tools liefern immer das getypte JSON-Ergebnis. Ein Agent, der das JSON bereits hat, kann daraus jedes gewünschte Diagramm selbst bauen oder für die kanonische Variante codecharter graph … --format mermaid aufrufen.

Ein Agent nutzt diese Tools typischerweise, bevor er unbekannten Code anfasst: graph_hubs zuerst, um zu sehen, was trägt; graph_overview, um zu sehen, wo die Kopplung liegt; graph_neighborhood auf der Datei, die er gerade bearbeiten will, um zu sehen, was sie berührt; graph_impact, um vor der Änderung zu prüfen, was sonst noch betroffen wäre; graph_cycles vor einer Aufgabe wie "das in ein eigenes Paket auslagern" oder "die Architektur aufräumen", bei der der eigentliche Blocker oft ein Zyklus ist, den keine einzelne Kanten-Inspektion gefunden hätte; und graph_paths, wenn die Frage enger ist als beides: "wenn ich diesen einen Typ lösche oder absichere, ist A dann wirklich von B abgeschnitten, oder gibt es eine Route drumherum", was der Kürzeste-Pfad-Modus von graph_impact nicht beantworten kann, denn ein kürzester Pfad beweist Erreichbarkeit, sagt aber nichts über jeden anderen Pfad. Weil graph_neighborhood, graph_impact und graph_paths auch einen Quelldateipfad direkt entgegennehmen, muss ein Agent, der bereits eine Datei geöffnet hat, nicht erst den vollständigen Typnamen nachschlagen.

Kantenbedeutung und Grenzen

Jedes Ergebnis, ob CLI oder MCP, trägt Hinweise, die genau beschreiben, was seine Kanten sind und was nicht. Es gibt drei Kantenfamilien:

  • Strukturelle Kanten sind signatur-basiert: der deklarierte Basistyp eines Typs, seine direkt implementierten Interfaces sowie jeder Typ, der in einem Konstruktor- oder Methodenparameter, einem Methoden-Rückgabetyp, einer Property, einem Feld oder einem Event-Delegate-Typ referenziert wird. Das reicht aus, um die Kopplung zu rekonstruieren, die DI-basierte, geschichtete Codebasen interessiert: wer wovon über die öffentliche Form abhängt.
  • calls-Kanten stammen aus Methodenrumpf-Aufrufen, die zu einem eindeutigen Methodensymbol aufgelöst wurden, und werden dem deklarierenden Typ des Aufgerufenen zugeordnet, nicht dessen Methode; dies ist ein Graph auf Typebene, ohne Methodenknoten. Sie sind standardmäßig enthalten und decken genau die Kategorie ab, die strukturelle Kanten nicht sehen können: Ein statischer Helfer (PathNormalizer.Normalize(p)), eine Extension-Methode oder ein konkreter Typ, der komplett innerhalb eines Methodenrumpfs erzeugt und aufgerufen wird, erreicht nie eine Signatur. Eine calls-Kante ist dort oft der einzige Weg zu einer eingehenden Kante. Das Gewicht ist eine Aufrufstellen-Anzahl, kein Ort: weight: 6 bedeutet sechs aufgelöste Aufrufstellen zu diesem Typ, nicht dass das Ergebnis sagen kann, wo sie stehen.
  • attributes- und creates-Kanten stammen ebenfalls aus aufgelösten Bindungen statt aus Signaturen. attributes verbindet einen Typ (oder ein auf ihm deklariertes Member) mit der Klasse eines angewendeten Attributs; Quelle der Kante ist immer der deklarierende Typ, ein Attribut auf Member-Ebene wird also dem Knoten des deklarierenden Typs zugeordnet. creates verbindet einen Typ mit jedem anderen Typ, den seine Methoden- oder Konstruktorrümpfe mit new T(...) (oder der zieltypisierten Form new(...)) erzeugen und der zu einem eindeutigen Konstruktorsymbol aufgelöst wurde. Beide sind standardmäßig enthalten und nutzen das Gewicht auf dieselbe Art wie calls, also als Stellenzahl, nicht als Ort. Eine Attributklasse oder ein erzeugter Typ außerhalb der Solution, oder mit mehrdeutigem gestripptem Namen innerhalb der Solution, erzeugt keine Kante.

Mit --edge-kinds/edge_kinds schränken Sie ein, welche Kantenarten enthalten sind (inherits, implements, uses, calls, attributes, creates, kommagetrennt, Groß-/Kleinschreibung egal; standardmäßig alle sechs). Der Filter entfernt nur Kanten. Ein Knoten, dessen einzige Kante herausgefiltert wurde, bleibt in der Knotenliste und den Summen enthalten, sodass Zahlen über verschiedene --edge-kinds-Werte hinweg vergleichbar bleiben.

Was calls weiterhin nicht sieht, und wie das Ergebnis das sagt:

  • Mehrdeutige oder nicht-methodenbasierte Aufrufe. Eine Überladungsauflösung, die sich nicht auf einen Kandidaten festlegen kann, oder ein dynamic-Aufruf, liefert kein Symbol zum Auflösen. Der calls-Hinweis nennt die Anzahl solcher Aufrufe. Ein Delegate-, Action- oder Func<>-Aufruf gehört nicht dazu, er wird wie jeder andere Aufruf erfasst und zeigt meist auf einen externen Delegate-Typ, der so oder so keine Kante beiträgt.
  • Mehrdeutige Zieltyp-Namen. Ein aufgelöster Aufruf, dessen gestrippter Zieltyp-Name mehr als einen Solution-Typ trifft (etwa ein Result/Result<T>-Geschwisterpaar), lässt sich keinem eindeutigen Knoten zuordnen. Er wird verworfen und separat gezählt, mit eigenem Hinweissatz, da dies ein anderer Verlust als ein nicht aufgelöster Aufruf ist.
  • Multi-Targeting-Solutions. Ein Projekt mit <TargetFrameworks>net8.0;net9.0</TargetFrameworks> erzeugt eine Projektinstanz pro TFM; nur die Methodenrümpfe des zuerst analysierten TFM fließen in calls-Kanten ein, sodass ein Aufruf innerhalb von #if NETFRAMEWORK-Code für ein anderes Target nicht abgebildet wird. Ein Hinweis nennt das, wenn es zutrifft.
  • Externe Typen sind keine Knoten. Ein Parameter-, Basis- oder Aufrufzieltyp aus einem NuGet-Paket oder der .NET-Runtime ist nicht Teil des Graphen, nur Typen, die in der analysierten Solution deklariert sind.
  • Namespaces sind pro Assembly. Derselbe Namespace-Name, in zwei Projekten deklariert, erzeugt bei --level namespaces zwei getrennte Namespace-Knoten; die Knoten-Id von overview kodiert beides.
  • Multi-Targeting-Projekte werden für Knoten nur einmal gezählt. Jede TFM-Kopie eines Typs fällt auf einen einzigen Knoten zusammen, statt Knotenzahlen und strukturelle Kantengewichte pro TFM zu vervielfachen. Die oben genannte Regel für calls ist die einzige Stelle, an der die TFM-Wahl noch eine Rolle spielt.

Nichts davon steht als Fußnote unter dem Ergebnis, sondern ist Teil jeder Antwort, sodass ein Agent oder ein Skript nie raten muss, ob eine Kantenmenge die ganze Wahrheit bedeutet.

Weiterführend