Erscheinungsbild
0018 — Geteilte Vertragsbeispiele, Allowlist auch ohne PHP-Handler
Status: angenommen · Milestone: Companion
Kontext
ADR 0009 hat den Vertrag auf eine Quelle gelegt und contract/form-submission.schema.json daraus erzeugt. ADR 0011 hat entschieden, dass die Joomla-Gegenstelle in einem eigenen Repository entsteht und dass die erzeugte Vertragsdatei „die einzige Schnittstelle zwischen zwei Repositories" ist, die sich sonst nicht kennen.
Beim Vorbereiten genau dieser Gegenstelle fielen zwei Lücken auf.
Die Vertragsdatei beschreibt die Form, aber nicht die Auslegung. Ob fields: [] ein gültiges leeres Feldobjekt ist, ob "title": null etwas anderes ist als ein fehlender Titel, ob 3.0 als ganze Zahl durchgeht — das steht im Schema, ist aber erst dann eindeutig beantwortet, wenn jemand es an Fällen durchspielt. Der PHP-Handler hatte diese Fälle: neunzehn Stück, als PHP-DataProvider in seinem eigenen Testverzeichnis. Ein zweites Backend hätte sie neu erfinden müssen — und „in beide Richtungen geprüft" hieße dann, dass jede Seite gegen ihre eigene Auslegung prüft.
Die Feld-Allowlist entstand nur dort, wo der PHP-Handler liegt. Sie wurde ausschließlich nach server/forms/config/forms.json geschrieben, und planInit() entfernt server/ in jedem Projekt, das den Handler nicht ausliefert. In einem Joomla-Projekt erzeugte pnpm forms:contract damit gar keine Allowlist, und der Snapshot-Test übersprang sich selbst. Wer im Joomla-Backend die Felder einträgt, hatte nichts, woraus er sie abschreiben konnte — außer der TypeScript-Datei.
Entscheidung
contract/examples/requests.json beschreibt die Fälle, von Hand gepflegt. Gültige und ungültige Rümpfe mit Name, Grund und erwartetem Fehlercode. Der PHP-Handler liest sie, statt sie zu bauen; die Joomla-Erweiterung liest dieselbe Datei; ein Vitest-Test hält sie an das Zod-Schema gebunden.
Die Allowlist entsteht zusätzlich als contract/forms.allowlist.json, in jedem Projekt. Die Fassung unter server/forms/config/forms.json bleibt, weil sie mit dem Handler auf den Server wandert. Beide entstehen aus demselben Bauer mit demselben Befehl.
Begründung
Warum die Beispiele nicht erzeugt werden. Welche Fälle einen Vertrag ausmachen, ist eine Aussage über Absicht, nicht über Struktur. Aus dem Schema ließe sich beliebig viel ableiten und nichts davon wäre die Frage, die jemand beantwortet haben wollte. Erzeugt wird, was mechanisch folgt; von Hand gepflegt wird, was jemand entschieden hat.
Warum zwei Allowlist-Dateien keine zweite Quelle sind. ADR 0009 verbietet die zweite handgepflegte Beschreibung, nicht die zweite Auslieferung. Quelle bleibt src/content/forms.ts; beide Dateien entstehen aus buildFormsAllowlist(), beide bewacht derselbe Test, und pnpm forms:contract schreibt beide. Auseinanderlaufen können sie nicht — und der Grund für die zweite ist kein Geschmack, sondern der Ort: contract/ wird nicht deployt, server/forms/ schon.
Warum die Allowlist auch dort entsteht, wo sie niemand automatisch liest. In einem Joomla-Projekt trägt ein Mensch die Felder im Backend ein. Genau deshalb braucht er eine erzeugte Vorlage: Sie ist im Pull Request als Diff sichtbar und sagt damit, dass das Gateway nachziehen muss. Ohne sie wäre die Feldänderung nur in einer TypeScript-Datei sichtbar, und der Abgleich fände im Kopf statt.
Verworfene Alternativen
Die Beispiele aus dem Schema erzeugen. Ergäbe syntaktisch gültige Rümpfe und keine einzige der Fragen, um die es geht — "title": null fiele nie auf.
Nur eine Allowlist-Datei, gelesen aus contract/. Hieße, das Deployment umzubauen, damit contract/ mit dem Handler auf den Server geht. Der Handler soll seine Konfiguration neben sich haben, nicht in einem Verzeichnis, das aus einem anderen Grund existiert.
Die Allowlist vom Gateway per URL abrufen lassen. Erspart die Handarbeit und bringt eine Abhängigkeit vom Betrieb der Website in ein Backend, das gerade dann funktionieren soll, wenn etwas anderes klemmt.
Konsequenzen
pnpm forms:contractschreibt drei Artefakte: Schema, Allowlist untercontract/und — sofern der Handler im Projekt liegt — dessen Fassung.- Ein neuer Vertragsfall gehört in
contract/examples/requests.json. Beide Backends erben ihn. - Die Beispieldatei trägt ihre Vertragsversion; ein Backend, dessen Version abweicht, bricht den Test ab, statt gegen fremde Fälle zu prüfen.
- Der Fehlercode steht am Fall. Alle heutigen Fälle sind
invalid_request— der Vertragsleser kennt die Allowlist nicht;validation_failedentsteht erst eine Stufe später.