Specs beschreiben das Was

In Spec-Driven Development beschreibt die Spezifikation, was das System tun muss, nicht wie es das tut. In der Praxis vermischen viele Specs beides.

Ein typisches Beispiel ist ein Use-Case-Schritt wie dieser: «Der Benutzer klickt im Vaadin Grid auf den Button Speichern, und der Service ruft das Repository auf.» Dieser Schritt vermischt fachliches Verhalten mit UI-Technologie und internem Design. Ein Fachexperte kann ihn nicht prüfen, und wenn man das UI-Framework oder die Persistenzschicht wechselt, ist die Spec falsch, obwohl das fachliche Verhalten gleich bleibt.

Ein guter Schritt sagt nur, was für das Business zählt: «Der Sachbearbeiter speichert die Bestellung. Das System prüft, dass das Lieferdatum in der Zukunft liegt.» Er bleibt gültig, wenn sich die Technologie ändert.

Das Wie aus der Spec wegzulassen heisst nicht, es aus dem Projekt wegzulassen. Wenn es niemand aufschreibt, entscheidet der KI-Agent, und er entscheidet vielleicht jedes Mal anders, wenn er den Code generiert.

Das Wie gehört in die Guidelines

Das Wie gehört in die Guidelines. Man schreibt sie einmal, und jeder Use Case verwendet sie.

Guidelines enthalten drei Dinge:

  • Coding- und Design-Regeln, zum Beispiel Namensgebung, Umgang mit null, wie man eine Vaadin-View schreibt, wie man eine jOOQ-Abfrage aufbaut.
  • Die generelle Architektur, zum Beispiel Schichten, Module, Self-Contained Systems, wie Systeme miteinander kommunizieren.
  • Architecture Decision Records (ADRs), die konkreten Entscheidungen mit ihren Gründen, zum Beispiel «wir verwenden jOOQ, nicht JPA».

Ich teile sie in zwei Ebenen auf. Firmenweite Guidelines sind in jedem Projekt gleich, so kann ein Team seine Spring-Boot-, Vaadin- und jOOQ-Regeln wiederverwenden. Projektspezifische Guidelines enthalten Entscheidungen, die nur für ein Projekt gelten, zum Beispiel welchen Payment-Provider wir aufrufen.

Eine Guideline ohne Sensor ist nur ein Wunsch

Guidelines aufzuschreiben reicht nicht. Ein KI-Agent hält sich meistens daran und driftet dann ab, zum Beispiel wenn der Kontext lang wird oder wenn seine Trainingsbeispiele in eine andere Richtung zeigen. Entwickler driften auch.

Darum braucht jede Guideline einen Sensor: eine automatische Prüfung, die fehlschlägt, wenn der Code die Regel verletzt.

Sensoren geben dem Agenten auch schnelles Feedback. Wenn der Build mit einer klaren Meldung fehlschlägt, korrigiert der Agent den Fehler im nächsten Schritt, bevor ein Mensch den Code reviewt. Die Meldung sollte darum sagen, was falsch ist und welche Regel verletzt wird.

Neal Ford, Rebecca Parsons und Patrick Kua nennen solche Prüfungen in ihrem Buch «Building Evolutionary Architectures» Fitness Functions. Wenn KI-Agenten den grössten Teil des Codes schreiben, braucht man sie für jede Guideline.

Beispiel: «Wir verwenden jOOQ, nicht JPA»

In meinen Projekten verwende ich jOOQ für den Datenbankzugriff, nicht JPA. Das ADR sieht so aus:

# ADR-003: jOOQ statt JPA verwenden

Status: Accepted

## Kontext
Die Applikation ist datenbankzentriert. Wir brauchen volle Kontrolle über SQL,
herstellerspezifische Features von Oracle und PostgreSQL und Typsicherheit zur Compile-Zeit.

## Entscheidung
Der gesamte Datenbankzugriff verwendet jOOQ mit Code, der aus dem Schema generiert wird.
JPA, Hibernate und Spring Data JPA werden nicht verwendet.

## Konsequenzen
Keine Lazy-Loading-Probleme und keine versteckten Queries.
Entwickler müssen SQL können.

## Verifikation
- Die Maven-Enforcer-Regel "adr-003-no-jpa" verbietet die JPA-Abhängigkeiten.
- Die ArchUnit-Regel "adr003NoJpa" verbietet die JPA-Packages im Code.

Ein KI-Agent, der einen neuen Use Case implementiert, hat Tausende Spring-Boot-Beispiele mit @Entity und JpaRepository gesehen. Ohne Sensor fügt er früher oder später spring-boot-starter-data-jpa hinzu. Der Build bleibt grün, und niemand merkt es.

Sensor 1: die Abhängigkeit verbieten

