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

ADR-001

Statische Site auf GitHub Pages, kein Backend

Accepted

ADR-002

Vanilla-JavaScript-ES-Module ohne Framework und ohne Build

Superseded by ADR-012

ADR-003

Kern als GitHub-Template, in Apps kopiert

Superseded by ADR-012

ADR-004

Tutor-Schnittstelle aus llms.txt, tutor.md und Deep Links mit Aufgabennummer

Accepted

ADR-005

YouTube per Zwei-Klick mit lokalem Platzhalter, youtube-nocookie nach Klick

Accepted

ADR-006

Zahlenantworten: Rundung nach getippten Stellen, eigener Term-Auswerter, Live-Vorschau

Accepted

ADR-007

Diagnosetest ohne Noten, Ergebniszeile für den Tutor, nur im localStorage

Accepted

ADR-008

Eigene Strukturprüfung ohne Abhängigkeiten; im Monorepo als Build-Fehler

Accepted

ADR-009

Terme mit Variablen: eigener Parser, Gleichwertigkeit an festen Prüfstellen, Formprüfung am Syntaxbaum

Accepted

ADR-010

Versionsnummer ?v=<APP_VERSION> an allen Importen, Skripten und Stylesheets

Superseded by ADR-014

ADR-011

Primärfarbe folgt dem Fach, eine Tabelle, geprüft im Build

Accepted

ADR-012

Monorepo mit Eleventy 3.1.6, exakt gepinnt

Accepted

ADR-013

Kern einmal ausgeliefert, App-Konfiguration per Dependency Inversion

Accepted

ADR-014

?v=<Inhalts-Hash> setzt der Build

Accepted

ADR-015

Basis-URL an genau einer Stelle; Organisation lernapps, Repo lernapps.github.io

Accepted

ADR-016

Bilder aus derselben Zeichenfunktion für Build (Mini-DOM) und Browser

Accepted

ADR-017

Kompetenzseiten aus Markdown-Front-Matter mit festem Gerüst, Regeln als Build-Fehler

Accepted

ADR-018

Mathe-Karte im Monorepo, App-Einträge aus den App-Konfigurationen

Accepted

ADR-019

Neutrales Play-Symbol statt der Logo-Form von YouTube

Accepted

ADR-020

Keine Volltextsuche; Navigation über Bundesland, Klassenstufe und Fach

Accepted

ADR-021

„Zurück zu Claude“ schließt den Tab mit window.close() und prüft danach

Accepted

ADR-022

Rechenweg auf Papier, geprüft vom Tutor per Foto; kein Rechenweg-Feld in der App

Accepted

ADR-023

Risikoeinstufung nach Vibe-Coding Risk Radar: Tier 2

Accepted

ADR-024

Wahrscheinlichkeitsbäume von oben nach unten; dreistufige quer mit Dreh-Hinweis

Accepted

ADR-025

Original-Harness-Rad als Zwei-Klick-Einbettung in der Doku

Accepted

ADR-026

Browser-Tests mit Playwright und axe-core in einem eigenen Workflow

Accepted (inferred)

ADR-027

KI-Review in frischem Kontext als Pflichtschritt vor dem Merge

Accepted

ADR-028

asciidoc-linter prüft die Architektur-Doku, gepinnt auf einen Commit

Accepted

ADR-029

Lizenzprüfung ohne neue Abhängigkeit plus Dependency Review

Accepted

ADR-030

Architektur-Review nach ATAM bei Architekturänderungen

Accepted

ADR-031

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 + ?

Konsequenzen. Kein Betrieb, keine Kosten, keine Daten auf fremden Servern. Die App kann dem Tutor nichts melden; das Kind trägt Nummer und Ergebniszeile (ADR-004, ADR-007). Alles hängt an GitHub Pages (R-010) und an den Konten der Organisation (R-005, R-017).

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

Status. Superseded by ADR-012 (ein Repository) und ADR-013 (Kern einmal ausgeliefert).

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

Konsequenzen. Null fremde Requests bis zum Klick; danach erfährt Google die IP-Adresse (R-009). Der Merker gilt auf geteilten Geräten für alle (R-008). Je Seite ist nur ein Video vorgesehen (TD-17, zurückgestellt, bis eine Seite ein zweites braucht).

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 + ?

Konsequenzen. Die Prüfung ist numerisch, nicht bewiesen; die Formregeln sind eine Setzung (R-012). Fehler treffen alle Apps mit Termfeldern (R-004).

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

Konsequenzen. Titel und Adressen auf der Karte stimmen immer mit der App überein. Das Monorepo trägt jetzt auch die Lehrplandaten von 16 Ländern. Die alte Karte lief parallel weiter, bis sie am 23.09.2026 gelöscht wurde (R-016); Einträge nennen lizenz: "" (R-018).

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 (src/kern/js/)

Language

2

JavaScript ohne Typprüfung

Deployment

2

Öffentlich erreichbar, aber ohne Konten und ohne personenbezogene Daten (ADR-001)

Data

0

Keine Datenhaltung; im localStorage nur Stufen und ein Merker (8.2 M-07)

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 test-und-build (pruefen.yml)

Dependency-Check (T1)

umgesetzt, #20

npm audit --audit-level=high im Pflicht-Check; Versionen exakt gepinnt, npm ci

Linter (T1)

umgesetzt, #21

ESLint Flat Config mit no-eval, no-implied-eval, no-unsanitized (eslint.config.js)

Type Checking (T1)

umgesetzt, #25

tsc --checkJs mit strict im Pflicht-Check (npm run typecheck, jsconfig.json), vorerst nur src/kern; die Apps folgen

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 ki-review (ADR-027)

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 browser.yml: Smoke, Übung, 360 px, axe WCAG 2.2 AA, ohne JS (ADR-026)

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.