Was macht c4?
C4 beantwortet „Was ist dieses System, woraus besteht es und wer ruft wen?"
auf drei Zoomstufen. Die Oberfläche ist die visualize-Engine: eine rataflow-Leinwand,
ein beschriebener Abschnitt pro Ebene, containers-Boxen für Eigentum. Lies
agents/.agents/skills/visualize/SKILL.md für den Renderer selbst – dieses
Skill deckt nur ab, was C4 hinzufügt.
C4 fügt drei Dinge hinzu, die visualize nicht tut:
- Eine Ebenenleiter. L0-Kontext, dann L1/L1b-Container, dann L2/L2b- Komponenten. Jede Ebene dient dazu, die nächste zu motivieren, und die Ebene, auf der sich ein Leser befindet, ist an der Box erkennbar, nicht an einer Legende.
- Fakten sammeln. C4 muss vollständig sein, was wer wen warum ruft betrifft. Scouts lesen die Codebasis und erstellen ein beleggestütztes Element- und Beziehungsinventar, bevor eine Spezifikation existiert.
- Enthaltung als Behauptung. Eine Container-Box enthält die eigenen Teile der Komponente und nichts anderes. Das ist eine Eigentumsbehauptung, also benötigt sie Quellen, und sie muss überleben, dass ein Leser die Box gegen den Code prüft.
Erfinde kein Element, keine Beziehung und keine Grenze. Wenn die Scouts es nicht zitieren können, wird es nicht gezeichnet.
Routing
| Frage | Verwenden |
|---|---|
| C4-Ebenen einer Komponente in einer echten Codebasis | dieses Skill |
| Ein Beziehungsdiagramm aus bereits vorhandenen Fakten | visualize |
| Diagramm in der Chat-Antwort | mermaid-Tool |
| Statisches ANSI / PNG | mermaid-Skill |
| Architektur-Prosa-Seite oder Präsentation | artifact-Skill |
/c4 <Komponente> oder „Gib mir eine C4-Ansicht von X" ist der Aufruf. Das Skill
funktioniert in einem Repository, das es noch nie gesehen hat: Die Scouts sind die
einzige Möglichkeit, wie es etwas lernt, also hängt hier nichts von Vorkenntnissen
des Codes ab.
Schritt 0 – Die Komponente festlegen
Gib an, bevor ein Scout läuft:
component: Maschinenverwaltung
repo root: /Users/aviral/modal
anchor: machine_manager/ (ein Paket, eine bereitstellbare Einheit)
- Komponente ist eine Sache, auf die ein Leser im Code zeigen kann: ein Dienst, ein Paket, eine bereitstellbare Einheit. Wenn die Anfrage ein Subsystem benennt, das mehrere bereitstellbare Einheiten umfasst, wähle die bereitstellbare Einheit und sage, welche Nachbarn sie als externe Systeme einbezieht.
- Repo root ist der Startpunkt für die Scouts. Bestätige, dass das Checkout existiert; nimm keinen Pfad an.
- Anchor ist das stärkste einzelne Beweisstück dafür, dass die Komponente eine
Sache ist (Paketmanifest, Helm-Chart, Terraform-Modul, Proto-Dienst,
main.go). Wenn du keinen nennen kannst, ist die Komponente wahrscheinlich zwei Komponenten – frage den Menschen statt zu raten.
Stelle höchstens eine klärende Frage (die Komponentengrenze ist die einzige, die sich lohnt zu fragen). Ansonsten entscheide und dokumentiere.
Schritt 1 – Die Scouts starten
Vier Prompts befinden sich in prompts/; jeder ist eine vollständige Subagenten-
Aufgabe. Starte sie als subagent-Aufrufe mit agent: "scout" (schreibgeschützte
Erkundung – Scouts dürfen das Repository nicht bearbeiten), alle vier in einem
Schritt, damit sie parallel laufen, cwd: die in Schritt 0 aufgelöste
Repository-Wurzel, und der Komponentenname für <component> eingesetzt:
| Scout | Prompt | Rückgabe |
|---|---|---|
| Scope | prompts/scope.md | Elementinventar: ID, Einzeiler-Rolle, Einstiegspunkte, bereitstellbare Einheiten, Datenspeicher, externe Dienste, Akteure, Dateipfade |
| Relationships | prompts/relationships.md | von → nach : Verb + Transport-Zeilen, jeweils mit Datei:Zeile |
| Boundary | prompts/boundary.md | Mitglieder vs. Externe + die Modul-/Bereitstellungsgrenze, die jede Mitgliedschaft rechtfertigt |
| Levels | prompts/levels.md | Ebenenzuweisung pro Element und welche Ebenen in L1/L1b (L2/L2b) aufgeteilt werden müssen |
Jeder Scout gibt Belege mit Datei:Zeile-Referenzen zurück. Eine Zeile ohne
solche ist ein Gerücht; verwerfe sie oder frage diesen Scout erneut nach einer
Quelle.
Starte die Scouts vom Repository-Stammverzeichnis aus – führe die Sitzung dort
aus (oder setze cwd: auf dasselbe Verzeichnis, in dem sich die Sitzung bereits
befindet). Ein Scout, dessen cwd sich von dem der startenden Sitzung
unterscheidet, kann sterben, bevor sein Prozess startet, und der Fehler erscheint
als „In seinem Fenster erschien kein Pi-Prozess".
Befrage die Unteragenten nicht. Die Ergebnisse treffen von selbst ein; fahre fort, wenn alle vier zurück sind. Wenn einer fehlschlägt, sag welcher, und wiederhole ihn entweder oder dokumentiere die Lücke im Generierungsprotokoll – fülle das Loch niemals aus dem Gedächtnis.
Schritt 2 – In ein Hauptbuch abgleichen
Führe die vier Rückgaben in drei Tabellen im Generierungsprotokoll zusammen. Dies ist die gesamte Faktenbasis; die Spezifikation darf nichts enthalten, was nicht hier ist.
elemente: id | label | rolle | klasse (akteur|extern|mitglied|speicher) | beleg
beziehungen: id | von → nach | verb + transport | ebene | beleg
grenze: id | innen/außen | rechtfertigende grenze | beleg
- Konflikte werden gelöst, indem die stärkere Quelle zitiert wird (ein Manifest oder Bereitstellungs-Chart schlägt einen Kommentar; eine Aufrufstelle schlägt einen Docstring), und die verlierende Behauptung wird aufgezeichnet, nicht gelöscht.
- Ungelöste Konflikte und Beziehungen ohne überlebende Quelle werden als offene Fragen aufgezeichnet. Stille ist ein Fehler; ein sichtbarer Zweifel ist keiner.
- Behalte eine ID pro Element bei und verwende sie über Ebenen hinweg wieder, damit
ein Leser, der
q → regverfolgt, auf L1 und L2 dieselben zwei Boxen sieht.
Schritt 3 – Was zu zeichnen ist (Priorisierung)
Eine C4-Ansicht ist ein Urteil über die Wichtigkeit, kein Auszug des Aufrufgraphen. Das Hauptbuch enthält jede Beziehung, die ein Scout gefunden hat; das Diagramm enthält die, die ein Leser braucht, und sagt laut, was es weggelassen hat.
Eine Beziehung verdient einen Platz, wenn sie mindestens eines dieser Kriterien erfüllt:
| ID | Test | Beispiel |
|---|---|---|
| I1 | Sie überschreitet die Komponentengrenze (Mitglied ↔ Extern, Extern ↔ Extern) | webapi → Latitude API, machines → sshx.io |
| I2 | Es ist ein primärer Anforderungs-, Steuerungs- oder Datenpfad, den die Quelle ausdrücklich nennt (Provisionierung, Heartbeat, Audit, Abgleich, Boot) | control plane → private gRPC, River queue → workers |
| I3 | Sie trägt ein Protokoll oder einen Qualifizierer, den der Leser braucht: RPC-Verb, Transport, Port, Bucket, Intervall, Timeout | register + heartbeat alle 1 Min, gcore sync alle 1 Min |
| I4 | Sie erklärt einen Datenspeicher oder einen Akteur, den der Leser nicht aus den Boxen ableiten kann | store → Postgres, ops + mmctl → webapi |
| I5 | Es ist ein Zweig eines redundanten oder parallelen Pfades, der zählbar bleiben muss | machines → boot und boot → machines; zwei unabhängige Züge |
Fallklassen – eine Beziehung verlässt das Diagramm nur als eine dieser:
| ID | Klasse | Was es ist |
|---|---|---|
| C1 | internes Detail | vollständig innerhalb eines Containers, fügt keine Grenz-, Protokoll- oder Akteursinformationen hinzu (worker → store pro Abfragezeile, wenn die Schreiber des Speichers bereits gezeichnet sind) |
| C2 | doppelte Aufrufstelle | derselbe Sprung wie eine gezeichnete Beziehung, der sich nur in Auftragstyp oder Argument unterscheidet, den die gezeichnete Beschriftung bereits trägt |
| C3 | Helfer | ein Element, dessen einzige Aufgabe darin besteht, denselben Container erneut zu betreten (ein Client-Wrapper zwischen zwei gezeichneten Boxen) |
| C4 | redundanter Zweig | ein weiterer paralleler Zweig eines Pfades, dessen Zählbarkeit der Leser nicht benötigt (ein zweites Artefakt aus demselben Bucket, derselbe Transport) |
Regeln, die das Urteil ehrlich halten:
- Ein Pfeil kann mehrere Aufrufstellen tragen, wenn die Beschriftung ihre Verben
trägt (
erstellen / beenden / reparieren). Das ist eine C2-Faltung, keine Löschung. - Grenzüberschreitung gewinnt einen Gleichstand (I1 vor I3 vor I2). Ein Diagramm, das einen externen Sprung fallen lässt, um Platz für einen internen zu schaffen, hat seine eigenen Prioritäten umgekehrt.
- Im Zweifel zeichne es, und lass die Beschreibung der Ebene sagen, warum es da
ist. Zweifel ist kein Grund zum Fallenlassen;
C1–C4sind es. - Niemals stillschweigend fallen lassen. Jede Hauptbuchzeile endet als: gezeichnet,
gefaltet in <gezeichnete Beziehung>(mit der Rechtfertigung der Faltung als C2), oderweggelassen – C1|C2|C3|C4mit einer Zeile warum. Die Abdeckungskarte (Schritt 7) gibt die vier Zählungen an: gezeichnet, gefaltet, nach Klasse weggelassen, nicht berücksichtigt. - Umfangsreduzierung ist eine andere Sache und benötigt die Worte des Anrufers.
„Nur die Steuerungsebene" oder „maximal 20 Beziehungen" ist eine Umfangsanfrage:
dokumentiere sie als Grundlage (
basis: caller scope, N relations) zusammen mit dem gezählten Hauptbuch. Priorisierung ist der Standard; eine Teilmenge ist eine Entscheidung, die der Anrufer trifft, nicht eine, die der Generator ableitet.
Schritt 4 – Die Ebenenleiter
Die Zielform sind fünf Abschnitte, von oben nach unten:
L0 · Systemkontext ein Knoten für die Komponente + ihre Nachbarn
L1 · Die Komponente die Komponente einmal geöffnet, Container auf ihren Teilen
L1b · <zweite Seite> dieselbe Tiefe, die andere Seite des Systems
L2 · Innen – <Slice A> Beziehungen zwischen Mitgliedern, Container auf ihnen
L2b · Innen – <Slice B> dieselbe Tiefe, der andere Slice
Das ist die Leiter: eine Leinwand mit fünf Abschnitten ist das Ergebnis. Mehr Abschnitte sind ein Fehler, es sei denn, die unten stehende Aufteilungsdisziplin ist mit Zahlen erfüllt.
Zeichne jede Ebene erst nachdem die darüber liegende existiert, und gib jeder ihre Beschreibung, bevor du fortfährst.
| Ebene | Zeichnet | Box-Verhalten |
|---|---|---|
| L0 Systemkontext | die Komponente als EINEN einfachen Knoten, plus jeden Akteur und jedes externe System darum herum, plus die groben Pfeile zwischen ihnen | kein Container, kein focus, keine Doppelrand – der Punkt ist die umgebende Welt, also sieht die Komponente aus wie jede andere Box |
| L1 Container | die Komponente eine Ebene geöffnet: ihre eigenen bereitstellbaren/laufzeit Einheiten innerhalb eines Containers; die L0-Externen bleiben außen | Containerbox auf den Mitgliedern |
| L1b Seitenansicht | der optionale zweite Slice, wenn ein L1 den Fluss wirklich nicht halten kann (das Fleet-Ende, die Datenebene, der Batch-Pfad) – gemessen, niemals ein neues Thema | Container, gleiche Regeln |
| L2 Komponenten | die Beziehungen innerhalb eines Containers – die Mitglieder von L1, die sich nun gegenseitig aufrufen | Containerbox um sie herum |
| L2b | optionaler zweiter Slice derselben Tiefe, wenn ein L2 seine Beziehungen nicht routen kann oder zwei Geschichten separate Ränge benötigen | Container, gleiche Regeln |
Ebenenregeln, die nicht verhandelbar sind:
- L0 ist die Komponente als EINEN einfachen Knoten. Eine Box für die gesamte
Komponente, ihre normale Umrandung, plus die Akteure und externen Systeme um sie
herum. Nicht ihre Teile, nicht ihre Listener, nicht ihre Tabellen – das sind die
nächste Ebene. Wenn L0 mehr als einen Knoten hat, der innerhalb der Komponente
liegt, ist es keine Kontextansicht. Eine Containment bei L0 würde Interna
beanspruchen, die dem Leser noch nicht gezeigt wurden, und
focuswürde auf eine Ebene zeigen, die noch nicht existiert. - L1/L1b öffnen die Box von L0. Jedes Mitglied sitzt innerhalb des Containers; jedes Element, das auf L0 ein Externes war, sitzt außerhalb. Ein Mitglied, das nicht innerhalb der Begrenzungsbox der Box gezeichnet werden kann, erhält einen eigenen Containereintrag (siehe Kontiguität) – niemals einen gefälschten.
- L2/L2b bleiben innerhalb einer Box. Beide Endpunkte jeder L2-Beziehung sind Containermitglieder. Wenn eine Beziehung die Box verlässt, gehört sie auf L1/L1b.
- Eine Ebene beschreibt sich selbst. Titel ist kurz (
L0 · Systemkontext); die Beschreibung gibt an, was sich in der Box befindet, was außerhalb und wie sie sich zur benachbarten Ebene verhält. Ziel sind 2–3 umbrochene Zeilen. - Bewegung ist keine C4-Eigenschaft. Ebenen 0–1b sind statisch (
dots: 0). Eine interne Ebene, deren Beziehungen Live-Flüsse sind, darf genau einen Punkt pro Route tragen (flow: true, dots: 1); dokumentiere die Wahl. Animiere niemals einen externen Pfeil, um die Leiter zu dekorieren.
Aufteilungsdisziplin – Eine Ebene ist eine TIEFE, kein Thema
Eine C4-Ebene beantwortet eine Frage auf einem Zoom. Zwei Ebenen derselben Tiefe existieren nur, weil ein Abschnitt den Inhalt nicht aufnehmen konnte; ein neues Thema ist kein Grund, einen Abschnitt hinzuzufügen.
- Füge niemals L1c, L1d, L1e oder eine fünfte Seite hinzu. Die Leiter hat drei Tiefen (Kontext, Container, Komponenten) und höchstens zwei Slice-Bilder auf jeder Tiefe unterhalb der ersten. Eine Leinwand, die einen Slice pro Subsystem oder pro Erwähnung wächst, ist eine Themenliste, die Ebenentitel trägt.
- Ein zweiter Slice muss seine Messung nennen: die Routen, die
--checkverweigert, den gemeinsamen Korridor, den er meldet, oder die Ränge, die kollidieren (zwei externe Enden ohne gemeinsames Externes). „Es fühlte sich überladen an" ist keine. - Slice-Bilder derselben Tiefe teilen sich die Box, die Beschriftungen und die IDs. L1 und L1b zeichnen dieselbe Komponente mit denselben Mitglieds-IDs; L2 und L2b zeichnen dieselben Mitglieder. Nur die Kantenmenge und die Ränge unterscheiden sich, und jede Beschreibung sagt, welcher Slice es ist.
- Ein Slice wird
bgenannt (l1b,l2b); erfinde niemals eine Tiefe. - Tiefe über L2 hinaus wird nicht gezeichnet. Wenn Komponenteninterna immer noch zerfallen, sage dies im Protokoll und höre auf: Code-Klassen und Funktionen sind keine C4-Ebene, nach der ein Leser gefragt hat. Die Leiter endet dort, wo die Frage endet.
- Alles, was das Hauptbuch enthält und die Leiter nicht zeichnet, ist eine Umfangsentscheidung, schriftlich festgehalten (Schritt 6), keine stillschweigende Löschung und kein neuer Abschnitt.
Schritt 5 – Spezifikationsmechanik
Eine Leinwand, eine Leserichtung, ein Abschnitt pro Ebene:
{
"direction": "lr",
"theme": "gruvbox",
"diagrams": [
{
"id": "l0",
"title": "L0 · Systemkontext",
"description": "Systemkontext: Der Manager ist eine Box. ...",
"nodes": [ ... ],
"edges": [ ... ]
},
{
"id": "l1",
"title": "L1 · Der Manager",
"description": "Die Box von L0 eine Ebene geöffnet: ...",
"containers": [ { "label": "Maschinenverwaltung", "members": ["api", "q", "db"] } ],
"layers": [ ["akteur"], ["api", "q"], ["db"], ["prov"] ],
"nodes": [ ... ],
"edges": [ ... ]
}
]
}
direction: "lr"einmal, auf der Leinwand. Niemals eine Überschreibung pro Diagramm: Ein Leser, der sich bei jedem Abschnitt neu orientiert, hat die Leiter verloren.- Fünf Abschnitte.
l0,l1,l1b,l2,l2b, in dieser Reihenfolge. Ein sechster Abschnitt benötigt die in Schritt 3 gemessene Aufzeichnung; eine Leinwand mit zehn oder zwanzig Abschnitten ist eine Themenliste, keine Leiter – falte sie zurück auf drei Tiefen mit je zwei Slice-Bildern. containersist eine Liste pro Diagramm von{label, members}. Die Box ist ein echtes, doppelt umrandetes Rechteck, das um die Mitglieder gewachsen ist, mit der Beschriftung in die Oberkante eingefügt. Mehrere Boxen pro Diagramm sind erlaubt.- Mitglieds-IDs müssen in den
nodesdieses Diagramms vorhanden sein. Eine Mitglieds-ID, die keinen Knoten nennt, wird vom Parser stillschweigend fallen gelassen – die Box zeichnet dann um weniger Knoten herum, als du behauptet hast, und--checkwarnt nicht. Zähle die gezeichnete Box gegen die von dir declarierten Mitglieder. - Kontiguitätsregel. Ein Container ist ein Rechteck aus
rect, das vom Mittelpunkt des Containers aus gemessen wird. Wenn der durch die Mitglieder definierte Bereich den Container überläuft, ist er kein gültiger Container – die Mitglieder passen nicht hinein. Ordne die Mitglieder innerhalb eines Containers so an, dass der Container sie mit mindestens 10–20 Pixeln Abstand auf jeder Seite umschließt. Wenn die Mitglieder nicht in die Begrenzungsbox des Containers passen, teile den Container auf oder ordne die Mitglieder neu an; niemals members außerhalb der Box des Containers zeichnen.
Schritt 6 – Das Protokoll
Ein Generierungsprotokoll muss existieren. Es sollte enthalten:
- Die ID der Komponente, den Repo-Pfad und den Anker
- Die Anzahl der von jedem Scout gefundenen Elemente und Beziehungen
- Die Anzahl der gelösten und ungelösten Konflikte
- Die Anzahl der gezeichneten, gefalteten, weggelassenen (nach Klasse) und nicht berücksichtigten Beziehungen
- Falls zutreffend den Grund für die Aufteilung in Slice-Bilder (gemessen)
- Alle offenen Fragen oder bekannten Lücken
Schritt 7 – Abdeckungskarte (Bonus)
Wenn die Zeit es erlaubt, füge am Ende eine kurze Zusammenfassung hinzu:
Beziehungen: 34 gefunden, 18 gezeichnet, 6 gefaltet (C2), 8 weggelassen (2 C1,
3 C3, 3 C4), 2 nicht berücksichtigt (Scouts nicht ausgeführt)
Konflikte: 3 (alle gelöst: Manifest schlägt Kommentar)
Offene Fragen: 1 – wohin streamt `audit_logger`? Keine Aufrufstelle gefunden
Diese Karte ist die letzte Zeile des Protokolls. Ein fehlender Wert ist eine Lücke, die aufgezeichnet werden muss.