Architekturentscheidungen: Neunzehn neue ADRs tragen das Monorepo, drei der Vorlage sind ersetzt
Die ADRs folgen Michael Nygard: Kontext, Entscheidung, Status, Konsequenzen. Jede enthält eine
Pugh-Matrix gegen die Qualitätsziele QZ-1 bis QZ-5 aus
Kapitel 1.2. Referenz (0) ist die gewählte
Option; eine Alternative ist besser (+1), gleich (0) oder schlechter (−1). ? heißt: Das Urteil ist
offen. Alle ?-Zellen betreffen verworfene Alternativen und ändern die Wahl nicht; der Product Owner
hat sie am 23.09.2026 so akzeptiert, wie sie stehen. Die Konsequenzen nennen die Risiken aus
Kapitel 11.
Status: „Accepted“ heißt, der Nutzer hat entschieden oder bestätigt (für ADR-001 bis ADR-011 aus der Vorlage übernommen). Die ADRs, die bis dahin „Accepted (inferred)“ hießen (ADR-004, ADR-007, ADR-008, ADR-013, ADR-014, ADR-016 bis ADR-019), hat der Product Owner am 23.09.2026 bestätigt. „Superseded by ADR-0xx“ heißt, eine spätere Entscheidung hat sie ersetzt; der Text bleibt zur Nachvollziehbarkeit stehen.
Neue Kern- und Build-Entscheidungen brauchen ein ADR hier (CLAUDE.md, „Architecture“).
ADR-Index
| ADR | Titel | Status |
|---|---|---|
Statische Site auf GitHub Pages, kein Backend |
Accepted |
|
Vanilla-JavaScript-ES-Module ohne Framework und ohne Build |
Superseded by ADR-012 |
|
Kern als GitHub-Template, in Apps kopiert |
Superseded by ADR-012 |
|
Tutor-Schnittstelle aus llms.txt, tutor.md und Deep Links mit Aufgabennummer |
Accepted |
|
YouTube per Zwei-Klick mit lokalem Platzhalter, youtube-nocookie nach Klick |
Accepted |
|
Zahlenantworten: Rundung nach getippten Stellen, eigener Term-Auswerter, Live-Vorschau |
Accepted |
|
Diagnosetest ohne Noten, Ergebniszeile für den Tutor, nur im localStorage |
Accepted |
|
Eigene Strukturprüfung ohne Abhängigkeiten; im Monorepo als Build-Fehler |
Accepted |
|
Terme mit Variablen: eigener Parser, Gleichwertigkeit an festen Prüfstellen, Formprüfung am Syntaxbaum |
Accepted |
|
Versionsnummer |
Superseded by ADR-014 |
|
Primärfarbe folgt dem Fach, eine Tabelle, geprüft im Build |
Accepted |
|
Monorepo mit Eleventy 3.1.6, exakt gepinnt |
Accepted |
|
Kern einmal ausgeliefert, App-Konfiguration per Dependency Inversion |
Accepted |
|
|
Accepted |
|
Basis-URL an genau einer Stelle; Organisation lernapps, Repo lernapps.github.io |
Accepted |
|
Bilder aus derselben Zeichenfunktion für Build (Mini-DOM) und Browser |
Accepted |
|
Kompetenzseiten aus Markdown-Front-Matter mit festem Gerüst, Regeln als Build-Fehler |
Accepted |
|
Mathe-Karte im Monorepo, App-Einträge aus den App-Konfigurationen |
Accepted |
|
Neutrales Play-Symbol statt der Logo-Form von YouTube |
Accepted |
|
Keine Volltextsuche; Navigation über Bundesland, Klassenstufe und Fach |
Accepted |
|
„Zurück zu Claude“ schließt den Tab mit |
Accepted |
|
Rechenweg auf Papier, geprüft vom Tutor per Foto; kein Rechenweg-Feld in der App |
Accepted |
|
Risikoeinstufung nach Vibe-Coding Risk Radar: Tier 2 |
Accepted |
|
Wahrscheinlichkeitsbäume von oben nach unten; dreistufige quer mit Dreh-Hinweis |
Accepted |
|
Original-Harness-Rad als Zwei-Klick-Einbettung in der Doku |
Accepted |
|
Browser-Tests mit Playwright und axe-core in einem eigenen Workflow |
Accepted (inferred) |
|
KI-Review in frischem Kontext als Pflichtschritt vor dem Merge |
Accepted |
|
asciidoc-linter prüft die Architektur-Doku, gepinnt auf einen Commit |
Accepted |
|
Lizenzprüfung ohne neue Abhängigkeit plus Dependency Review |
Accepted |
|
Architektur-Review nach ATAM bei Architekturänderungen |
Accepted |
|
Kennzahlen der Übersichtsseite beim Doku-Build erzeugen |
Accepted |
ADR-001: Statische Site auf GitHub Pages, kein Backend
Kontext. Die Nutzer sind Kinder; die Apps kosten nichts und werden von einer Person mit einem
KI-Agenten gebaut. edugo nennt als Qualitätsziel „Zero-server deployability“. Die Mathe-Karte führt
backend: none als Eigenschaft jedes Eintrags.
Entscheidung. Alle Lern-Apps sind eine statische Site auf GitHub Pages. Es gibt keinen Server, keine
Konten, keine Datenbank. Was gespeichert wird, liegt im localStorage des Kindes. Im Monorepo gilt das
unverändert; der Build (ADR-012) läuft nur vor der Auslieferung.
Status. Accepted.
| Ziel | Statisch, kein Backend (Referenz) | Kleines Backend (Fortschritt speichern) | LMS-Plugin (Moodle o. Ä.) |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
-1 |
-1 |
QZ-2 Tutor-Anschluss |
0 |
+1 |
-1 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
-1 |
-1 |
QZ-5 Zugänglich |
0 |
0 |
? |
Summe |
0 |
-1 |
-3 + ? |
ADR-002: Vanilla-JavaScript-ES-Module ohne Framework und ohne Build
Status. Superseded by ADR-012. Weiter gilt: kein Framework im Browser, Vanilla-ES-Module, Inhalt im HTML. Ersetzt ist „kein Build, keine npm-Abhängigkeiten“.
Kontext (damals). Seiten mussten ohne JavaScript lesbar sein, die Apps sollten jahrelang ohne Pflege laufen, und ein KI-Agent sollte jede ausgelieferte Datei direkt ändern können.
Entscheidung (damals). HTML-Seiten mit dem ganzen Text, JavaScript nur als ES-Module, kein
Framework, kein Bundler, keine npm-Abhängigkeiten. Die Pugh-Matrix der Vorlage ließ den Vergleich mit
einem Static Site Generator bei QZ-4 als ? offen; der Spike zu ADR-012 hat diese Frage beantwortet.
Konsequenzen (damals). Wiederholtes HTML stand in jeder Seite und wurde von Skripten gleich gehalten. Im Monorepo erzeugt das Layout diese Teile (ADR-017).
ADR-003: Kern als GitHub-Template, in Apps kopiert
Kontext (damals). Prozent- und Zufall-Trainer hatten denselben Aufbau mit kopierten Modulen; die Vorlage zog das Generische heraus. Ohne Build (ADR-002) blieb nur das Kopieren.
Entscheidung (damals). Der Kern lag im Template-Repo; neue Apps entstanden mit
gh repo create --template; ein Fix ging zuerst in die Vorlage und wurde dann in jede App kopiert.
Konsequenzen (damals). Der Kern driftete, sobald eine Kopie vergessen wurde (ehemals R-001, TD-1). Bei rund 20 Apps je Schuljahr hätte jede Kern-Korrektur 20 Commits bedeutet; das war der Anlass für ADR-012. In den alten Einzel-Repos gilt dieser Zustand weiter (R-016).
ADR-004: Tutor-Schnittstelle aus llms.txt, tutor.md und Deep Links mit Aufgabennummer
Kontext. Das Kind lernt mit Claude auf claude.ai. Ohne Backend (ADR-001) gibt es keinen Ort für einen API-Schlüssel. Der Tutor muss trotzdem eine bestimmte Aufgabe zeigen und über sie sprechen können.
Entscheidung. llms.txt je App (src/<app>/llms.njk) beschreibt Seiten, Anker, URL-Parameter,
Antwortformate und Fehlercodes; tutor.md (src/<app>/tutor.njk) ist der Prompt. Jede Aufgabe
entsteht deterministisch aus einer Aufgabennummer (seed/nr, mulberry32). Der Start ist ein Link
https://claude.ai/new?q=Lade <App-Adresse>tutor.md …. Im Monorepo kommen llms.txt der Übersicht
(src/llms.njk) und der Karte (src/karte/llms.njk) dazu; alle Adressen darin leitet der Build aus
der Basis-URL ab (ADR-015). URL-Parameter und Anker sind öffentlicher Vertrag.
Status. Accepted. Vom Product Owner bestätigt am 23.09.2026.
| Ziel | llms.txt + tutor.md + Deep Links (Referenz) | LLM-API in der App | Keine Tutor-Anbindung |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
-1 |
+1 |
QZ-2 Tutor-Anschluss |
0 |
+1 |
-1 |
QZ-3 Richtige Rückmeldung |
0 |
? |
0 |
QZ-4 Neue Apps schnell |
0 |
-1 |
+1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
Summe |
0 |
-1 + ? |
+1, aber QZ-2 verfehlt |
Konsequenzen. Der Tutor sieht nie, was das Kind eingibt. Ändert ein Generator seine Logik, zeigt dieselbe Nummer eine andere Aufgabe (R-003). Ob claude.ai die Dateien lädt, liegt außerhalb (R-002). Texte, die ein LLM als Anweisung liest, sind ein Angriffsweg (R-006). Tutor-Links auf die alten Einzel-Apps veralten (R-016).
ADR-005: YouTube per Zwei-Klick mit lokalem Platzhalter, youtube-nocookie nach Klick
Kontext. Gute Erklärvideos liegen auf YouTube. Ein normales iframe und schon ein Vorschaubild von
i.ytimg.com schicken die IP-Adresse an Google, bevor das Kind etwas tut.
Entscheidung. src/_includes/kompetenz.njk erzeugt aus video: { id, titel, kanal } eine Karte mit
Link und Hinweis, ohne JS lesbar. src/kern/js/video.js macht daraus einen Platzhalter mit lokal
gezeichnetem Play-Symbol (ADR-019); erst der Klick erzeugt das iframe auf youtube-nocookie.com. Ein
Merker „Videos immer direkt laden“ gilt je App (<APP.id>.video-direkt).
Status. Accepted.
| Ziel | Zwei-Klick, lokaler Platzhalter (Referenz) | Direktes Embed | Fassade mit ytimg-Vorschaubild | Keine Videos |
|---|---|---|---|---|
QZ-1 Datenschutz |
0 |
-1 |
-1 |
+1 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
-1 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
0 |
0 |
+1 |
QZ-5 Zugänglich |
0 |
+1 |
0 |
-1 |
Summe |
0 |
0, verletzt TC-4 |
-1 |
0 |
ADR-006: Zahlenantworten: Rundung nach getippten Stellen, eigener Term-Auswerter, Live-Vorschau
Kontext. Früher prüften Generatoren mit eigenen festen Toleranzen; dasselbe „3,3“ war mal richtig,
mal falsch. Keine Eingabe darf mit eval laufen.
Entscheidung. Eine Regel für alle Zahlenfelder: Die Toleranz folgt den getippten Nachkommastellen; zu
grob gerundet ist ein Hinweis; Brüche und Terme gelten überall und exakt; ein eigener Tokenizer mit
rekursivem Abstieg wertet Terme ohne eval aus; unter dem Feld steht der Wert nach 150 ms. Keine
Toleranzen je Aufruf. Umgesetzt in src/kern/js/zahlantwort.js und bruch.js, Details in
8.7.
Status. Accepted.
| Ziel | Stellen-Regel + eigener Auswerter (Referenz) | Feste Toleranz je Feld | Mathe-Bibliothek (z. B. math.js) |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
? |
QZ-2 Tutor-Anschluss |
0 |
-1 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
-1 |
0 |
QZ-4 Neue Apps schnell |
0 |
-1 |
-1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
Summe |
0 |
-3 |
-1 + ? |
Konsequenzen. Der Prüfer ist Kern-Code; ein Fehler darin trifft alle Apps, eine Korrektur aber auch alle auf einmal (R-004). Die Vorschau zeigt „≈“ auch dort, wo der Wert exakt ist (TD-18); die Korrektur ist beschlossen, ein PR in Arbeit.
ADR-007: Diagnosetest ohne Noten, Ergebniszeile für den Tutor, nur im localStorage
Kontext. Der Tutor soll bei der schwächsten Kompetenz beginnen. Die App kann ihm nichts melden (ADR-001). Kinder sollen üben, nicht bewertet werden.
Entscheidung. Die Testseite jeder App (src/test.njk, paginiert über alle Apps) stellt je Kompetenz
zwei Aufgaben (Test) oder eine (Schnelltest) ohne Tipp, Lösung und Zeitmessung
(src/kern/js/testablauf.js:9). Ergebnis je Kompetenz: ✓, ~, ✗; am Ende eine Zeile
Test Nr. 4711 (<App>, Schnelltest): 1 ✓ 2 ~ 3 ✗. Das letzte Ergebnis liegt nur im localStorage.
Status. Accepted. Vom Product Owner bestätigt am 23.09.2026.
| Ziel | Ergebniszeile, localStorage (Referenz) | Punkte und Note | Ergebnis an Server/Tutor senden |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
-1 |
QZ-2 Tutor-Anschluss |
0 |
-1 |
+1 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
0 |
-1 |
QZ-5 Zugänglich |
0 |
? |
0 |
Summe |
0 |
-1 + ? |
-1 |
Konsequenzen. Kompakte Diagnose ohne Datenabfluss. Auf geteilten Geräten sieht das nächste Kind das Ergebnis (R-008).
ADR-008: Eigene Strukturprüfung ohne Abhängigkeiten; im Monorepo als Build-Fehler
Kontext. Viele Regeln (kein fremder Request, vollständige Kompetenz, Dateilänge, llms.txt) prüft
kein Unit-Test. KI-Agenten übersehen Regeln sonst. In der Vorlage lief die Prüfung als
scripts/pruefe.mjs in npm test.
Entscheidung. Die Regeln sind reine Funktionen in lib/pruefungen.js; eleventy.config.js ruft sie
nach dem Schreiben von _site auf und bricht den Build bei jedem Fund ab. Sie prüfen die
tatsächliche Ausgabe, nicht die Quellen. Regeln, die das Layout überflüssig macht (Kopfblock,
Footer-Version, Farbe an vier Stellen), sind entfallen. Unit-Tests der Regeln stehen in
test/build/pruefungen.test.js.
Status. Accepted. Vom Product Owner bestätigt am 23.09.2026. Den Umzug in den Build hat ADR-012 nötig gemacht; „Build-Fehler statt Warnung“ war erschlossen und ist damit bestätigt.
| Ziel | Eigene Prüfung als Build-Fehler (Referenz) | Linter/HTML-Validator (Abhängigkeit) | Nur Review und Checkliste |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
-1 |
QZ-2 Tutor-Anschluss |
0 |
-1 |
-1 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
-1 |
-1 |
QZ-5 Zugänglich |
0 |
+1 |
0 |
Summe |
0 |
-1 |
-3 |
Konsequenzen. Nichts geht live, was eine Regel bricht. Die Prüfung sieht nur Struktur, nicht Verhalten
im Browser (TD-5); llms.txt prüft sie nur auf die Nennung jeder Seite (TD-3). Die Doku unter /docs/
liegt außerhalb der Prüfung (TD-23). Accepted without tracked risk.
ADR-009: Terme mit Variablen: eigener Parser, Gleichwertigkeit an festen Prüfstellen, Formprüfung am Syntaxbaum
Kontext. Ab Klasse 8 geben Lernende Terme mit Variablen ein. Eine Termantwort ist richtig, wenn sie
gleichwertig ist und die verlangte Form hat. Abhängigkeiten und eval bleiben ausgeschlossen.
Entscheidung. src/kern/js/variablenterm.js liest Terme mit eigenem Parser zu einem Syntaxbaum;
src/kern/js/termantwort.js prüft Gleichwertigkeit an 8 festen Prüfstellen und die Form am
Syntaxbaum. Falsche Form ist ein Hinweis. Details in 8.7.1.
Erste Nutzerin ist die Binom-App.
Status. Accepted.
| Ziel | Eigener Parser + Prüfstellen (Referenz) | Symbolische Normalform (eigenes CAS) | CAS-Bibliothek |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
? |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
+1 |
+1 |
QZ-4 Neue Apps schnell |
0 |
-1 |
-1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
Summe |
0 |
0 |
0 + ? |
ADR-010: Versionsnummer ?v=<APP_VERSION> an allen Importen, Skripten und Stylesheets
Status. Superseded by ADR-014.
Kontext (damals). GitHub Pages liefert jede Datei mit Cache-Control: max-age=600. Nach einem
Deployment kombinierte ein Browser eine neue test.html mit einer alten js/app.config.js; die
Start-Knöpfe blieben tot. Ohne Build (ADR-002) gab es keine Hash-Dateinamen.
Entscheidung (damals). Jede lokale Referenz trug ?v=<APP_VERSION>, gesetzt von
scripts/version.mjs, geprüft von scripts/pruefe.mjs.
Konsequenzen (damals). Wer JS änderte und die Version nicht hob, lieferte neuen Inhalt unter alter URL (ehemals R-014). Im Monorepo setzt der Build einen Inhalts-Hash, und diese Lücke entfällt.
ADR-011: Primärfarbe folgt dem Fach, eine Tabelle, geprüft im Build
Kontext. Frei gewählte Farben ließen Apps verschiedener Fächer gleich aussehen, und niemand prüfte, ob weiße Schrift lesbar ist (QZ-5).
Entscheidung. Jedes Fach hat eine Primärfarbe und eine dunkle Variante, beide mit weißer Schrift nach
WCAG AA lesbar. Die Tabelle steht einmal in lib/fachfarben.js; APP.fach wählt die Zeile, das Layout
setzt die Farbe überall. Anders als in der Vorlage ist ein unbekanntes Fach ein Build-Fehler
(lib/fachfarben.js:29-32), kein Hinweis. Details in 8.11.
Status. Accepted. Farbwerte vom Nutzer festgelegt; Kontraste in test/build/fachfarben.test.js.
| Ziel | Fachfarbe aus Tabelle, im Build (Referenz) | Freie Farbe je App | Tabelle nur als Doku |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
-1 |
-1 |
QZ-5 Zugänglich |
0 |
-1 |
0 |
Summe |
0 |
-2 |
-1 |
Konsequenzen. Ein sechstes Fach braucht eine Zeile in der Tabelle und einen grünen Kontrasttest. Icons tragen die Farbe, werden aber nicht geprüft; accepted without tracked risk.
ADR-012: Monorepo mit Eleventy 3.1.6, exakt gepinnt
Kontext. Pro Schuljahr sollen rund 20 Apps entstehen. Mit Vorlage und Kopie (ADR-003) hätte jede Korrektur am Kern 20 Commits in 20 Repos bedeutet, und jede vergessene Kopie hätte Apps unterschiedlich prüfen lassen (ehemals R-001). Wiederholtes HTML (Kopf, Menü, Footer, Checkliste) musste in jeder Seite von Skripten gleich gehalten werden (ADR-002). Ein Spike mit 20 künstlichen Apps hat gemessen: Build aller 20 Apps ≈ 1,5 s; eine neue Kompetenz = 3 Dateien + 1 Zeile (Messwerte vom Nutzer, der Spike-Code ist verworfen). Der heutige Build mit 3 Apps und Karte braucht 0,41 s (45 Seiten, Messung 23.09.2026).
Entscheidung. Alle Apps und die Mathe-Karte liegen in einem Repository. Eleventy 3.1.6 baut daraus
statisches HTML; es ist die einzige npm-Abhängigkeit und exakt gepinnt
(npm install --save-dev --save-exact), gebaut wird nur über npm run build. Im Browser bleibt es bei
Vanilla-ES-Modulen ohne Framework. Die alten Einzel-Repos bleiben unberührt, bis sie abgelöst sind.
Status. Accepted. Entschieden vom Nutzer nach dem Spike.
| Ziel | Monorepo + Eleventy (Referenz) | Einzel-Repos, Kern kopiert (bisher) | Einzel-Repos, Kern als npm-Paket | Monorepo mit Hugo |
|---|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
-1 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
-1 |
-1 |
-1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
-1 |
Summe |
0 |
-2 |
-1 |
-2 |
Kopierte Kerne driften (QZ-3 −1) und kosten 20 Commits je Korrektur (QZ-4 −1). Ein npm-Paket verteilt
Korrekturen sicher, verlangt aber 20 Versionsanhebungen und 20 Deployments je Kern-Release (QZ-4 −1).
Hugo ist schnell, kann aber die JavaScript-Generatoren und Zeichenfunktionen nicht ausführen; ohne sie
gäbe es kein Bild ohne JavaScript (QZ-5 −1, ADR-016) und zwei Sprachen im Repo (QZ-4 −1). Ein eigenes
Node-Skript statt Eleventy wurde nicht bewertet; ob es weniger Pflege kostet als eine gepinnte
Abhängigkeit, ist ?.
Konsequenzen. Eine Kern-Korrektur ist ein Commit für alle Apps. Das Repo hängt an Eleventy (R-015). Die alten Einzel-Repos liefen parallel weiter und veralteten, bis sie am 23.09.2026 gelöscht wurden (R-016, erledigt). Ein Fehler im Build oder Kern trifft alle Apps zugleich (R-004); dafür prüft der Vertragstest jede Kompetenz jeder App. Ein gescheiterter Doku-Build hält jetzt das Deployment aller Apps an (R-011).
ADR-013: Kern einmal ausgeliefert, App-Konfiguration per Dependency Inversion
Kontext. In der Vorlage importierte der Kern js/app.config.js über einen festen relativen Pfad. Im
Monorepo gibt es viele Konfigurationen, aber der Kern soll nur einmal existieren.
Entscheidung. src/kern/ wird einmal nach /kern/ kopiert (eleventy.config.js, Passthrough). Apps
importieren ihn relativ (../kern/js/seite.js von einer Seite). Der Kern importiert nie eine
App-Konfiguration: Die Seite reicht APP, KOMPETENZEN und Generatoren hinein
(starteSeite, starteStartseite, starteTestseite, erzeugeSpeicher(praefix),
initVideos(praefix)). Keine Symlinks, keine Kopien je App. Commit fec2b1e.
Status. Accepted. Vom Product Owner bestätigt am 23.09.2026.
| Ziel | Kern einmal, DI (Referenz) | Kern importiert Konfiguration, Kopie je App im Build | Kern importiert Konfiguration über Import-Map je Seite |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
0 |
-1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
Summe |
0 |
0 |
-1 |
Die Kopie je App ist gegen die Ziele gleichwertig. Den Ausschlag gibt, dass derselbe Kern unter einer URL im Browser-Cache für alle Apps liegt und reine, konfigurationsfreie Module ohne Tricks testbar sind. Eine Import-Map müsste in jeder Seite gepflegt werden (QZ-4 −1).
Konsequenzen. Der Kern ist ohne App testbar; jede Kern-Funktion bekommt ihre Abhängigkeiten als Parameter. Eine Änderung an einer Kern-Signatur trifft alle Apps; der Vertragstest fängt das (R-004). Accepted without further tracked risk.
ADR-014: ?v=<Inhalts-Hash> setzt der Build
Kontext. Das Cache-Problem von ADR-010 besteht weiter (max-age=600). ADR-010 verlangte, die Version
von Hand zu heben; wer es vergaß, lieferte neuen Inhalt unter alter URL (ehemals R-014). Mit dem Build
(ADR-012) lässt sich die Nummer aus dem Inhalt ableiten.
Entscheidung. Nach dem Schreiben von _site bildet der Build einen SHA-256-Hash (8 Hex-Zeichen) über
Pfad und Inhalt aller ausgelieferten JS-, CSS- und JSON-Dateien und hängt ?v=<Hash> an jede lokale
Referenz in HTML und JS (lib/versionierung.js:9-28, eleventy.config.js). In den Quellen steht nie
?v=. Die Site-Version im Footer kommt getrennt davon aus package.json.
Status. Accepted. Vom Product Owner bestätigt am 23.09.2026.
| Ziel | Inhalts-Hash vom Build (Referenz) | ?v=<Version> von Hand (ADR-010) |
Hash im Dateinamen (Bundler) | Nichts tun |
|---|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
-1 |
QZ-3 Richtige Rückmeldung |
0 |
-1 |
0 |
-1 |
QZ-4 Neue Apps schnell |
0 |
-1 |
-1 |
+1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
-1 |
Summe |
0 |
-2 |
-1 |
-2 |
Die Version von Hand lässt vergessene Anhebungen zu; dann rechnet ein alter Prüfer im Browser weiter (QZ-3 −1, QZ-4 −1). Hash-Dateinamen sind die Standardlösung, brauchen aber einen Bundler oder ein Plugin und ändern jeden Importpfad (QZ-4 −1).
Konsequenzen. Keine Handarbeit, keine vergessene Version. Ein Hash für die ganze Site: Ändert sich eine Datei, laden alle Apps alle Module neu; bei rund 20 kleinen Modulen je App in Kauf genommen. Die alte HTML-Seite im Cache bleibt ein Fenster von bis zu 10 Minuten (R-013).
ADR-015: Basis-URL an genau einer Stelle; Organisation lernapps, Repo lernapps.github.io
Kontext. Die Adresse steckt in Canonicals, llms.txt, tutor.md, dem claude.ai-Link, Quellcode-Links
und im pathPrefix. Ein Umzug (etwa vom persönlichen Konto in eine Organisation) hätte sonst dutzende
Stellen geändert, und jede vergessene Stelle bricht einen Tutor-Link.
Entscheidung. Die Basis-URL steht nur in src/_data/site.js:9; lib/adressen.js leitet alles andere
ab, auch das Repository einer Organisations-Site an der Wurzel (Commit ef00de2). Die Site liegt in
der GitHub-Organisation lernapps im Repository lernapps.github.io und ist damit unter
https://lernapps.github.io/ erreichbar (Commit 60c307a).
Status. Accepted. Organisation und Repository hat der Nutzer entschieden.
| Ziel | Eine Stelle, Org-Site (Referenz) | Adresse literal an jeder Stelle | Eigene Domain |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
? |
QZ-2 Tutor-Anschluss |
0 |
-1 |
+1 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
-1 |
-1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
Summe |
0 |
-2 |
0 + ? |
Eine eigene Domain machte Links unabhängig von GitHub (QZ-2 +1), kostet aber Geld und DNS-Pflege
(QZ-4 −1); ob ein Registrar oder DNS-Anbieter etwas über Besucher erfährt, ist ?.
Konsequenzen. Ein Umzug ist eine Zeile. Die Organisation unabhängig vom persönlichen Konto macht
weitere Mitwirkende möglich, verlangt aber heute keine Zwei-Faktor-Anmeldung
(R-017). Ein Repo <org>.github.io schaltet Pages im Branch-Modus
ein; das muss einmal auf „GitHub Actions“ umgestellt werden
(Kapitel 7). Die alten Adressen unter
raifdmueller.github.io bleiben bestehen (R-016).
ADR-016: Bilder aus derselben Zeichenfunktion für Build (Mini-DOM) und Browser
Kontext. In den Einzel-Apps entstand das Bild erst mit JavaScript oder war zusätzlich von Hand als SVG gezeichnet. Ohne JS fehlte es dann, oder die Handzeichnung passte nicht zur Aufgabe.
Entscheidung. Zeichenfunktionen zeichne…(svg, aufgabe, ergebnis) nutzen nur svgEl aus
src/kern/js/svg.js. Im Build läuft dieselbe Funktion gegen ein Mini-DOM und schreibt statisches SVG in
die Seite (lib/bild.js:14-26); im Browser zeichnet sie zu jeder Aufgabe neu. Keine handgezeichneten
SVGs.
Status. Accepted. Vom Product Owner bestätigt am 23.09.2026.
| Ziel | Eine Funktion, Mini-DOM (Referenz) | Handgezeichnetes SVG + JS-Zeichnung | Nur JS-Bild | Headless-Browser im Build |
|---|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
-1 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
-1 |
+1 |
-1 |
QZ-5 Zugänglich |
0 |
0 |
-1 |
0 |
Summe |
0 |
-2 |
0 |
-1 |
„Nur JS“ spart das Mini-DOM, verfehlt aber die Randbedingung TC-3 (Bild ohne JS); ein Headless-Browser bringt eine schwere Abhängigkeit und langsame Builds.
Konsequenzen. Zeichenfunktionen dürfen document nicht anfassen. Das Mini-DOM kann nur, was die
Zeichenfunktionen brauchen, und kann vom Browser-DOM abweichen
(R-019).
ADR-017: Kompetenzseiten aus Markdown-Front-Matter mit festem Gerüst, Regeln als Build-Fehler
Kontext. In der Vorlage war jede Kompetenzseite eine HTML-Datei mit Kopf, Menü und Footer; Skripte
hielten die Kopien gleich. Der Tutor verlässt sich auf feste Anker (#warum … #uebung).
Entscheidung. src/<app>/<id>.md enthält nur Front Matter (warum, regel, beispiel, video
oder ohneVideo, bild). src/_includes/kompetenz.njk setzt immer dasselbe Gerüst mit denselben
Ankern; Start-, Test- und Manifest-Seite entstehen per Pagination über alle Apps. Der Build verlangt je
Kompetenz Seite, Generator und Test und bricht sonst ab (lib/pruefungen.js:36-49).
Status. Accepted. Vom Product Owner bestätigt am 23.09.2026.
| Ziel | Front Matter + Gerüst + Build-Fehler (Referenz) | HTML je Seite (Vorlage) | Freies Markdown mit eigenem Aufbau |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
-1 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
-1 |
0 |
QZ-5 Zugänglich |
0 |
0 |
-1 |
Summe |
0 |
-1 |
-2 |
Konsequenzen. Eine neue Kompetenz ist klein, aber das Anlegen der Dateien ist Handarbeit; das Hilfsskript der Vorlage kommt mit der nächsten App (TD-20). Längere HTML-Abschnitte im Front Matter sind unhandlich; mehrere Videos je Seite sieht das Gerüst nicht vor, und das bleibt so, bis eine Seite ein zweites braucht (TD-17). Accepted without tracked risk.
ADR-018: Mathe-Karte im Monorepo, App-Einträge aus den App-Konfigurationen
Kontext. Die Mathe-Karte lag in einem eigenen Repo; Apps wurden per PR mit einer eigenen Eintragsdatei eingetragen. Titel, Adresse und Kompetenzen standen damit doppelt und konnten auseinander laufen.
Entscheidung. Die Karte liegt unter src/karte/ und wird von Eleventy gebaut (Commits e6123d1,
cfcfb9e). Eine App trägt ihren Eintrag selbst: APP.kartenEintrag mit den edugo-Feldern und
kartenKnoten je Kompetenz. Titel, Adresse, Quellcode und Beschreibung kommen aus APP. Ein
unbekannter Knoten bricht den Build (lib/karte/eintraege.js:37-50). Die Karte ist keine App.
Status. Accepted. Vom Product Owner bestätigt am 23.09.2026.
| Ziel | Im Monorepo, Eintrag aus der App (Referenz) | Eigenes Repo, Eintrag per PR (bisher) | Im Monorepo, Eintrag als eigene Datei |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
-1 |
-1 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
-1 |
0 |
QZ-5 Zugänglich |
0 |
0 |
0 |
Summe |
0 |
-2 |
-1 |
ADR-019: Neutrales Play-Symbol statt der Logo-Form von YouTube
Kontext. Eine Stichprobe auf Übernahmen fand als einzigen wörtlichen Treffer die Play-Button-Form
von YouTube in der Videokarte, eingefärbt in der Fachfarbe: formal ein verändertes Markenlogo, was die
Markenrichtlinien von YouTube nicht erlauben (PR #3, Commit 94a6f35).
Entscheidung. Die Videokarte zeigt ein neutrales Symbol, einen Kreis mit Dreieck
(PLAY_SYMBOL, src/kern/js/video.js:61-79, mit Test). Der Hinweis nennt YouTube als Text.
Status. Accepted. Vom Product Owner bestätigt am 23.09.2026. Die Begründung steht in PR #3.
| Ziel | Neutrales Symbol (Referenz) | Logo-Form in Fachfarbe (bisher) | Offizielles Logo unverändert, lokal |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
0 |
0 |
QZ-5 Zugänglich |
0 |
0 |
? |
Summe |
0 |
0, verletzt OC-5 |
? |
Gegen die Qualitätsziele sind die Optionen gleich. Den Ausschlag gibt die Randbedingung OC-5; ob das
unveränderte Logo das Video besser erkennbar macht, ist ?, verlangt aber die Einhaltung der
Markenrichtlinien.
Konsequenzen. Kein Markenproblem in der Videokarte. Die Stichprobe war keine vollständige Prüfung auf Übernahmen; zusammen mit der offenen Lizenz steht das in R-018.
ADR-020: Keine Volltextsuche; Navigation über Bundesland, Klassenstufe und Fach
Kontext. Mit rund 20 Apps je Schuljahr wächst die Zahl der Kompetenzseiten schnell. Die naheliegende Antwort wäre eine Suche. Ein Kind weiß aber meist nicht, wie das Thema heißt, sondern nur, in welcher Klasse es ist und welches Fach gerade dran ist. Auf serlo.org war zu beobachten, dass Suchvorschläge ins Leere führen können: Ein Vorschlag, der auf keine passende Seite zeigt, ist für ein Kind ein eigenes Risiko, weil es aufgibt, statt weiterzuklicken.
Entscheidung. Es gibt keine Volltextsuche. Das Kind wählt Bundesland, Klassenstufe und Fach; das
grenzt die Auswahl auf eine Handvoll Kompetenzen ein. Die Mathe-Karte trägt diese Navigation mit ihren
URL-Parametern land und jahrgang (8.14); jede
Auswahl führt auf eine Seite, die es gibt.
Status. Accepted. Vom Product Owner entschieden am 23.09.2026.
| Ziel | Navigation Land/Klasse/Fach (Referenz) | Pagefind (statischer Suchindex) | Filter auf der Mathe-Karte |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
−1 |
0 |
QZ-5 Zugänglich |
0 |
−1 |
? |
Summe |
0 |
−2 |
? |
Pagefind liefe lokal und ohne fremde Hosts (QZ-1 0), bräuchte aber einen zusätzlichen Index-Schritt und
eine weitere Abhängigkeit im Build (QZ-4 −1). Die Suche funktioniert nur mit JavaScript, und Treffer
ohne passende Seite führen ein Kind ins Leere (QZ-5 −1). Ein Freitext-Filter auf der Mathe-Karte kostet
wenig; ob er Kindern hilft oder dieselben Leerläufe erzeugt, ist ? und bräuchte einen Test mit
Kindern.
Konsequenzen. Die Navigation bleibt statisch und ohne JavaScript lesbar. Wird eine Kombination aus Land, Klasse und Fach so groß, dass sie mehr als eine Handvoll Kompetenzen zeigt, reicht sie nicht mehr; dieses Risiko steht als R-020 in Kapitel 11. Die Entscheidung wird dann neu bewertet, zuerst mit dem Filter auf der Karte.
ADR-021: „Zurück zu Claude“ schließt den Tab mit window.close() und prüft danach
Kontext. Der Tutor auf claude.ai öffnet jede verlinkte Aufgabe in einem neuen Tab. Das Kind jongliert
dann zwei Tabs: Es löst die Aufgabe, sucht den Chat wieder und tippt nach, was herauskam. Die App kann
dem Tutor nichts melden (ADR-001, ADR-004). Ein Spike am 23.09.2026 mit Playwright 1.63 (Chromium 153,
Firefox 155, WebKit 26.6) hat gezeigt: window.close() schließt einen per Link geöffneten Tab in allen
drei Engines, bei jedem rel-Wert und auch nach Navigation innerhalb der App. Hat das Kind den Tab
selbst geöffnet (Lesezeichen, eingetippte Adresse), schlägt der Aufruf still fehl. Weder
document.referrer noch window.opener verraten diesen Fall vorher; nur eine Prüfung nach dem Aufruf.
In den In-App-Browsern mobiler Apps ist das Verhalten ungetestet.
Entscheidung. Der Tutor hängt von=tutor an jeden Link (tutor.md, llms.txt); der Build lässt den
Parameter auf jeder Seite zu, aber nur mit dem Wert tutor (lib/llms-vertrag.js, erlaubeHerkunft).
Nur dann zeigt die Übung unter „Link kopieren“ und der Test neben der Ergebniszeile den Knopf „Zurück
zu Claude“ (src/kern/js/zurueck.js). Der Klick kopiert eine Ergebniszeile, wartet 100 ms, ruft
window.close() und prüft nach 300 ms window.closed. Ist der Tab noch offen, sagt eine
aria-live-Zeile: „Ergebnis kopiert. Schließ diesen Tab und füge es bei Claude ein.“ Einen Link auf
claude.ai gibt es nicht; er öffnete einen neuen, leeren Chat. Scheitert die Zwischenablage, bleibt der
Tab offen und die Zeile steht markiert in einem Feld. „Link zu dieser Aufgabe“ und „Link kopieren“
lassen von=tutor weg: Ein weitergegebener Link kommt nicht vom Tutor, und der Knopf liefe dort ins
Leere.
Status. Accepted. Vom Product Owner entschieden am 23.09.2026.
| Ziel | window.close() mit Nachprüfung (Referenz) |
postMessage an den Opener |
Kein Knopf |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
? |
0 |
QZ-2 Tutor-Anschluss |
0 |
−1 |
−1 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
−1 |
+1 |
QZ-5 Zugänglich |
0 |
0 |
−1 |
Summe |
0 |
−2 + ? |
−1 |
postMessage scheitert schon am Opener: claude.ai öffnet Links mit noopener, window.opener ist
null, und claude.ai hört auf keine Nachricht (QZ-2 −1). Es bräuchte zudem eigenen Code auf beiden
Seiten (QZ-4 −1). Ob eine Nachricht an eine fremde Seite dem Datenschutz genügt, ist ?. Ohne Knopf
bleibt der Kern kleiner (QZ-4 +1), aber das Kind tippt das Ergebnis ab (QZ-2 −1), und zwei Tabs auf dem
Handy sind für Kinder eine Hürde (QZ-5 −1).
Konsequenzen. Die Ergebniszeile geht nur in die Zwischenablage des Geräts; die App sendet nichts
(8.4). Wo der Browser den Tab nicht schließt, fängt die
Nachprüfung den Fall ab. Wo er window.closed meldet, den Tab aber nicht schließt, bleibt das Kind ohne
Hinweis; das betrifft vor allem ungetestete In-App-Browser
(R-021). Die 100 ms vor dem Schließen stammen aus der Messung im
Browser-Check: Headless-Chromium verlor ohne Pause 6 von 20 kopierten Zeilen, mit 50 ms keine.
Der Knopf hängt am Tutor: Er erscheint nur, wenn Claude von=tutor an den Link hängt. Vergisst Claude
das bei einem selbst gebauten Link, fehlt der Knopf. Deshalb steht die Regel in tutor.md als erste
App-Regel, mit einer Selbstprüfung vor dem Senden; der Build prüft, dass jeder App-Link in tutor.md
von=tutor trägt (pruefeTutorHerkunft). Fehlt der Knopf, zeigt die Adresszeile die Ursache: Fehlt
von=tutor, hat der Tutor den Parameter vergessen; steht er da, liegt der Fehler in der App.
ADR-022: Rechenweg auf Papier, geprüft vom Tutor per Foto; kein Rechenweg-Feld in der App
Kontext. In der Klassenarbeit zählt der Rechenweg, nicht nur das Ergebnis. Die Apps prüfen aber nur Ergebnisse (8.7). Ein Kind kann also jede App-Aufgabe richtig lösen und in der Arbeit trotzdem Punkte verlieren, weil der Weg fehlt oder einen Fehler hat. Die App kennt den Rechenweg nicht, und der Tutor sieht ihn nur, wenn das Kind ihn zeigt.
Entscheidung. Der Tutor arbeitet je Kompetenz in zwei Phasen. In der Lernphase übt das Kind, der
Tutor erklärt, und die App prüft die Ergebnisse. Um ein Foto bittet er dann nicht; Fehler klärt er im
Gespräch. In die Klassenarbeit-Phase kommt eine Kompetenz auf einem von zwei Wegen: Drei Ergebniszeilen
derselben Kompetenz melden hintereinander „richtig im 1. Versuch“, oder der Schnelltest oder
Diagnosetest markiert sie mit ✓. Dann bittet der Tutor um genau eine Aufgabe dieser Kompetenz, ganz
von Hand: Aufgabe und ganzer Rechenweg, wie in der Klassenarbeit, als Foto nur vom Blatt, ohne Namen.
Nach einem Test gilt das für jede Kompetenz mit ✓. Der Tutor prüft jeden Schritt, nennt den ersten
falschen und stellt eine Leitfrage, statt die Korrektur zu geben. Stimmt alles, ist die Kompetenz
„arbeitsreif“, und der Tutor sagt es dem Kind. Das Ergebnis tippt das Kind wie bisher in die App; die
App prüft es. Die Regel steht in tutor.md (Abschnitt 3, „Rechenweg auf Papier“) und in llms.txt
jeder App („Regeln für den Tutor“) sowie in der Übersicht /llms.txt. Die App ändert sich nicht.
Den Auslöser liest der Tutor aus der Ergebniszeile von „Zurück zu Claude“ (ADR-021), etwa „Binomische Formeln, Kompetenz 2 „Erste binomische Formel“, Aufgabe Nr. 4: richtig im 1. Versuch (Sitzung: 3 von 3 richtig)“. Sie nennt Kompetenz und Versuch, aber nur für die letzte Aufgabe im Tab. Frühere Aufgaben im selben Tab stehen nur als Zähler „x von y“ darin: x kleiner y zeigt einen Fehler, ob sie im ersten Versuch richtig waren, zeigt die Zeile nicht. Der Tutor zählt deshalb Zeilen, nicht Aufgaben.
Status. Accepted. Vom Product Owner entschieden am 24.09.2026.
Nachtrag 24.09.2026 (PO, verfeinert). Die erste Fassung verlangte zu jeder Aufgabe ein Foto. Das war zu viel, und kein Auslöser sagte, wann der Tutor fragt. Der Product Owner hat die Regel am selben Tag auf die zwei Phasen oben verfeinert: ein Foto je Kompetenz, erst wenn sie reif für die Klassenarbeit ist. Nebenwirkung: Viel weniger Fotos gehen an den Chat-Anbieter (R-022).
| Ziel | Papier + Foto, Tutor prüft (Referenz) | Rechenweg-Feld mit Äquivalenzprüfung je Schritt | Kein Rechenweg |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
+1 |
+1 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
? |
−1 |
QZ-4 Neue Apps schnell |
0 |
−1 |
0 |
QZ-5 Zugänglich |
0 |
−1 |
0 |
Summe |
0 |
−1 + ? |
0 |
Die Referenz schickt ein Foto je Kompetenz, nicht mehr eines je Aufgabe; QZ-1 bleibt 0, die
Belastung sinkt aber deutlich. Ein Rechenweg-Feld bliebe im Browser, und kein Foto verließe das Gerät
(QZ-1 +1). Das Kind müsste aber
eine Feld-Syntax lernen, Zeile für Zeile Terme tippen, auf dem Handy mit 360 px (QZ-5 −1). Das hat der
Product Owner ausdrücklich verworfen: Das Kind soll Mathe üben, keine Eingabe-Syntax. Ein Schritt-Prüfer
wäre ein neuer Kern-Baustein, und jede App bräuchte eigene Regeln, welche Schritte zulässig sind
(QZ-4 −1). Ob eine Äquivalenzprüfung je Schritt verlässlicher urteilt als der Tutor am Foto, ist ?:
Sie findet Rechenfehler sicher, erkennt aber nicht, ob ein Weg in der Arbeit Punkte bringt. Ohne
Rechenweg gibt es keine Fotos (QZ-1 +1), aber die Rückmeldung deckt nur das Ergebnis ab, nicht das, was
in der Klassenarbeit zählt (QZ-3 −1). Die Summen trennen die Referenz nicht von „Kein Rechenweg“; den
Ausschlag gibt das Lernziel: sicher werden für Unterricht und Klassenarbeit.
Konsequenzen. Fotos der Handschrift gehen an den Anbieter des Chats. Die App selbst überträgt nichts (8.4); ob das Kind Fotos schickt, entscheiden das Kind und seine Eltern in ihrem Chat-Werkzeug. Die Bedrohung steht als T-014 in 8.1, die Maßnahme als M-18 in 8.2, das Risiko als R-022 in Kapitel 11. Der Tutor kann Handschrift falsch lesen oder einen richtigen Schritt als falsch werten (R-023); die Leitfrage statt der fertigen Korrektur lässt das Kind widersprechen, und das Ergebnis prüft weiter die App. Ob der Tutor Schritte richtig beurteilt, prüft kein automatischer Test, nur der Tutor-Dialog (8.3).
ADR-023: Risikoeinstufung nach Vibe-Coding Risk Radar: Tier 2
Kontext. Ein KI-Agent schreibt fast den ganzen Code; der Product Owner prüft und merged. Welche
Prüfungen dafür reichen, war bisher Gefühlssache. Das
Vibe-Coding Risk Radar macht daraus eine
Rechnung: fünf Dimensionen mit Werten von 0 bis 4, die Stufe ist das Maximum, und eine
LLM-Laufzeit ab L3 hebt die Untergrenze an (Modell im Repository llm-coding/vibe-coding-risk-radar,
.claude/skills/shared/risk-model.md). Jede Stufe verlangt ihre Maßnahmen und die aller Stufen darunter.
Die Einstufung für dieses Repository:
| Dimension | Wert | Begründung |
|---|---|---|
Code Type |
2 |
Geschäftslogik: Term-Parser, Rundung, Generatoren und Prüfer ( |
Language |
2 |
JavaScript ohne Typprüfung |
Deployment |
2 |
Öffentlich erreichbar, aber ohne Konten und ohne personenbezogene Daten (ADR-001) |
Data |
0 |
Keine Datenhaltung; im |
Blast Radius |
1 |
Eine kaputte App ist nicht erreichbar oder falsch; es gehen keine Daten verloren |
LLM Runtime |
L0 |
Im Code läuft kein LLM. Der Tutor auf claude.ai ist generativ (L2), liegt aber außerhalb und hebt die Stufe nicht (erst ab L3) |
Das Modell kennt einen Schaden nicht, der hier am schwersten wiegt: den Lernschaden. Eine falsche Rückmeldung lässt kein System ausfallen und verliert keine Daten; sie bringt einem Kind still etwas Falsches bei, und niemand merkt es. Keine der fünf Dimensionen misst das.
Eine zweite Besonderheit steht ebenfalls nicht im Modell: tutor.md ist ein Prompt, den das Kind in
seinen Chat lädt. Wer das Repository übernimmt, gibt der Chat-KI Anweisungen, während sie mit einem Kind
spricht (T-015). Ein Link auf eine fremde Seite reicht dafür, denn deren
Inhalt ändert sich nach jedem Review.
Entscheidung. Das Repository ist Tier 2 (Moderate), bestimmt von Code Type, Language und Deployment mit je 2. Es gelten die Maßnahmen von Tier 1 und Tier 2, mit dem Stand aus EPIC #18:
| Maßnahme | Stand | Umsetzung |
|---|---|---|
CI-Build und Unit-Tests (T1) |
vorhanden |
Pflicht-Check |
Dependency-Check (T1) |
umgesetzt, #20 |
|
Linter (T1) |
umgesetzt, #21 |
ESLint Flat Config mit |
Type Checking (T1) |
umgesetzt, #25 |
|
Pre-Commit-Hooks (T1) |
Won’t, #27 |
Der Pflicht-Check prüft dasselbe; Hooks lassen sich lokal umgehen |
SAST (T2) |
umgesetzt, #19 |
CodeQL Default Setup; dazu Secret Scanning mit Push Protection und Dependabot |
Property-Based Tests (T2) |
umgesetzt, #22 |
fast-check für Term-Parser, Termgleichheit und Rundung; fand gleich den Fehler #30, behoben |
KI-Code-Review (T2) |
umgesetzt, #24 |
Review in frischem Kontext vor jedem Merge, Check |
SonarQube Quality Gate (T2) |
Won’t, #28 |
Dateilänge ≤ 500 Zeilen und ESLint decken das Nötige ab |
Sampling Review, ~20 % (T2) |
100 % |
Der Product Owner merged jeden PR selbst und sieht ihn dabei durch |
Playwright in CI |
umgesetzt, #26 |
Workflow |
Die Review-Quote von 100 % ist eine Regel, keine technische Sperre: Der Branch-Schutz verlangt einen PR,
aber keine Freigabe (required_approving_review_count: 0). Mit einem einzigen Maintainer geht es nicht
anders, denn GitHub lässt niemanden den eigenen PR freigeben.
Gegen die zweite Besonderheit kommen drei Maßnahmen hinzu. Die Organisation verlangt Zwei-Faktor-Anmeldung
(entschieden am 24.09.2026, 8.2 M-21). Branch-Schutz (M-15) und Secret Scanning mit Push Protection
(M-19) bleiben an. Und der Build lässt in tutor.md und llms.txt jeder App, in der Übersicht
/llms.txt und in karte/llms.txt nur Links auf eine Allowlist zu (pruefeTutorLinks in
lib/pruefungen.js, 8.2 M-22). Erlaubt sind relative Links, die eigene Site
(https://lernapps.github.io/), das eigene Repository, https://de.serlo.org/ und YouTube-Videoseiten
(https://www.youtube.com/watch?v=). Jedes andere Ziel bricht den Build.
Dieses ADR deckt auch die neuen Entwicklungs-Abhängigkeiten aus EPIC #18: eslint, @eslint/js,
eslint-plugin-no-unsanitized, globals und fast-check. Alle sind devDependencies, exakt gepinnt;
keine wird ausgeliefert.
Status. Accepted. Vom Product Owner entschieden am 24.09.2026.
| Ziel | Tier 2 (Referenz) | Tier 1 | Tier 3 (Deployment = 3, weil Kinder die Nutzer sind) |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
−1 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
? |
QZ-3 Richtige Rückmeldung |
0 |
−1 |
+1 |
QZ-4 Neue Apps schnell |
0 |
+1 |
−1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
Summe |
0 |
−1 |
0 + ? |
Tier 1 spart Arbeit (QZ-4 +1), verzichtet aber auf SAST und Secret Scanning; ein durchgerutschtes Token
oder eingeschleuster Code könnte unbemerkt Tracking einbauen (QZ-1 −1). Ohne Property-Based Tests wäre
der Fehler #30 geblieben (QZ-3 −1). Tier 3 brächte Fuzzing für den Term-Parser (QZ-3 +1), verlangt aber
eine Pflicht-Freigabe durch eine zweite Person, Penetrationstests und Canary-Deployments, die ein
einzelner Maintainer auf GitHub Pages nicht leisten kann (QZ-4 −1). Ob PromptBOM, also Modell, Prompt und
Freigabe je Änderung zu dokumentieren, den Tutor-Anschluss sicherer macht, ist ?. Die Summen trennen
Tier 2 nicht von Tier 3; den Ausschlag gibt das Modell selbst: Kinder als Nutzer machen die Site nicht zu
einem regulierten System, solange sie keine Daten erfasst.
Konsequenzen. Die Pflicht-Maßnahmen für Tier 2 sind bis auf das KI-Review umgesetzt. Die Review-Quote
hängt an der Disziplin einer Person (R-026); #24 soll das mildern.
Der Lernschaden liegt außerhalb des Modells und bleibt ein eigenes Risiko
(R-025); ihn mindern die Property-Based Tests (QZ-3,
8.3). Wer das Repository übernimmt, erreicht über tutor.md
die Chat-KI eines Kindes (R-024); die Allowlist verhindert fremde
Links, aber nicht fremden Text in einem gemergten PR. Die Zwei-Faktor-Pflicht erledigt
R-017. Die fünf neuen Abhängigkeiten laufen nur in CI und lokal; eine
kompromittierte Version träfe den Build, nicht die Kinder (R-015).
Ändert sich eine Dimension, etwa durch Konten, ein Backend oder ein LLM im Code, wird das Repository neu
eingestuft; der Abschnitt „Risk Radar Assessment“ in CLAUDE.md hält den Stand für /risk-mitigate fest.
ADR-026: Browser-Tests mit Playwright und axe-core in einem eigenen Workflow
Kontext. Unit-, Vertrags- und Property-Based Tests laufen unter Node und sehen kein HTML. Ob eine Seite im Browser ohne Fehler lädt, keinen fremden Host anfragt, bei 360 px passt und ohne JavaScript Text und Bild zeigt, prüften bisher Agenten von Hand (TD-5). Der Product Owner hatte das am 23.09.2026 auf später gelegt und mit #26 (EPIC #18) eingeplant, ausdrücklich mit einem ADR für die neue Testabhängigkeit.
Entscheidung. @playwright/test und @axe-core/playwright kommen als exakt gepinnte devDependencies
dazu (Microsoft bzw. Deque, beide die verbreiteten Standardpakete). Getestet wird nur mit Chromium,
gegen das gebaute _site/, ausgeliefert von einem 40-Zeilen-Server auf Node-Bordmitteln
(e2e/server.js) statt eines weiteren Pakets. Die Tests laufen im eigenen Workflow browser.yml parallel
zum Pflicht-Check test-und-build; der bleibt unverändert und schnell. Der Browser liegt im Actions-Cache,
Schlüssel ist die Playwright-Version. Bei Fehlschlag lädt der Job den HTML-Bericht als Artefakt hoch.
Status. Accepted (inferred). Umsetzung von #26; der Product Owner entscheidet mit dem Merge.
| Ziel | Playwright + axe im eigenen Workflow (Referenz) | Schritt in pruefen.yml |
Weiter von Hand |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
−1 |
QZ-2 Tutor-Anschluss |
0 |
0 |
−1 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
−1 |
QZ-4 Neue Apps schnell |
0 |
−1 |
−1 |
QZ-5 Zugänglich |
0 |
0 |
−1 |
Summe |
0 |
−1 |
−5 |
Beide automatischen Varianten prüfen bei jedem Push, was vorher vom Gedächtnis des Agenten abhing:
externe Requests (QZ-1), Deep Links und „Zurück zu Claude“ (QZ-2), die Übungsrunde (QZ-3), 360 px und
WCAG (QZ-5); von Hand fehlt das, sobald ein Agent es vergisst. Ein Schritt in pruefen.yml verlängert den
Pflicht-Check um die Browser-Installation, der eigene Workflow läuft parallel (QZ-4); von Hand kostet jede
neue App dieselbe Durchsicht noch einmal.
Konsequenzen. Zwei neue Testabhängigkeiten und ein Browser-Download in CI vergrößern die Lieferkette des
Builds, nicht die der Site: Ausgeliefert wird nichts davon. Das Risiko entspricht R-015 (Eleventy) und wird
wie dort gemildert: exakte Pins, npm ci, npm audit, Dependabot. Browser-Tests können flackern; bisher
nutzen sie keine Wartezeiten, nur Playwright-Assertions mit Auto-Wait, und laufen ohne Wiederholung, damit
ein Flackern auffällt. Dieses Risiko ist bewusst ohne eigene Risiko-ID akzeptiert. Bis der Job browser
Pflicht-Check ist, verhindert ein roter Browser-Test den Merge nicht (Branch-Schutz: Check „browser“ ergänzen).
Nachtrag vom 25.09.2026. Seit dem 24.09.2026 ist browser Pflicht-Check im Branch-Schutz von main;
ein roter Browser-Test verhindert den Merge.
ADR-024: Wahrscheinlichkeitsbäume von oben nach unten; dreistufige quer mit Dreh-Hinweis
Kontext. Die Zufall-App zeichnete jeden Baum waagerecht: Wurzel links, Blätter rechts, Knoten als Pillen mit vollem Namen. Ein zweistufiger Baum war so 450 px breit, ein dreistufiger 600 px. Auf dem Handy (360 px) hat das Übungsbild aber nur 279 px Platz, „Bild dazu“ 313 px. Das Kind musste auf fast jeder Baum-Seite waagerecht scrollen und sah nie den ganzen Baum; QS-12 verlangt das Gegenteil. Ein Experiment auf der Seite „Ohne Zurücklegen“ (PR #36) zeichnete die Bäume hochkant: Wurzel oben, Blätter als 24-px-Kreise mit Kürzel (R, B, G) und Legende, Pfadwahrscheinlichkeiten unter den Blättern. Ein Baum aus 27 Blättern (drei Farben, drei Züge) passt so aber in keine 279 px.
Entscheidung. zeichneBaumIn (src/zufall/js/vis/baum.js) zeichnet jeden Baum hochkant, wenn er
so in 279 px passt (MAX_BREITE, breiteUnten in baum-unten.js). Das trifft alle zweistufigen
Urnen-, Münz- und Würfelbäume und die dreistufigen mit zwei Ergebnissen je Stufe (Münze, „6 / keine 6“,
zwei Farben); dort stehen die Pfadwahrscheinlichkeiten bei Bedarf abwechselnd in zwei Zeilen. Die Seite
„Gegenereignis“ zeichnet dreistufige Bäume nur mit „gelb / nicht gelb“ statt aller Farben (Lektorat L-040,
24.09.2026); dort liegt kein Baum quer, der Dreh-Hinweis erscheint nicht. Zwischen Geschwistergruppen liegt eine
Lücke von bis zu 14 px, so groß, wie die 279 px es zulassen. Fehlende Zweige der Seite „Baumdiagramm“
zeigen am Zweigende nur „?“, der Buchstabe steht auf der Mitte des Zweigs. Passt ein Baum nicht, liegt
er quer wie bisher; die Gruppe trägt die Klasse baum-quer. Dann zeigt zufall.css auf schmalen
Hochkant-Bildschirmen (orientation: portrait, höchstens 40rem) darüber den Hinweis „Tipp: Dreh dein
Handy, dann siehst du den ganzen Baum.“ Der Hinweis ist reines CSS; der Baum bleibt sichtbar und
scrollt. Das gilt für „Bild dazu“, die Übungsbilder, die Testseite und „Baum bauen“.
Status. Accepted. Vom Product Owner entschieden am 24.09.2026.
| Ziel | Hochkant, sonst quer mit Dreh-Hinweis (Referenz) | Quer wie bisher, waagerecht scrollen | Nur Dreh-Hinweis, alle Bäume quer | CSS-Umschaltung nach Bildschirmbreite |
|---|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
+1 |
+1 |
−1 |
QZ-5 Zugänglich |
0 |
−1 |
−1 |
? |
Summe |
0 |
0 |
0 |
−1 + ? |
Quer mit Scrollen braucht keinen neuen Code (QZ-4 +1), aber das Kind sieht auf dem Handy nie den
ganzen Baum und muss für jeden Pfad scrollen (QZ-5 −1). Nur der Dreh-Hinweis ist ebenso billig (QZ-4
+1), zwingt aber jedes Kind auf jeder Baum-Seite, das Handy zu drehen, auch für Bäume, die hochkant
passen würden (QZ-5 −1). Die CSS-Umschaltung legt beide Zeichnungen ins HTML und blendet eine per
Media Query aus. Jedes Bild entstünde zweimal, und die anklickbaren Blätter müssten in beiden
Zeichnungen denselben Zustand halten (QZ-4 −1). Ob Screenreader die ausgeblendete Kopie sauber
überspringen, ist ?. Die Summen trennen die Referenz nicht von den billigen Alternativen; den Ausschlag
gibt QS-12: Auf 360 px soll das Kind den Baum ohne Scrollen sehen, und das schafft nur die Referenz für
die zweistufigen Bäume, die den Großteil der Aufgaben ausmachen.
Konsequenzen. Dreistufige Bäume mit mehr als zwei Ergebnissen scrollen auf 360 px weiter; der
Dreh-Hinweis mildert das nur (R-027, bewusst akzeptiert). Auf
einer Übungsseite wechselt die Richtung mit der Aufgabe: ein zweistufiger Baum steht hochkant, ein
dreistufiger mit mehr als zwei Ergebnissen liegt quer. Ob ein Baum passt, schätzt der Code aus der Textbreite (Ziffer 0,636,
Schrägstrich 0,337 je px Schriftgröße, so breit wie DejaVu Sans). Eine noch breitere Systemschrift
könnte Brüche in der Gruppe berühren lassen
(R-028); die Tests prüfen die Überlappung mit 0,6 je Zeichen, der
Browser-Check mit getBoundingClientRect. Die Bildtexte der Seiten sagen „unter den Enden“ statt
„rechts“; llms.txt und tutor.md beschreiben beide Richtungen
(8.12).
ADR-025: Original-Harness-Rad als Zwei-Klick-Einbettung in der Doku
Kontext. Kapitel 8.16 zeigt den Harness als eigenes, statisches SVG aus scripts/harness-rad.js.
Wer den Stand im Original-Rad von Semantic Anchors prüfen wollte, musste bisher eine
localStorage.setItem(…)-Zeile in die Browser-Konsole kopieren. Seit
LLM-Coding/Semantic-Anchors#748 liest das Original den Stand aus der URL
(harness-coverage-wheel.html?tier=2#on=<ids>&na=<ids>) und kennt die Marke „nicht zutreffend“.
„In Arbeit“, „geplant“ und „verworfen“ hat es bewusst nicht übernommen: Sie sagen nichts darüber,
was der Harness heute abdeckt. Die Doku selbst lädt bisher nichts von fremden Hosts; doku.yml
prüft das mit lib/pruefe-ausgabe.js in jedem PR.
Entscheidung. scripts/harness-rad-original.js baut aus ROH die URL: „vorhanden“ wird on,
„entfällt“ wird na, alles andere bleibt Lücke. Eine Schicht-id, die das Original nicht kennt, bricht den
Generator ab. Unter dem SVG stehen der Link „Unseren Stand im Original öffnen“, beide Prozentwerte und
ein lokaler Platzhalter mit dem Hinweis „Beim Laden wird die Seite llm-coding.github.io (GitHub Pages)
aufgerufen.“ Erst der Klick auf den Knopf setzt ein <iframe> mit loading="lazy",
referrerpolicy="no-referrer" und sandbox="allow-scripts allow-same-origin". Die URL steht bis dahin
nur im Attribut data-quelle; ein kleines Inline-Skript im erzeugten Abschnitt setzt den Rahmen und
akzeptiert nur Adressen, die mit der Original-Seite beginnen. Ohne JavaScript bleibt der Platzhalter
versteckt: SVG und Link genügen. Das Muster folgt den Videos (ADR-005).
Status. Accepted. Vom Product Owner entschieden am 24.09.2026.
| Ziel | Zwei-Klick-Einbettung plus Link (Referenz) | Nur Link | Nur statisches SVG |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
+1 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
+1 |
+1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
Summe |
0 |
+1 |
+2 |
Der Link allein ruft llm-coding.github.io ebenfalls erst beim Klick auf (QZ-1 0) und braucht kein Skript
(QZ-4 +1). Das SVG allein fragt nie einen fremden Host (QZ-1 +1) und braucht weder Skript noch Link
(QZ-4 +1), lässt den Leser aber unsere Einstufung nicht im Original nachprüfen. Die Summen sprechen für die
Alternativen, weil QZ-1 bis QZ-5 die Apps für Kinder bewerten; die Doku lesen Team und Reviewer. Den
Ausschlag gibt die Nachprüfbarkeit: Der Leser vergleicht unser Rad mit dem Original auf derselben Seite,
ohne Konsole und ohne den Stand von Hand nachzustellen. In der Sandbox läuft das Original vollständig
(Playwright, 24.09.2026: 52 %, 15 von 29, alle 21 Häkchen und 15 n/a-Marken); allow-same-origin
braucht es für seinen localStorage. Weil die Doku auf lernapps.github.io und das Original auf
llm-coding.github.io liegen, gibt allow-same-origin dem Rahmen keinen Zugriff auf die Doku.
Konsequenzen. Die Doku stellt jetzt genau eine externe Anfrage, und nur nach einem Klick: an
llm-coding.github.io, also an GitHub Pages, denselben Anbieter, der die Site selbst ausliefert. Einen
neuen Dritten gibt es damit nicht; das nehmen wir ohne eigenes Risiko hin. pruefe-ausgabe bleibt ohne
Ausnahme grün, weil die URL nur in data-quelle und im <a href> steht. Ändert das Original sein
URL-Format oder seine ids, zeigt der Link einen falschen Stand
(R-029). Die Prozentwerte stimmen heute überein, weil auch unser
Rad nur Vorhandenes zählt; das Original zeigt aber nicht, welche Lücken in Arbeit oder geplant sind.
ADR-027: KI-Review in frischem Kontext als Pflichtschritt vor dem Merge
Kontext. Claude-Code-Agenten schreiben fast den ganzen Code. Das Review hängt an einer Person: Der Product Owner sieht jeden PR durch und merged ihn (ADR-023, R-026). Das Vibe-Coding Risk Radar verlangt für Tier 2 zusätzlich ein KI-Code-Review. Die Session, die einen PR schreibt, ist dafür der falsche Prüfer: Sie kennt ihre Absicht und liest deshalb, was sie gemeint hat, nicht was dasteht. Eine Freigabe lässt sich über den Branch-Schutz nicht erzwingen, denn ein einzelner Maintainer kann seinen eigenen PR nicht freigeben.
Entscheidung. Vor jedem Merge prüft ein Reviewer in frischem Kontext den PR: ein neuer Sub-Agent oder
eine neue Session, nie die Autoren-Session, am besten ein anderes Modell. Er folgt dem Prompt
werkzeuge/review/ki-review.md, einer Checkliste nach Fagan: Korrektheit, Tests mit Bezug auf Issue oder
Befund, Sicherheit nach OWASP (innerHTML, eval, externe Anfragen, Tutor-Link-Allowlist), Static
First, Datenschutz, Lehrerstimme und richtige Mathematik, ADR, Doku und Version. Sein Ergebnis postet er
als PR-Review vom Typ „Comment“; es beginnt mit ## KI-Review und nennt Stand: <Kopf-SHA> und
Ergebnis: freigegeben oder Ergebnis: Änderungen nötig.
Der Check ki-review (.github/workflows/ki-review.yml, scripts/ki-review-pruefen.js) macht den
Schritt sichtbar. Er ist grün, wenn das neueste KI-Review eines berechtigten Kontos nach dem letzten
Commit kam, den Kopf-Commit nennt und „freigegeben“ sagt. Nach jedem neuen Commit fehlt er, bis ein neues Review kommt. Der
Check ruft kein Sprachmodell auf, braucht kein Secret und liest nur, mit dem GITHUB_TOKEN und
Leserechten. Er startet nur bei pull_request_review: Ein issue_comment-Lauf hinge am letzten
Commit von main und erschiene nicht im PR, und ein zusätzlicher pull_request-Lauf ließe sein rotes
Ergebnis neben dem grünen des Reviews am selben Commit stehen (beim Dogfooding in #43 beobachtet). Ohne
Review steht der Pflicht-Check auf „Expected“ und sperrt den Merge. Pflicht im Branch-Schutz macht ihn der Product
Owner mit einem Aufruf (8.2 M-23).
Status. Accepted. Vom Product Owner entschieden am 24.09.2026.
| Ziel | Frischer Kontext plus Check (Referenz) | Claude-GitHub-Action mit API-Schlüssel | Copilot-Code-Review | Nur menschliches Review |
|---|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
? |
0 |
QZ-3 Richtige Rückmeldung |
0 |
+1 |
? |
−1 |
QZ-4 Neue Apps schnell |
0 |
+1 |
+1 |
+1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
0 |
Summe |
0 |
+2 |
+1 + ? |
0 |
Die Claude-GitHub-Action (anthropics/claude-code-action) prüft jeden Push von selbst; niemand muss
daran denken (QZ-3 +1), und kein Schritt kommt hinzu (QZ-4 +1). Sie verlangt aber einen bezahlten
API-Schlüssel als Secret im Repository: laufende Kosten und ein neues Geheimnis, das der Product Owner
anlegen und schützen muss (T-001). Wir verwerfen sie vorerst; sie ist die naheliegende Erweiterung, wenn
Kosten und Secret tragbar werden. Das Copilot-Code-Review braucht ein Abonnement; ob es die Regeln aus
CLAUDE.md, die Tutor-Texte und die Mathematik so genau prüft wie unsere Checkliste, ist offen (?).
Nur menschliches Review spart den Schritt (QZ-4 +1), lässt aber alles an einer eiligen Person hängen
(QZ-3 −1). Die Summen sprechen für die Action; den Ausschlag geben Kosten und Secret, die keines der
Qualitätsziele misst.
Konsequenzen. Jeder PR bekommt einen zweiten, dokumentierten Blick; offene Befunde brauchen eine
Begründung im PR. Das mindert R-026, löst es aber nicht:
Autor und Reviewer posten über dasselbe Konto, der Check prüft also Konto, Zeitpunkt und Stand, nicht die
Unabhängigkeit des Kontexts. Ein Review aus demselben Modell teilt dessen blinde Flecken. Workflow und
Skript kommen aus dem PR-Zweig; ein PR könnte den Check selbst aufweichen, deshalb
gehört jede Änderung an ki-review.yml und ki-review-pruefen.js ausdrücklich ins Review. Admins
können den Branch-Schutz weiter umgehen. Ein fehlerhaftes Beispiel in einem Lerntext fängt das Review nur,
wenn es nachrechnet; die Checkliste verlangt das (R-025).
ADR-028: asciidoc-linter prüft die Architektur-Doku, gepinnt auf einen Commit
Kontext. Die Architektur-Doku ist AsciiDoc und wächst mit jedem PR. Kaputte Überschriften,
Markdown-Syntax oder unvollständige Tabellen fallen erst auf, wenn jemand die gebaute Seite liest. Das
Harness-Rad führt „Markdown / AsciiDoc lint“ als extrinsische Schicht ab Tier 1; sie stand auf „offen“
(8.16). docToolchain bietet mit
asciidoc-linter einen Linter aus demselben Umfeld
wie unser Doku-Build. Er ist ein Python-Werkzeug ohne Release, ohne Tag und ohne PyPI-Paket. Das README
zeigt ein MIT-Badge, GitHub erkennt aber keine LICENSE-Datei.
Entscheidung. Der Job doku-bauen-und-pruefen in .github/workflows/doku.yml installiert
asciidoc-linter per pip install git+https://github.com/docToolchain/asciidoc-linter@<SHA>, gepinnt auf
den vollen Commit-SHA 911440ac35d5849349389cce7ba04311d75dca49 (angehoben am 24.09.2026, siehe zweiten Nachtrag); Python kommt über
actions/setup-python, ebenfalls per SHA gepinnt. Der Wrapper scripts/doku-lint.js ruft den Linter mit
--format json über src/docs/*/.adoc auf: ERRORs brechen den Job, WARNINGs erscheinen nur als
Annotation. Der Linter selbst endet bei jedem Fund mit Exit-Code 1, auch bei einer Warnung; deshalb der
Wrapper. Fehlalarme unterdrücken wir nur mit Begründung: Die Regel IMG001 sucht Bilder relativ zum
Arbeitsverzeichnis und kennt :imagesdir: nicht. Abschalten ließe sie sich nur ganz (Konfiguration
rules.IMG001.enabled), dann fehlten auch echte Funde. Der Wrapper verwirft „Image file not found“ nur,
wenn das Bild über das :imagesdir: der Datei existiert. Ein Update heißt: neuen SHA eintragen, Linter
lokal laufen lassen, Funde beheben, PR.
Status. Accepted. Vom Product Owner entschieden am 24.09.2026.
| Ziel | asciidoc-linter per SHA (Referenz) | Asciidoctor mit --failure-level=WARN |
Vale mit eigenen Regeln | Kein Linter |
|---|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
? |
−1 |
0 |
QZ-5 Zugänglich |
0 |
0 |
0 |
0 |
Summe |
0 |
? |
−1 |
0 |
Keine Option berührt die Apps; alle Ziele außer QZ-4 bleiben gleich. Asciidoctor warnt beim Rendern etwa
vor fehlenden Includes und Ankern; ob --failure-level=WARN im docToolchain-Build ohne Fehlalarme aus dem
Theme läuft, ist offen (?). Vale prüft Stil und Sprache, braucht aber Regeln, die wir erst schreiben
müssten (QZ-4 −1). Kein Linter spart den Schritt, lässt die Schicht aber offen. Die Summen entscheiden
nicht; den Ausschlag gibt, dass asciidoc-linter ohne eigene Regeln Struktur und Markdown-Reste prüft und
aus dem docToolchain-Umfeld kommt.
Konsequenzen. Die Schicht „Markdown / AsciiDoc lint“ steht auf „vorhanden“ (siehe die Nachträge);
Markdown-Dateien prüft sie nicht. Das Werkzeug ist ein reines Entwicklungswerkzeug: Es läuft nur in doku.yml mit Leserechten und
ohne Secret, seine Ausgabe gelangt nicht in die Site. Eine kompromittierte Version (T-012) könnte also den
Doku-Job, aber keine App verändern; der SHA-Pin schließt ungeprüfte Updates aus. Eine fehlende
LICENSE-Datei akzeptieren wir für ein Werkzeug, das wir nur ausführen und nicht ausliefern. Beides ist
bewusst akzeptiert, ohne eigenes Risiko in Kapitel 11. Fällt GitHub oder der Linter-Commit weg, scheitert
nur der Doku-Job in PRs, nicht das Deployment (pages.yml ruft den Linter nicht auf;
R-011 bleibt unverändert). Der Linter ist jung: Einige Regeln melden
in unseren Proben nichts, wo sie sollten; er ergänzt das Review, ersetzt es nicht.
Nachtrag 24.09.2026 (berichtigt). Die Kernregeln feuern in der zuerst gepinnten Version 52e7fa5 nicht.
Nachgestellt mit Proben: Ein übersprungener Überschriften-Level, ein nicht geschlossener -----Block, eine
kleingeschriebene Überschrift und eine Markdown-Überschrift ## in einer .adoc-Datei ergeben jeweils
„No issues found“. Über das CLI still bleiben die Überschriftenregeln HEAD001–HEAD003, die Blockregeln
BLOCK001/BLOCK002 und die Bildregel IMG001; die Markdown-Regeln FMT004 (Markdown-Syntax) und FMT005
(Markdown-Tabellen) melden zuverlässig. Eine frühere Fassung dieses Nachtrags nannte nur FMT005; das war
falsch. Unser „0 Fehler“ belegt also wenig. Die Schicht „Markdown / AsciiDoc lint“ steht darum auf „in
Arbeit“, nicht auf „vorhanden“, bis der Fix upstream da ist. Der Schritt in doku.yml bleibt, weil FMT004,
FMT005 und der Wrapper funktionieren; nach dem Fix genügt es, den SHA anzuheben und die Schicht wieder auf
„vorhanden“ zu setzen.
Zweiter Nachtrag 24.09.2026. Upstream hat docToolchain/asciidoc-linter#62 den Fehler behoben: Die
Überschriften-, Block- und Bildregeln laufen jetzt auch über das CLI, der Inhalt wörtlicher Blöcke (----,
…., , ////) bleibt ungeprüft, und BLOCK002 überspringt Attribut- und Titelzeilen vor einem Block.
Der SHA steht jetzt auf 911440ac35d5849349389cce7ba04311d75dca49, die Schicht wieder auf „vorhanden“
(8.16). Der erste Lauf über unsere Doku fand zwei Warnungen: Eine
BLOCK002-Meldung in der erzeugten Tabelle _harness-inventar.adoc (Kommentarzeile direkt vor dem Block) ist
behoben, der Generator setzt jetzt eine Leerzeile. HEAD002 am Titel von arc42.adoc, der mit dem Bild-Makro für das arc42-Logo
beginnt, ist ein Fehlalarm, weil die Regel ein Inline-Makro am Anfang für ein kleingeschriebenes Wort hält; der Wrapper
unterdrückt HEAD002 nur für Überschriften, die mit einem Inline-Makro beginnen. Offen upstream bleiben #58
(Exit-Code und --fail-level; die Schwere bewertet weiter der Wrapper), #59 (Fehler in der Konfiguration
werden ignoriert) und #60 (:imagesdir:; der Wrapper filtert IMG001 weiter). Am 24.09.2026 kamen #63 (HEAD002
meldet eine Überschrift, die mit einem Inline-Makro beginnt; genau diesen Fall unterdrückt der Wrapper
scripts/doku-lint.js) und #64 (BLOCK002 bei Zeilenkommentaren und am Dateianfang) dazu.
ADR-029: Lizenzprüfung ohne neue Abhängigkeit plus Dependency Review
Kontext. Die Site liefert keine npm-Pakete aus; alle 236 Pakete in package-lock.json sind
devDependencies, die bauen und prüfen. Am 25.09.2026 trugen alle eine Lizenz: MIT 161, Apache-2.0 36,
BSD-2-Clause 15, ISC 13, BSD-3-Clause 4, BlueOak-1.0.0 3, MPL-2.0 3 (axe-core, @axe-core/playwright,
eslint-plugin-no-unsanitized), Python-2.0 1 (argparse). Ausgeliefert wird nur fremder Code unter
src/kern/vendor/ (talkitover.js, MIT im Dateikopf). Geprüft hat das bisher niemand: Ein Update oder ein
neues Paket könnte eine Copyleft-Lizenz oder gar keine mitbringen, ohne dass es auffällt. Das Harness-Rad
führt die Schicht „License compliance“ ab Tier 2 (8.16).
Entscheidung. Zwei Stufen. Erstens prüft lib/pruefe-lizenzen.js ohne neue Abhängigkeit in npm test
(Pflicht-Check test-und-build) jede Lizenz im Lockfile gegen eine Allowlist, die von der Verwendung
abhängt: ausgelieferte Pakete nur permissiv (MIT, ISC, Apache-2.0, BSD-2-Clause, BSD-3-Clause, 0BSD,
BlueOak-1.0.0, CC0-1.0, Unlicense, Python-2.0), dev-Pakete zusätzlich MPL-2.0. MPL-2.0 ist
Datei-Copyleft und betrifft nur weitergegebene, geänderte Dateien; im Build laufen diese Pakete nur. SPDX-Ausdrücke
wertet die Prüfung aus: OR genügt ein erlaubter Teil, AND brauchen alle. Jede Datei unter src/**/vendor/
braucht einen Lizenzkopf. Zweitens läuft actions/dependency-review-action (v5.0.0, per Commit-SHA gepinnt)
in abhaengigkeiten.yml auf jedem PR mit derselben dev-Allowlist und fail-on-severity: high. Es sieht nur
die Änderung im PR, meldet aber schon vor dem Merge und mit GitHubs Lizenz- und Schwachstellendaten. Eine
Änderung der Allowlist braucht ein neues ADR.
Status. Accepted. Vom Product Owner entschieden am 25.09.2026.
| Ziel | Eigene Prüfung plus Dependency Review (Referenz) | license-checker-rseidelsohn |
Keine Prüfung |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
−1 |
0 |
QZ-5 Zugänglich |
0 |
0 |
0 |
Summe |
0 |
−1 |
0 |
Keine Option berührt die Apps selbst. license-checker-rseidelsohn liest node_modules und kann mehr (Berichte,
Lizenztexte), bringt aber eine weitere devDependency mit eigenem Paketbaum und damit mehr Lieferkette (T-012);
jedes Update muss geprüft werden (QZ-4 −1). Keine Prüfung kostet nichts, lässt die Schicht aber offen, und
eine GPL-Lizenz fiele erst bei einer Weitergabe auf. Die Summen entscheiden nicht; den Ausschlag geben die
Linie der Strukturprüfung (ADR-008: eigene Prüfung ohne Abhängigkeit) und dass das Lockfile die Lizenzen schon enthält.
Konsequenzen. Die Schicht „License compliance“ steht auf „vorhanden“ (8.2 M-24). Die Prüfung vertraut dem
Feld license aus dem Lockfile, also der Angabe im package.json des Pakets; eine falsche Angabe dort sieht
sie nicht. Dependency Review braucht den Abhängigkeitsgraphen von GitHub. Seit dem 25.09.2026 ist der Job
abhaengigkeiten Pflicht-Check in required_status_checks (wie bei M-23); ein Ausfall von GitHubs Dienst hält
damit Merges auf, bis er zurück ist. Die Action ist ein Werkzeug von außen im PR-Lauf mit Leserechten
(R-015); der SHA-Pin schließt ungeprüfte Updates aus. Die Lizenz des
Projekts selbst regelt diese Entscheidung nicht; sie bleibt offen (R-018).
ADR-030: Architektur-Review nach ATAM bei Architekturänderungen
Kontext. Das KI-Review (ADR-027) prüft jeden PR auf Korrektheit, Sicherheit, Datenschutz und Mathematik. Ob eine neue Entscheidung zu den Qualitätszielen passt, fragt seine Checkliste nur mit einem Satz: „ADR mit Pugh-Matrix und Risiko-IDs“. Das reicht, um ein fehlendes ADR zu finden, aber nicht, um eine Abwägung zu prüfen. Am 25.09.2026 entstand die erste ATAM-Bewertung (Anhang Bewertungen) mit einem Utility Tree (Kapitel 10). Eine Baseline allein veraltet mit dem nächsten ADR; ein ATAM in jedem PR kostet bei Text- und App-PRs Zeit, ohne etwas zu finden. Das Harness-Rad führt „LLM design review“ ab Tier 2 als offene Lücke (8.16).
Entscheidung. Ein PR bekommt ein Architektur-Review nach ATAM nur, wenn er Entscheidungen oder
Qualitätsdefinitionen ändert. Auslöser sind ADR-Texte (src/docs/arc42/chapters/adr-.adoc), die
datierten ATAM-Berichte selbst (_atam-.adoc, sie sind die Bewertung, kein Nachtrag), Kapitel 9
(09*.adoc), die Qualitätsziele in Kapitel 1 (01_*.adoc), die Szenarien in Kapitel 10 (10_*.adoc),
die Lösungsstrategie in Kapitel 4 (04_*.adoc) und die Bausteine in Kapitel 5 (05_*.adoc). Die Liste
steht an einer Stelle, ARCHITEKTUR_PFADE in scripts/ki-review-pruefen.js; Kapitel 8 und der Rest von
Kapitel 11 lösen nicht aus, weil fast jeder PR sie nachführt und sie Entscheidungen umsetzen, nicht treffen. Der Reviewer
schreibt dann den Abschnitt # Architektur (ATAM) nach werkzeuge/review/ki-review.md: betroffene
Szenarien, neue Sensitivity und Tradeoff Points, neue Risiken mit R-ID oder „keine“ und ob Pugh-Matrix und
Konsequenzen eines neuen ADR zu QZ-1 bis QZ-5 passen. Der Check ki-review holt die geänderten Dateien
lesend über pulls/{n}/files und bleibt rot, wenn der Abschnitt fehlt. Die Baseline wiederholen wir jedes
Quartal mit dem Harness-Audit.
Status. Accepted. Vom Product Owner entschieden am 25.09.2026.
| Ziel | ATAM bei Architekturänderung (Referenz) | ATAM in jedem PR | Nur Baseline je Quartal | Kein Architektur-Review |
|---|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
−1 |
−1 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
−1 |
−1 |
QZ-4 Neue Apps schnell |
0 |
−1 |
+1 |
+1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
? |
Summe |
0 |
−1 |
−1 |
−1 + ? |
Ein ATAM in jedem PR findet nichts, was die Auslöser nicht auch finden: Eine App oder ein Lerntext ändert
keine Abwägung, kostet aber bei jedem der rund 20 App-PRs je Schuljahr einen Abschnitt (QZ-4 −1). Nur die
Baseline spart diesen Schritt (QZ-4 +1); ein neues ADR, das zum Beispiel den Tutor-Vertrag oder den Kern
berührt, bliebe bis zu drei Monate ungeprüft gegen die Szenarien (QZ-2, QZ-3 −1). Ohne Review fehlt
auch die Baseline. Ob das die Zugänglichkeit trifft, deren automatische Prüfung nur Stichproben zieht
(R-030), ist offen (QZ-5 ?); die Wahl ändert es nicht.
Konsequenzen. Die Harness-Schichten „LLM design review“ (Tier 2) und „ATAM“ stehen auf „vorhanden“.
Der Check prüft nur, dass der Abschnitt da ist, nicht, dass er etwas taugt: Ein Reviewer kann ihn als
Formalie ausfüllen (R-032). Autor und Reviewer bleiben dasselbe Konto
(R-026). PRs, die nur Tippfehler in Kapitel 1 oder 10 beheben, brauchen den
Abschnitt trotzdem; das ist bewusst akzeptiert, ein Satz „keine Szenarien betroffen“ genügt. Wer die
Auslösepfade ändert, ändert Skript, Test, ki-review.md und dieses ADR gemeinsam.
Nachtrag vom 25.09.2026: Bewertungen stehen im Anhang. Der Product Owner hat entschieden, dass datierte
Bewertungen Momentaufnahmen sind und weder in das Konzeptkapitel 8 noch in das Risikokapitel 11 gehören. Sie
stehen seitdem im Anhang Bewertungen nach Kapitel 12, jede
Runde als eigener datierter Abschnitt; die Dateien heißen _atam-JJJJ-MM-TT.adoc,
_security-JJJJ-MM-TT.adoc und _harness-audit-JJJJ-MM-TT.adoc. Kapitel 11 behält Risiken und
Risikothemen, Kapitel 10 den Utility Tree, 8.1 und 8.2 die Bedrohungen und Maßnahmen mit Link auf den
Bericht. Der Auslösepfad _atam-*.adoc bleibt und trifft jeden datierten ATAM-Bericht, auch die künftigen.
Der Security-Bericht und das Harness-Audit lösen nicht aus: Sie bewerten keine Abwägung gegen den Utility
Tree, sondern Bedrohungen und Prüfschichten, und ihre Ergebnisse landen in Kapitel 8 und 11, die aus
demselben Grund nicht auslösen. Verlangt ein Befund eine Entscheidung, bekommt sie ein ADR, und das löst
aus. Die Einleitung des Anhangs (13_bewertungen.adoc) löst ebenfalls nicht aus; sie bindet die Berichte
nur ein.
ADR-031: Kennzahlen der Übersichtsseite beim Doku-Build erzeugen
Kontext. Die Übersichtsseite (8.17) zeigt Zahlen, die sich mit fast jedem PR ändern: Tests, Kompetenzen, Risiken, ADRs. Von Hand gepflegt veralten sie. Das Harness-Rad löst dasselbe Problem mit eingecheckten, erzeugten Dateien und einem Test, der sie gegen den Generator prüft. Bei Zahlen, die jeder PR bewegt, hieße das: Jeder PR ändert dieselben Dateien, und zwei offene PRs geraten in einen Merge-Konflikt.
Entscheidung. scripts/dashboard.js erzeugt die Dateien der Übersicht beim Doku-Build.
scripts/dtc-v4.sh ruft es vor docToolchain auf; doku.yml lintet die AsciiDoc-Quellen nach dem Build.
Die Dateien sind git-ignoriert. Tests zählt es mit einem echten node --test-Lauf und mit
playwright test --list; deshalb laufen vor dem Doku-Build npm ci und npm run build. test/build/dashboard.test.js läuft in npm test gegen das echte
Repository, damit ein Formatbruch im Pflicht-Check test-und-build auffällt und nicht erst beim Deployment.
Status. Accepted. Vom Product Owner entschieden am 25.09.2026: erst die Übersichtsseite (Option 2), dann die Erzeugung beim Build statt eingecheckter Dateien.
| Ziel | Beim Doku-Build erzeugen (Referenz) | Eingecheckt, Test prüft | Von Hand pflegen |
|---|---|---|---|
QZ-1 Datenschutz |
0 |
0 |
0 |
QZ-2 Tutor-Anschluss |
0 |
0 |
0 |
QZ-3 Richtige Rückmeldung |
0 |
0 |
0 |
QZ-4 Neue Apps schnell |
0 |
−1 |
−1 |
QZ-5 Zugänglich |
0 |
0 |
0 |
Summe |
0 |
−1 |
−1 |
Eingecheckte Dateien kosten jeden PR einen Generatorlauf und bringen Merge-Konflikte (QZ-4 −1); von Hand gepflegte Zahlen veralten still (QZ-4 −1). Die Qualitätsziele der Apps berührt keine der drei Varianten.
Konsequenzen. Die Zahlen können nicht driften, und kein PR ändert erzeugte Dateien. Dafür hängt der
Doku-Build an einem weiteren Skript: Liest es eine Tabelle nicht mehr, scheitert der Build, und
pages.yml deployt auch keine App (R-011). Der Test in npm test
fängt das vor dem Merge. Wer die Seite ohne den Build liest, etwa in der GitHub-Ansicht, sieht leere
Includes; das nehmen wir ohne eigenes Risiko hin.
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.