Der erste Sensor arbeitet auf Build-Ebene. Das Maven Enforcer Plugin lässt den Build fehlschlagen, wenn eine verbotene Abhängigkeit auftaucht, auch als transitive Abhängigkeit.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <executions>
        <execution>
            <id>adr-003-no-jpa</id>
            <goals>
                <goal>enforce</goal>
            </goals>
            <configuration>
                <rules>
                    <bannedDependencies>
                        <excludes>
                            <exclude>jakarta.persistence:jakarta.persistence-api</exclude>
                            <exclude>org.hibernate.orm:*</exclude>
                            <exclude>org.springframework.boot:spring-boot-starter-data-jpa</exclude>
                        </excludes>
                        <message>ADR-003: We use jOOQ, not JPA. See docs/architecture/decisions/ADR-003.md</message>
                    </bannedDependencies>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

Die Meldung nennt das ADR und seine Datei, so weiss der Agent, welche Regel er verletzt hat und wo er die Begründung findet.

Sensor 2: den Code verbieten

Der zweite Sensor arbeitet auf Code-Ebene. Ein ArchUnit-Test prüft, dass keine Klasse die JPA-Packages verwendet, auch wenn diese über eine andere Library in den Classpath kommen.

@AnalyzeClasses(packages = "ch.martinelli.demo",
        importOptions = ImportOption.DoNotIncludeTests.class)
class ArchitectureDecisionsTest {

    @ArchTest
    static final ArchRule adr003NoJpa = noClasses()
            .should().dependOnClassesThat()
            .resideInAnyPackage(
                    "jakarta.persistence..",
                    "org.hibernate..",
                    "org.springframework.data.jpa..")
            .because("ADR-003: we use jOOQ, not JPA");
}

Die because-Klausel erscheint in der Fehlermeldung.

Wenn der Agent jetzt JPA hinzufügt, schlägt der Build mit «ADR-003: We use jOOQ, not JPA» fehl. Der Agent liest das ADR und schreibt den Code mit jOOQ neu, ohne menschlichen Reviewer.

Daraus eine Gewohnheit machen

Jedes ADR und jede Guideline sollte einen Sensor haben. Zwei Regeln helfen.

1. Einen Abschnitt «Verifikation» im ADR-Template ergänzen. Er nennt den Sensor, und der Sensor trägt die ADR-ID in seinem Namen oder seiner Meldung. Das ergibt Traceability in beide Richtungen, so wie die Use-Case-ID Use Cases und Tests verbindet.

2. Den Sensor an den Lebenszyklus des ADR binden.

  • Wenn ein ADR akzeptiert wird, muss sein Sensor existieren.
  • Wenn ein ADR ersetzt wird, wird sein alter Sensor in derselben Änderung entfernt oder ersetzt. Sonst blockiert die alte Regel die neue Entscheidung, oder die neue Entscheidung ist nicht geschützt.
  • Wenn ein ADR veraltet ist, fällt sein Sensor weg.

Dasselbe funktioniert für die generelle Architektur. Modulgrenzen lassen sich mit Spring Modulith oder ArchUnit prüfen. «Keine gemeinsame Datenbank zwischen Self-Contained Systems» lässt sich mit separaten Datenbankbenutzern und Grants prüfen. Eine Anforderung an die Antwortzeit lässt sich mit einem Gatling- oder k6-Test mit Schwellwert prüfen. Und Coding-Regeln haben ihre üblichen Sensoren: Checkstyle, Error Prone, NullAway mit JSpecify, OpenRewrite-Rezepte.

Wenn jede Guideline ihren Sensor nennt, sieht man, welche Regeln geprüft werden und welche nicht. Die Guidelines ohne Sensor sind die To-do-Liste.

Wenn kein Tool es prüfen kann

Nicht jede Guideline passt in einen Compiler oder einen Test. «Verwende die Begriffe aus dem Glossar» oder «halte Views einfach» lassen sich mit einem Tool kaum prüfen.

Dafür kann ein KI-Reviewer ein weicherer Sensor sein: Ein zweiter Agent prüft das Resultat gegen genau eine Guideline. Er ist weniger zuverlässig als ein fehlschlagender Build, findet aber viele Probleme.

Manche ADRs haben nichts zu prüfen, zum Beispiel «Wir haben Vaadin gewählt, weil das Team Java kann». Dann steht im Abschnitt Verifikation «Nur Dokumentation», damit die Leser wissen, dass der Sensor nicht vergessen wurde.

Fazit

Zusammengefasst:

  1. Specs definieren das Was, in der Sprache des Business, damit Fachexperten sie prüfen können und sie Technologiewechsel überleben.
  2. Guidelines, inklusive genereller Architektur und ADRs, definieren das Wie, einmal für alle Use Cases.
  3. Sensoren prüfen, dass der Code den Guidelines folgt, und geben dem KI-Agenten schnelles Feedback.

Das JPA-Beispiel braucht ein ADR, eine Enforcer-Regel und einen ArchUnit-Test, verbunden durch die ADR-ID. Am besten beginnt man mit der wichtigsten Entscheidung und ergänzt die Sensoren Schritt für Schritt.