ADR-0015: Das Zählwerk trägt Ergebnisse ein¶
- Status: accepted
- Datum: 2026-09-08
- Betrifft: Schnittstellen, Datenmodell, Messung, Betrieb
- Bezug: führt
0014-kiosk-benennt-wer-eintraegtfort — das Zählwerk ist eine weitere Maschine ohne Identität, und die Antwort darauf ist dieselbe. Verbraucht die Invariante „keine JSON-APIs, solange kein externer Konsument existiert" ausCLAUDE.md, weil es ab hier einen gibt.
Kontext¶
zaehlwerk zählt das laufende Spiel an der Platte: Punkte von Tastern, Piezos
oder einem Telefon, Satzlogik, Aufschlagwechsel, Undo. Sein ADR-0001 sieht die
Übergabe hierher von Anfang an vor — „Schmetterpause keeps players, TTR,
tournaments and history, and receives a finished result at match end" — und
scorer.State.CompletedSets trägt dort den Kommentar, dass es für genau diesen
Zweck existiert. Gebaut ist die Übergabe nie worden.
Zwei Richtungen fehlen. Das Zählwerk kennt Spieler nur als Freitext-Anzeigenamen und braucht die echte Liste, damit ein Ergebnis überhaupt jemandem gehören kann. Und das fertige Ergebnis muss hierher.
Beides scheitert heute an derselben Stelle: diese Anwendung gibt nirgends JSON
aus. Kein Handler in internal/server/ ruft json.Marshal oder
json.NewEncoder auf, und das ist kein Versehen, sondern die Invariante. Sie
gilt aber ausdrücklich nur, „solange kein externer Konsument existiert".
Naheliegend wäre, den Kiosk wiederzuverwenden. POST /kiosk/matches tut fast
das Richtige: es trägt für zwei Spieler ein, die beide nicht der Eintragende
sind. Zwei Dinge passen nicht. Der Kiosk authentifiziert über Freischaltung,
Cookie und Grant — ein Sitzungstanz, der für einen Browser gedacht ist, an dem
abends jemand steht. Und scoring.Record ruft immer settle: ein Kiosk-Ergebnis
zählt sofort, ohne dass jemand zustimmt. Für eine Testphase ist das die falsche
Voreinstellung.
Entscheidung¶
Eine schmale JSON-Fläche für das Zählwerk, mit denselben Regeln, die für den Kiosk gelten, und einem Ergebnis, das zunächst wartet.
-
GET /api/playersgibtid,display_nameundttr. Gelesen, nicht gespiegelt: das Zählwerk hält keine Kopie, keinen Cache und gleicht nichts ab. Diese Anwendung bleibt der einzige Eigentümer der Spieleridentität. -
POST /api/resultsträgt ein Ergebnis ein, authentifiziert über ein eigenes Token statt über den Kiosk-Cookie. Ohne gesetztes Token gibt es die Routen nicht, so wie es den Kiosk ohneSP_KIOSK_TOKENnicht gibt. -
Der Operator ist Pflicht, wie an jedem schreibenden Kiosk-Weg. Ein Aufruf ohne benannten Operator wird abgelehnt;
reported_byist der Operator; der Operator darf nicht mitspielen. ADR-0014 begründet das, und die Begründung gilt hier unverändert — eine Maschine hat keine Identität, also benennt sie den Menschen, der zusieht und zählt. -
Das Ergebnis bleibt
pending. Nichtscoring.Record, sondernMatches().Create(…, Status: domain.MatchPending)wie der Spieler-Pfad. Weil der Melder ein Dritter ist, dürfen beide Spieler bestätigen:loadverlangt Teilnehmer und nicht-Melder, und das sind hier zwei statt einem. -
entered_viabekommt den Wertscoreboard, per vorwärtsgerichteter Migration samt erweitertem CHECK-Constraint. -
Die Definition of Done zählt diese Zeilen nicht ins Verdikt, sondern zeigt sie als eigene Spalte neben
of which kioskundof which tournament.
Warum nicht anders¶
Warum nicht sofort werten, wie am Kiosk. Die Testphase soll zeigen, dass die
Kette Taster → Zählwerk → Datenbank das Richtige einträgt. Solange das offen
ist, ist ein Ergebnis, dem ein Mensch zugestimmt hat, mehr wert als eines, das
schon in der Rangliste steht. Der Umstieg später ist der Wechsel von
Create(pending) auf scoring.Record — beide existieren, es ist keine
Umstellung, sondern eine andere Zeile.
Warum die Zeilen nicht als player schreiben. Das spart die Migration und
kostet die Herkunft, und die lässt sich nachträglich nicht rekonstruieren. Genau
diese Lücke hat Issue #71 geschlossen; sie wieder aufzureißen, um eine Migration
zu sparen, wäre ein schlechter Tausch.
Warum sie nicht ins Verdikt zählen. Die Messung fragt, ob Leute ihre
Ergebnisse freiwillig und unerinnert eintragen. Ein Zählwerk-Match ist
strukturell der Kiosk-Fall: eine Person zählt für andere. Entscheidend ist die
zweite Kennzahl — reporters ist count(distinct m.reported_by), und mit dem
Operator als Melder hat ein ganzer Abend an der Platte genau einen. Die Hürde
„zehn Matches" fiele also, die Hürde „von fünf verschiedenen Spielern" bewegte
sich nicht. Das ist wörtlich die Verwässerung, vor der das Skript warnt: die
Messung besteht und beweist nichts.
Die Zeilen werden deshalb daneben ausgewiesen statt versteckt — „wieviel lief übers Zählwerk" ist wissenswert, nur eben keine Antwort auf diese Frage. Die Zählregel lässt sich jederzeit ändern, sobald die Frage eine andere ist; die Provenienz nicht.
Konsequenzen¶
- Diese Anwendung hat ab jetzt einen externen Konsumenten. Die Invariante, die
JSON-Handler verboten hat, ist damit verbraucht — nicht aufgehoben: sie gilt
weiter für alles, was keinen Konsumenten hat, und
/apibleibt so schmal wie hier beschrieben. - Ein Token mehr im Betrieb, mit denselben Eigenschaften wie das Kiosk-Token: nicht gesetzt heißt, die Fläche existiert nicht.
- Ergebnisse aus dem Zählwerk erscheinen erst in Rangliste und TTR, wenn einer der beiden Spieler zustimmt. In der Testphase ist das der Zweck; danach ist es eine offene Aufgabe und keine Eigenschaft.
- Ein Abend am Zählwerk ohne Bestätigung hinterlässt eine Reihe wartender
Matches.
/fragments/pendingzeigt sie bereits. - Die Migration erweitert einen CHECK-Constraint, statt Daten anzufassen. Nach Invariante 8 vorwärtsgerichtet und additiv.
Was ausdrücklich wartet¶
- Turniere über das Zählwerk.
tournament_idundtournament_roundbleiben in diesem Weg leer. Ein Turnierspiel hat einen Platz im Tableau, und wer den vergibt, ist eine eigene Frage. - Sofortige Wertung. Siehe oben; die Entscheidung dazu fällt, wenn die Testphase etwas gezeigt hat.
- Ergebnisse zurück ins Zählwerk. Die Fläche ist einseitig: Spieler heraus, Ergebnis herein. Ein Zählwerk, das den TTR-Stand anzeigt, braucht sie nicht.