Teil 7 – Wo die Dark Factory das Licht anlässt – war der Schluss. Die Serie hat dort ihre Klammer zugemacht, und sie hat dir am Ende ein Versprechen mitgegeben: Der Käfig ist nie fertig, er wächst mit dem, was die Wirklichkeit ihm beibringt. Genau das ist dieser Teil. Ein Gate, das in den fertigen Bogen nachträglich eingefaltet wird, weil ich es erst danach scharf genug fassen konnte.
Erinnerst du dich an den Satz, an dem Teil 4 mit dem Achselzucken endete? Der Loop prüft die Treue zur Spec, nicht ob die Spec das Richtige will.
Ich habe ihn damals stehen lassen wie eine offene Tür. Jede Prüfung in dieser Serie, die Gates, der Critic, das Traceability-Gate, sie alle fragen am Ende dasselbe: Tut der Code, was die Spec sagt? Eine Frage haben sie nie gestellt. Sagt die Spec überhaupt das Richtige?
Über diese Tür reden wir jetzt. Sie lässt sich ein Stück weiter aufstoßen, als ich in Teil 7 zugegeben habe. Nicht ganz. Aber weiter.
Tests und Code haben eine gemeinsame Wurzel
Geh noch einmal zurück zu der Trennung aus Teil 4. Wer den Code schreibt, schreibt nicht die Tests. Wer die Tests ableitet, ist ein anderer Agent mit frischem Kontext als der, der implementiert. Gewaltenteilung, eine Ebene tiefer. Das fängt eine bestimmte Sorte Unheil: dass dieselbe Instanz ihren eigenen Bug doppelt einkodiert, im Code und im Test, und sich dann selbst grün abnickt. Test-Code-Kollusion, wie ich es in Teil 3 genannt habe.
Woher der Test-Agent sein „korrekt“ bezieht
Aber sieh genauer hin, woher der Test-Agent sein „korrekt“ bezieht. Er liest dieselbe Spec. Er übersetzt dasselbe Akzeptanzkriterium in eine Assertion. Steht im Kriterium ein Denkfehler, übersetzt der Test-Agent ihn brav mit. Der Code erfüllt den Test, der Test erfüllt die Spec, alles ist grün, und alle drei liegen am selben Punkt falsch.

Die Trennung von Producer und Critic trennt die Autoren. Sie trennt nicht die Quelle. Beide Seiten schöpfen ihr „so soll es sein“ aus derselben Spec. Und die Spec ist, wie Teil 2 gezeigt hat, das einzige Artefakt im ganzen System ohne Backstop. Sie wird nicht von einer unbeeinflussbaren Instanz geprüft, sondern vom Menschen, und ein müder Mensch übersieht im Kriterium genau das, was er später im Code übersehen hätte.
Die schärfste Validitäts-Frage ist nicht „stimmt der Code mit der Spec?“, sondern „misst die Prüfung das richtige Verhalten, oder nur Treue zu einer Lesart der Spec?“ Tests, selbst vom unabhängigen Agenten, leiten ihren Erwartungswert aus derselben Spec ab. Das ist die gemeinsame Wurzel, durch die ein falsch verstandener Intent ungefangen durchläuft.
Ein zweites Gutachten, mit anderer Methode
Was diese Wurzel kappt, ist kein schärferes Gate. Es ist eine zweite, unabhängig abgeleitete Quelle für „korrekt“. Ich weiß, dass es bei „zweite Quelle“ bei dir klingelt: Single Source of Truth, eine einzige Wahrheit, alles andere ist Drift. Für Daten stimmt das auch; für Korrektheit ist es eine Falle, denn eine einzige Quelle heißt hier, dass der Code mitbestimmt, woran er gemessen wird.
Aber ich will dich gleich vor dem zu schönen Bild bewahren. Der naheliegende Vergleich wäre der zweite Zeuge vor Gericht, der den ersten nie gesprochen hat. Das passt nicht ganz, und der Unterschied ist wichtig. Im Greenfield, beim Neubau, stammen Twin und Code aus derselben Quelle: deinem Team, deinem Domänenwissen. Es ist kein fremder Zeuge. Es ist eher ein zweites Gutachten desselben Sachverständigen, nur mit einer anderen Methode.
Dekorrelation, nicht Unabhängigkeit
Und genau die Methode trägt hier die Last. Der Code beschreibt das Verhalten prozedural, Schritt für Schritt. Der Twin beschreibt es deklarativ, als Invarianten, als Wahrheiten, die immer gelten müssen. Zwei verschiedene Formalismen für denselben Gedanken machen verschiedene Fehler wahrscheinlich. Wo beide übereinstimmen, ist die Wahrscheinlichkeit hoch, dass es stimmt. Wo sie auseinandergehen, steckt ein Defekt bei einem von beiden. Das ist keine Unabhängigkeit wie zwischen zwei Fremden. Es ist Dekorrelation, und das ist schwächer. Es ist aber genau dann etwas wert, soweit die beiden Ableitungen wirklich auseinanderlaufen.
Das ist der Zwilling, der Digital Twin: eine zweite Quelle der Wahrheit über „korrektes Verhalten“, gegen die der echte Code läuft. Eine Regel macht ihn überhaupt erst sinnvoll.
Der Twin muss aus einer anderen Ableitung stammen als der Code, den er prüft. Sonst ist die Kollusion nur dupliziert. Seine Unabhängigkeit ist von derselben Art wie die zwischen Producer und Critic, nur eine Ebene höher: nicht „wer schreibt die Tests“, sondern „woher kommt ‚korrekt‘“.
Zwei Dinge machen ihn anders als jeden Test, den du kennst.
- Sein Erwartungswert wird berechnet, nicht geschrieben. Ein klassischer Test sagt
expect(balance).toBe(20). Die 20 hat ein Mensch oder ein Agent hingeschrieben, aus seiner Lesart der Spec. Der Twin schreibt keine 20 hin. Er hält eine zweite, unabhängig gebaute Maschinerie daneben und vergleicht, was beide ausrechnen. - Und seine Eingaben werden generiert, nicht von Hand gewählt. Du suchst keine Testfälle mehr aus, von denen du hoffst, dass sie die interessanten Stellen treffen. Eine Maschine würfelt Tausende Operationsfolgen, auch die absurden, an die kein Mensch beim Tippen gedacht hätte, und vergleicht nach jedem Schritt.
Übereinstimmung zweier unabhängig abgeleiteter Beschreibungen des Verhaltens über Tausende Sequenzen ist ein komplementäres Korrektheits-Signal, eines anderer Art als Spec-Konformität, soweit die beiden Ableitungen voneinander unabhängig sind. Die Spec verliert damit nicht ihren Wert. Sie verliert nur einen Titel, den sie nie verdient hatte.
Spec = Intent-Anker, nicht Korrektheits-Orakel.
Die Spec sagt, was du willst. Das ist unverzichtbar, und kein Twin nimmt es ihr ab. Aber sie ist kein Beweis, dass der Code richtig ist, denn die Tests erben ihren Maßstab von ihr. Genau die Bewegung, die Teil 6 gegen die perfekte Verifikation gemacht hat, „nötig, nicht hinreichend“, dieselbe, die diese Serie bei Coverage längst fährt, gehört auch auf die Spec: notwendig für den Intent, nicht hinreichend für die Korrektheit.
Die meisten Erklärungen des Twins scheitern daran, dass sie ihn für eine Sache halten. Sein Wert hängt an der Topologie deines Projekts. Dieser Teil holt zum ersten Mal in der Serie die Frage in den Vordergrund, ob du etwas Neues baust oder Bestehendes erhältst. Greenfield oder Brownfield, und der Twin sieht in beiden Fällen anders aus, mal ein geschriebenes Modell, mal die Aufnahme des Laufenden.
Greenfield: das absichtlich dumme Referenzmodell
Du baust etwas Neues. Dann ist der Twin eine absichtlich triviale Parallel-Version dessen, was du baust. Für ein Wallet etwa ein In-Memory-Ledger in zwanzig Zeilen, das jeden Eintrag stumpf in eine Liste schiebt und den Kontostand als Summe aller Einträge zurückgibt.
Dieses Modell ist vertrauenswürdig, weil es dumm ist, nicht weil es verifiziert wäre. Es hat keine Datenbank, keine Nebenläufigkeit, keine Optimierung, keine der Stellen, an denen sich Bugs verstecken. Bei einem hinreichend einfachen Modell liest du es in einer Minute und siehst, dass es stimmt. Genau diese Durchschaubarkeit ist sein ganzer Wert. Sie ist allerdings kein Geschenk: Nicht jede Domäne hat ein Trivialmodell, das man noch durchschaut. Wird das „dumme“ Modell selbst kompliziert, ist der Vorteil weg.
Die vier Invarianten
Dazu kommen Invarianten, Wahrheiten über die Domäne, die immer gelten müssen, egal welche Folge von Operationen man würfelt:
- Der Kontostand ist immer die Summe der signierten Einträge.- Der Kontostand wird nie negativ.- Eine abgelehnte Buchung verändert den Stand um exakt nichts.- Eine Operation auf Konto A verändert nie den Stand von Konto B.
Sieh dir die zweite Zeile genau an. „Nie negativ“ folgt nicht aus „Stand gleich Summe“. Es ist eine eigene Geschäftsregel, eine Ablehnungsregel, die das triviale Modell mittragen muss: Eine Buchung, die den Stand unter null drücken würde, lehnt es ab. Das Modell ist dumm, aber nicht regelfrei. Es trägt genau die Domänen-Wahrheiten, gegen die geprüft wird.
Property-based Testing: die Maschinerie dahinter
Die Maschinerie dahinter ist nicht neu, sie heißt property-based testing, genauer model-based testing. Ein Werkzeug wie fast-check würfelt Tausende Operationsfolgen, fährt sie parallel durch das echte System und das dumme Modell, vergleicht nach jedem Schritt, und wenn es eine Abweichung findet, schrumpft es die auslösende Folge zusammen. Statt „irgendwo in 800 Schritten stimmt was nicht“ bekommst du ein kleines Gegenbeispiel, etwa „genau diese drei Operationen brechen die Invariante“. Klein, nicht garantiert das kleinstmögliche: Das Schrumpfen braucht einen deterministischen Lauf und findet nur ein lokal-minimales Beispiel.
Was leistet der Greenfield-Twin, und was nicht?
Er härtet die Implementierung. Wenn dein echter, komplizierter Code von dem dummen Modell abweicht, hast du mit hoher Wahrscheinlichkeit einen Bug im Code gefunden, einen, den keine von Hand gewählte Testreihe getroffen hätte. Was er nicht leistet: Er rettet den Intent nicht.
Und eine Ehrlichkeit gehört direkt hierher, nicht erst ans Ende: Der Twin ist selbst ein Stück Code, das du prüfen musst. Schreibst du die Invarianten zu lax, prüft das Orakel fast nichts, und du hast dir ein assert(true) eine Ebene höher gebaut. Der Punkt, an dem ein Mensch das Orakel abnickt, erbt damit genau die Schwäche des Spec-Reviews aus Teil 2. Ein durchschaubares Modell verkleinert diese Schwäche, es hebt sie nicht auf.
Brownfield: die Aufnahme ersetzt den Verhaltensteil der Spec
Jetzt der andere Fall, und der ist der radikalere. Du schreibst ein bestehendes System neu, ohne sein Verhalten zu ändern. Eine Migration, ein Rewrite, ein Sprachwechsel.
Hier brauchst du kein geschriebenes Modell. Das laufende Altsystem ist das Orakel. Du nimmst seinen echten Verkehr auf, an der Grenze. Das ist die Idee hinter Michael Feathers’ Charakterisierungs-Tests: nicht festschreiben, was das System tun soll, sondern aufnehmen, was es tut. Anfragen rein, Antworten und Events raus. Dann spielst du dieselben Eingaben gegen das neue System. Stimmen die Ausgaben überein, ist das Verhalten erhalten.
Feathers selbst bringt beides auf einen Satz, die Aufnahme-ist-Spec-These und die Disziplin dahinter:
„When a system goes into production, in a way, it becomes its own specification. We need to know when we are changing existing behavior regardless of whether we think it’s right or not.“
Das laufende System wird seine eigene Spezifikation, genau das ist die Aufnahme-ist-Spec-These. Und der zweite Halbsatz ist die Charakterisierungs-Disziplin in Reinform: Du hältst das Verhalten fest, egal ob es dir richtig vorkommt, inklusive der Bugs, die das Altsystem mitschleppt. Eine Korrektur ist dann kein Versehen mehr, sondern ein bewusstes Delta.
Der Satz, der die Tür aus Teil 4 ein Stück weiter aufstößt: Du schreibst keine Spec für das Verhalten des Bestehenden. Du nimmst es auf.
Das ist die handfeste Form von „setz nicht mehr auf die geschriebene Spec“. Für alles, was gleich bleiben soll, ersetzt die Aufnahme den Verhaltens-Teil der Spezifikation, das „was kommt bei welcher Eingabe heraus“. Nicht die ganze Spec, und nicht unfehlbar. Die von Hand geschriebene Spec schrumpft auf die Deltas, die paar Stellen, an denen du das Verhalten absichtlich ändern willst. Und genau dort, an den Deltas, sitzt der menschliche Intent.
Was leistet der Greenfield-Twin, und was nicht?
Im Brownfield-Erhalt fällt eine Grenze, die sonst hält. Wenn dein Intent „verhalte dich wie bisher“ lautet, dann ist Intent gleich Parität mit dem Bestand, und Parität ist maschinell prüfbar. Hier, nur hier, wird sogar die Intent-Frage zu einem Gate, inklusive der stillen Abweichung, dem Intent-Drift. Für absichtliche Änderungen bist du wieder beim Menschen, aber der hat jetzt nur noch die Deltas vor sich, nicht das ganze System.
Leitplanken gegen die Naivität
Es gibt Leitplanken, sonst wird das naiv. Und die wichtigste ist selbst Handarbeit: Die Aufnahme konserviert auch die alten Bugs. Welcher davon ein absichtlich mitgenommenes Verhalten ist und welcher ein echter Fehler, den niemand erben will, entscheidet kein Werkzeug. Halte die zwei Ebenen sauber auseinander: Ist einmal aufgenommen, welches Verhalten gelten soll, dann prüft die Maschine die Parität gegen diese Aufnahme verlässlich und unbestechlich. Fehlbar bleibt nur der Schritt davor, die menschliche Kuration, welches Verhalten überhaupt aufgenommen und welcher alte Bug maskiert wird. Das ist eine menschlich kuratierte Liste, fehlbar wie jede. Markierst du einen Bug falsch, kopierst du ihn entweder blind mit oder meldest ihn fälschlich als Regression. Diese Maskierungs-Liste gehört darum selbst in den geschützten Satz, unter CODEOWNERS, reviewpflichtig wie die Spec.
Und „deckungsgleich“ ist nicht gratis: Aufnahmen stecken voller Zeitstempel, IDs und Reihenfolge, du musst definieren, welche Felder exakt stimmen müssen und welche variieren dürfen. Zu streng, und das Gate wird zu rotem Rauschen, dem keiner mehr traut. Zu lose, und es verschläft echte Regressionen. Das ist dieselbe Spannung zwischen „prüft die Wahrheit“ und „prüft am Ende nichts“, die Teil 3 schon am Mutation Testing hatte, eine Ebene höher.
Der Twin gehört in den Käfig, nicht daneben
Du ahnst schon, wo er strukturell sitzt. Im geschützten Satz aus Teil 3.
Das Orakel, also das Modell, die Invarianten oder die Aufnahme, muss für den produzierenden Agenten unerreichbar sein. Sonst passiert das Naheliegende und Fatale: Findet der Twin eine Divergenz, „behebt“ der Agent sie auf dem billigsten Weg, indem er das Orakel anfasst statt den Code. Er biegt das Modell zurecht, bis es zu seinem Bug passt. Dann sind wieder beide einig und beide falsch, nur diesmal mit einem Theater aus Unabhängigkeit davor.
Also lebt der Twin in einem eigenen, in CODEOWNERS genannten Pfad, und ein Pull Request, der ihn anfasst, ist ein sichtbarer Alarm, kein stiller Diff. Das ist exakt die Regel aus Teil 4: Ändert der Agent ein Gate statt den Code, ist das ein Alarm, kein Fortschritt.
Und der Twin ist auch keine Dublette zu den Gates, die du schon hast. Der Mutation-Ratchet aus Teil 3 härtet die Tests eines Codestücks. Der Twin härtet das Verhalten des ganzen Pfads gegen eine unabhängige Quelle. Das eine prüft, ob deine Tests zupacken. Das andere prüft, ob „korrekt“ überhaupt korrekt definiert war. Sie ergänzen sich, sie ersetzen sich nicht.
Hier muss ich einen scheinbaren Widerspruch zu Teil 7 ausräumen, bevor er stört. Dort hieß es: Den Rest schließt du nicht, indem du noch einen Prüfer obendrauf stapelst, denn eine Schicht, die mit den vorigen korreliert, liefert fast kein neues Signal. Der Twin ist kein solcher Aufstapel. Er ist gerade die dekorrelierte Quelle, die anders ableitet als alles darunter, nicht der korrelierte Prüfer, den Teil 7 verwirft. Und er ist auch nicht der letzte Anker. Der letzte Anker bleibt menschlich, daran ändert der Twin nichts.
Bleibt die alte Frage aus Teil 3: Wer urteilt, wenn der Twin rot wird? Eine Divergenz heißt „untersuchen“, nie automatisch „Code kaputt“. Meistens hat das dumme Modell recht und der schlaue Code unrecht. Manchmal aber hat der Twin selbst einen Bug. Dann fixt ihn ein Mensch, weil er im geschützten Satz liegt. Die Maxime dahinter ist bequem und streng zugleich: Faul beim Tippen, wach beim Urteil. Du tauschst „jeden Diff lesen, für immer“ gegen „ein Orakel pro Kontext, einmal sorgfältig abnicken“.
Ein Betrieb beschreibt etwas Verwandtes
Bis hier klingt das nach Theorie, die noch niemand unter Last gesehen hat. Für den berechneten Twin, das spec-unabhängige Orakel, gibt es zwei Belege von zweierlei Art. Den eigenen, ersten Lauf zeige ich weiter unten an echtem Code. Davor steht ein verwandter Baustein aus der Praxis eines fremden Betriebs.
StrongDM, ein Unternehmen für Zugriffs-Infrastruktur, baut Software nach zwei Regeln: Code wird nicht von Menschen geschrieben, und Code wird nicht von Menschen reviewt. In Johanns „Dark Factories“-Vortrag ist es das Team, an dem die ganze These hängt: Genau diese „Stufe fünf“, auf der kein Mensch mehr Code schreibt oder reviewt, sei „bislang weltweit öffentlich exakt einmal dokumentiert“, eben von StrongDM. Wichtig: Was öffentlich vorliegt, ist die Eigendarstellung des Anbieters, kein kontrollierter Versuch, den jemand unabhängig nachgemessen hätte. So beschreiben es StrongDM und der Vortrag; eine unabhängige Einordnung dazu liefert Simon Willison.
Und ein Detail daraus trifft genau eine der Säulen aus dieser Serie. Sie speichern ihre Eval-Szenarien getrennt vom Code, außerhalb der Codebase, damit der bauende Agent sie beim Bauen gar nicht sieht. Das ist, wörtlich, der Holdout aus dem Machine Learning, der zurückgehaltene Datensatz, gegen den man am Ende prüft, damit das Modell nicht auf die Prüfung hin optimiert. Es ist der geschützte Satz aus Teil 3, konsequent zu Ende gedacht: Das Prüfkriterium ist für den Produzenten nicht nur unschreibbar, es ist während des Bauens nicht einmal sichtbar. Das ist der EINE Gedanke, den ich aus StrongDM mitnehme.
Der engste Verwandte des berechneten Twins
Derselbe Vortrag liefert gleich den engsten Verwandten des berechneten Twins, den ich kenne. Johann beschreibt eine eigene Fabrik, in der pro Feature ein digitaler Zwilling steht, aus dem Intent gebaut, nicht aus dem fertigen Artefakt. Die Validierung wird aus dem Zwilling generiert, nicht aus dem, was der Agent gerade gebaut hat. Sein Satz trifft wörtlich die These dieses Teils: der Agent prüft nie gegen sein eigenes Werk, sondern gegen ein Modell, das er nicht angefasst hat. Und dieses Modell ist bewusst kein zweites LLM, sondern deterministisch: Ein zweites Sprachmodell teilte dieselbe Fehlerklasse und korrelierte mit dem ersten, der Twin dagegen ist ein triviales, durchschaubares Referenzmodell, das anders ableitet, statt dasselbe noch einmal zu raten. Das ist die Trennung „woher kommt korrekt“, einen Schritt weiter gedacht als StrongDMs Holdout, als Konzept beschrieben, nicht als nachgemessener Lauf.
Ein Detail räumt noch einen Kurzschluss ab. StrongDM fährt auch ein „Digital Twin Universe“, aber das ist etwas anderes: nachgebaute Versionen ihrer Abhängigkeiten wie Okta, Slack oder Jira, gegen die sie testen, ohne die echten Systeme anzufassen. Das ist Dependency-Sandboxing, nicht das berechnete Verhaltens-Orakel dieses Teils. Der Name kollidiert, die Sache nicht.
Was sich an StrongDM ablesen lässt, vorsichtig gelesen: „Kein Mensch schreibt oder reviewt Code“ heißt nicht „keine Spec“. Der Mensch bleibt beim Intent, bei der Spec und bei den Szenarien; nur Schreiben und Prüfen des Codes ist ihm entzogen. Und der Erfolg hängt an der unabhängigen Verifikation, nicht am Spec-Abarbeiten: Die These des Vortrags lautet „Verifikation schlägt Spezifikation“, das Prüfkriterium liegt außerhalb der Codebase, damit der Agent nicht dagegen optimieren kann. Genau unsere Aufteilung.
So gelesen ist StrongDM kein Beweis für den berechneten Twin, aber ein Bericht, der eine Säule dieser Serie in der Praxis plausibel macht: Spec-Intent menschlich, das Prüfkriterium unabhängig und unsichtbar gehalten.
Alle Teile aus dieser Serie
Teil 1 · 11 Min.Warum bessere Prompts deinen KI-Code nicht retten
Teil 2 · 12 Min.Spec-Driven Development, oder: EARS vs. Vibe Coding
Teil 3 · 16 Min.Mutation Testing bei KI-Agenten: Wer hält das Schloss?
Teil 4 · 12 Min.Merge Queue für KI-Agenten
Teil 5 · 17 Min.Blast Radius – was dein Agent anrichtet, wenn er nicht mehr dir gehört
Teil 6 · 16 Min.Die Risiko-Staffel gegen den Kontrollverlust beim Agenten
Teil 7 · 14 Min.Wo die Dark Factory das Licht anlässt
Teil 8 · 23 Min. · Du liest gerade100 % Mutation Score und trotzdem kaputt: Der Digital Twin
Die ehrliche Grenze
Und jetzt der Teil, den ich mir bei dieser Serie nie verkneife. Was kann der Twin nicht?
Im Greenfield rettet er den Intent nicht. Wenn deine Spec das Falsche will, und du beim Bauen des Twins demselben Denkfehler aufsitzt wie beim Schreiben der Spec, dann kodiert der Twin den Irrtum brav mit, und alle Instanzen geben grün. Zwei Darstellungen desselben Gedankens, einmal als prozedurale Spec, einmal als deklarative Invarianten, senken nur die Wahrscheinlichkeit, dass dir dein eigener Fehler auffällt. Das ist Dekorrelation, kein Beweis, derselbe Vorbehalt, mit dem dieser Teil schon angefangen hat. Es ist eine probabilistische Verbesserung an einer Aufgabe, die im Kern menschlich bleibt.
Teil 7 hat den uneliminierbaren Rest auf drei Wurzeln zurückgeführt: die gemeinsame Quelle von Test und Code, die unbekannten Fehler, und den Intent, der das Falsche wollen kann. Der Twin greift genau eine davon, die gemeinsame Quelle, und kappt sie. Die unbekannten Fehler und den Intent lässt er unberührt. Er schrumpft den Rest also, er deckt ihn nicht. Was er kann, ist trotzdem beachtlich: Er fängt Implementierungsfehler, an die niemand gedacht hat, und im Brownfield-Erhalt macht er sogar die Intent-Frage maschinell prüfbar, weil Intent dort schlicht Parität heißt. Was er nicht kann: dir die Frage abnehmen, ob das Gewollte das Richtige war. Diese Frage liegt außerhalb jedes Systems, gegen das man prüfen kann. Sie bleibt dein Platz, der aus Teil 7.
Und manchmal lohnt er gar nicht. Wie jedes Werkzeug in dieser Serie hat auch der Twin seinen Preis, und es gibt Stellen, an denen er sich nicht rechnet. Eine triviale CRUD-Domäne, die nur Felder hin- und herschiebt, hat kaum eine Invariante, gegen die ein Orakel etwas prüfen könnte. Code ohne klare Geschäftsregeln gibt dem Twin nichts zu greifen. Wegwerf-Code, der nächste Woche gelöscht ist, ist die Mühe nicht wert. Und der härteste Fall, der mich selbst noch umtreibt: Wenn das „dumme“ Modell so kompliziert würde wie der Code, den es prüfen soll, ist der Durchschaubarkeits-Vorteil weg, und du hast nur ein zweites, ebenso fehlbares System gebaut. Wo genau diese Schwelle liegt, ab der das Trivialmodell nicht mehr trivial bleibt, weiß ich noch nicht. Diese Frage lasse ich hier offen stehen, statt sie wegzumoderieren.
Der Twin ist also keine Wende, sondern eine Vollendung. Er ist ein weiteres nicht-korrumpierbares Gate, eine Ebene höher als alle anderen, und das menschliche Abnicken des Orakels ist ein weiterer Ausdruck der Intent-Ownership aus Teil 2. Er rastet in die Drei-Säulen-These dieser Serie ein, statt sie umzustoßen.
Der erste eigene Lauf: so lief der Obol-Twin
Du kennst die Disziplin dieser Serie inzwischen. Ich erzähle von Dingen, wenn sie passiert sind, nicht vorher. Teil 3 und Teil 4 haben sich daran gehalten. Hier ist der Punkt, an dem das kippt: nicht mehr „kommt, wenn es gelaufen ist“, sondern „so lief es“.
In Obol, meinem Wallet-Referenz-Repo, steht jetzt der erste Greenfield-Twin: das dumme In-Memory-Ledger als Referenzmodell, die Invarianten von oben (Summen-Identität, nie negativ, Konto-Isolation), ein fast-check-Lauf gegen den echten Wallet-Service mit echtem Postgres dahinter, und ein eigener Pflicht-Job twin in der CI mit Schwelle null Divergenz. Das Orakel lebt in einem eigenen, per CODEOWNERS geschützten Pfad unter services/wallet-service/twin/, damit ein PR, der es anfasst, sofort auffällt.
Genau das „mit echtem Postgres dahinter“ ist die heikelste Stelle, und sie ist eine Vorbedingung, kein Detail. Der Twin würfelt Tausende Sequenzen und vergleicht nach jedem Schritt. Trägt das echte System dabei Zustand von einem Lauf in den nächsten, weil die Datenbank nicht sauber zurückgesetzt wird, dann meldet der Vergleich falsche Divergenzen, und du hast kein Signal, sondern Rauschen. Im Obol-Lauf löst das ein Container für den ganzen Lauf mit einem TRUNCATE pro Sequenz, plus deterministischer Lauf. Erst das macht eine rote Meldung überhaupt deutbar und das Shrinking auf ein kleines Gegenbeispiel verlässlich.
Der Lackmus, und warum er erst grün war
Der Lackmustest ist derselbe wie bei jedem Gate in Teil 3: Erst wenn ich einen Defekt absichtlich einbaue und der Twin daraufhin rot wird, glaube ich ihm. Also habe ich die Deckungsprüfung im Spend-Handler von „kleiner oder gleich“ auf „kleiner“ verschoben, den Guard > auf >=. Der lehnt das Ausgeben des vollen Saldos fälschlich ab, obwohl REQ-SPD-02 Gleichheit erlaubt.
Womit ich nicht gerechnet hatte: Der erste Lauf blieb grün. Der Bug sitzt genau auf der einen Linie, auf der Betrag gleich Saldo gilt, und die trifft mein Generator praktisch nie. Nicht, weil sie mathematisch „Maß null“ hätte, fast-check würfelt keine Kommazahlen über ein Kontinuum, sondern ganze Beträge mit Vorliebe für Ränder wie 0 und 1. Sondern weil meine Generatoren Beträge und Saldo getrennt würfeln: Dass zwei so gezogene Werte in derselben Sequenz exakt gleich landen, kommt bei den gewählten Wertebereichen kaum je vor. Ein Property-Test ist nur so gut wie sein Eingaberaum, und meiner traf die eine Stelle, an der es darauf ankam, nicht. Die Korrektur war keine neue Invariante, sondern eine Operation, die die Grenze gezielt trifft: eine spend("all")-Variante, die zur Laufzeit exakt den aktuellen Saldo ausgibt. Danach wurde der Twin sofort rot und schrumpfte das Gegenbeispiel auf das Minimum zusammen:
Counterexample: [ open(owner="owner-a"), topUp(amount=1), spend(amount=1) ] spend(amount=1) // "all" = der gesamte aktuelle Saldo: status divergence, model expected 200, system returned 409
topUp(1); spend(1), das Modell erwartet 200 und Saldo 0, das echte System antwortet 409. Bug zurückgerollt, Gate wieder grün. Der Twin beißt, sobald sein Eingaberaum die richtige Stelle trifft.
Der eigentliche Beleg: ein Bug, den alle anderen Gates durchlassen
Der injizierte Off-by-one ist nur das Aufwärmen. Der eigentliche Beleg ist ein zweiter Fehler, einer, den ein Agent völlig plausibel als Optimierung schreiben würde. Statt den Saldo nach jeder Buchung frisch aus den Einträgen zu projizieren, wie REQ-SPD-05 es verlangt, ein heißer Saldo-Cache pro Konto: beim Spend dekrementiert, vom GET /balance bedient, schnell. Nur eben bei einem abgelehnten Spend nicht sauber zurückgerollt.
Das Entscheidende an diesem Defekt: Er ist grün an allen anderen Gates. Nicht behauptet, gemessen, mit eingebautem Bug:
| Gate | Ergebnis mit Bug |
|---|---|
typecheck (tsc -b) | 🟢 |
| lint (biome) | 🟢 |
| arch (dependency-cruiser) | 🟢 |
| trace (Spec zu Test) | 🟢, 55 von 55 |
| test und coverage (105 Tests) | 🟢, Coverage 97,87 %, handlers.ts 100 % stmts / 96,96 % branch |
mutation (Stryker auf projectBalance) | 🟢, 100 % |
Sieh dir diese Spalte an. Typecheck, Lint, Architektur, das Traceability-Gate aus Teil 5 grün über alle 55 Kriterien, 105 Tests mit fast 98 Prozent Coverage, und eine Mutation-Score von 100 Prozent. Jedes Gate, das diese Serie über sieben Teile aufgebaut hat, nickt diesen Code ab. Warum entkommt der Bug ihnen allen? Die Beispieltests prüfen „Spend über Budget gibt 409“ und den Happy-Path-Saldo, aber nie den Saldo nach einer Ablehnung. Die Mutation läuft, wie in Teil 3 etabliert, auf der reinen Projektion projectBalance, und die ist unberührt und korrekt, also tötet sie dort nichts; ein Cache-Bug im Handler liegt damit per Konstruktion außerhalb ihrer Reichweite. Und die Coverage ist erfüllt, weil der Cache-Code brav auf dem Happy-Path mitläuft. Jedes dieser Gates fragt „tut der Code, was die Tests sagen“, und die Tests behaupten diesen einen Schritt nie.
Der Twin fragt etwas anderes. Er liest nach jedem Schritt einen unabhängigen Saldo, auch nach einem abgelehnten Spend, und vergleicht ihn mit dem berechneten Soll. Real, rot, auf das absolute Minimum geschrumpft:
Counterexample: [ open(owner="owner-a"), spend(amount=1) ] [REQ-TOP-04][REQ-SPD-05][REQ-BAL-01][REQ-SPD-02] I1/I4 divergence: model balance=0, independent boundary read=-1
Ein einziger abgelehnter Spend auf ein leeres Konto. Der Status ist 409 und damit korrekt, aber der unabhängig gelesene Saldo steht auf minus 1 statt null: Der Cache wurde vorab dekrementiert und auf dem Reject-Pfad nie zurückgerollt. Das ist sogar minimaler als die im Design skizzierte Sequenz topUp(1); spend(2). fast-check fand, dass schon open; spend(1) genügt. Und die Fehlermeldung benennt die gebrochenen Invarianten per REQ-ID, statt dich mit „irgendwo stimmt was nicht“ allein zu lassen. Der Defekt sitzt an einem Schritt, den kein einziger Beispieltest behauptet, und nur ein berechnetes Soll fällt darüber.
CI-Zeit und Naht
Die zweite offene Frage war, ob das überhaupt in tragbarer Zeit läuft. Ein Lauf mit 100 gewürfelten Sequenzen, jede mit unabhängigem Read nach jedem Schritt, dauert lokal rund 7 Sekunden gegen echtes Postgres: ein Container für den ganzen Lauf, ein TRUNCATE pro Sequenz. Die Beispiel-Suite fährt ohnehin schon Testcontainers, der Twin ist nur ein weiterer Job derselben Klasse. Für einen tiefen Lauf gibt es TWIN_RUNS=1000. Die Sorge aus dem Konzeptteil, der Lauf gegen echtes Postgres könnte zu schwer für jeden PR werden, hat sich für Obol schlicht nicht bestätigt.
Bleibt die letzte Frage: Lässt sich aus diesem einen Twin eine wiederverwendbare Naht ziehen, ohne aus einem einzigen Beispiel falsch zu verallgemeinern? Die Naht ist sauber gezogen. Modell, Adapter und Orakel sind getrennte, interface-förmige Module hinter einem spi.ts. Generisch werden später nur der Runner, die Testcontainers-Orchestrierung, der Default-Matcher und die Gate-Verdrahtung. Projekt-lokal bleiben immer das Referenzmodell, die Operationen und die Invarianten, denn ein generisches Modell wäre eine generische Spec und damit kein unabhängiges Orakel mehr. Den generischen Harness extrahiere ich aus dem zweiten und dritten realen Nutzer, nicht aus Obol allein. Das ist die Rule of Three, dieselbe Geduld wie bei jedem anderen Werkzeug in dieser Serie.
Was du mitnimmst
Die offene Tür aus Teil 4 lässt sich weiter aufstoßen, als ich dort zugegeben habe. „Der Loop prüft die Treue zur Spec, nicht ob die Spec das Richtige will“ stimmt, solange die Spec die einzige Quelle von „korrekt“ ist. Der Twin entfernt sie als Quelle, ohne sie als Intent-Anker abzuschaffen.
Faustregel 8, eine Ebene höher (Korollar): Verlass dich für „korrekt“ nie auf eine einzige Quelle. Faustregel 8 sagte: Der grüne Haken ist eine Behauptung, kein Beweis, bis ihn eine vom Autor unbeeinflussbare Instanz bestätigt. Hier die Stufe darüber: Lass eine zweite Wahrheit, unabhängig abgeleitet, über deinen Code wachen, mit berechnetem statt geschriebenem Erwartungswert, über generierte statt von Hand gewählte Eingaben. Im Greenfield härtet sie die Implementierung. Im Brownfield-Erhalt ersetzt sie den Verhaltens-Teil der Spec.
Und die Grenze bleibt. Der Zwilling macht deinen Code nicht korrekt, er macht ihn verifizierbarer, gegen eine Wahrheit, die der Autor nicht geschrieben hat. Den letzten Rest, die Frage, ob das Gewollte das Richtige war, nimmt dir auch der beste Zwilling nicht ab. Den verantwortest du selbst, an dem Platz, der seit Teil 7 dir gehört.


Schreibe einen Kommentar