Querschnittliche Konzepte: Wenig Laufzeit-Technik, viele Regeln, die der Build erzwingt

Die Apps haben kaum Technik, die man zur Laufzeit absichern, beobachten oder skalieren müsste. Umso mehr hängt an Regeln, die jede Datei einhält, und seit dem Monorepo prüft der Build sie. Die ersten fünf Abschnitte sind in jedem System Pflicht; ab 8.6 folgen die Anliegen, die diese Apps wirklich haben, jedes mit dem ADR, das es entschieden hat.

8.1 Bedrohungsmodell: Die größten Gefahren kommen über Konten, Pakete und Texte, nicht über einen Server

Das Modell folgt STRIDE. Schützenswert sind die Privatsphäre des Kindes, die Richtigkeit dessen, was Tutor und App ihm sagen, und die Integrität des einen Repositorys, aus dem alle Apps entstehen. Einen Server oder eine Datenbank gibt es nicht (ADR-001). Neu seit dem Monorepo: Zur Build-Zeit läuft fremder Code (Eleventy, docToolchain) und eigener Code der Apps in Node.

ID STRIDE Bedrohung Betroffen

T-001

S, T

Jemand übernimmt das Konto eines Organisationsmitglieds oder ein Token und ändert Kern, Apps oder tutor.md. Alle Kinder aller Apps bekommen die veränderte Fassung.

Repository, alle Apps

T-002

T

Prompt Injection über tutor.md oder llms.txt: Ein Beitrag oder eine gefälschte Kopie gibt Claude Anweisungen, die dem Kind schaden.

Apps (Tutor-Texte)

T-003

T

Manipulierter Deep Link: präparierte Parameter sollen Script einschleusen (XSS) oder eine irreführende Aufgabe zeigen.

Kern (Seitenstart, Aufgabenlink, Trainer)

T-004

T, E

Eine kompromittierte Fassung von src/kern/vendor/talkitover.js gelangt ins Repo und läuft auf jeder Startseite.

Kern

T-005

I

YouTube erfährt IP-Adresse und Browserdaten des Kindes schon beim Laden der Seite.

Kern (Video), Layout

T-006

I

Auf einem geteilten Schulgerät sieht das nächste Kind Selbsteinschätzung und letztes Testergebnis.

Browser-Speicher

T-007

I

Ein Beitrag oder ein Theme bindet eine fremde Ressource ein (CDN, Webfont, Analytics); jeder Aufruf meldet sich bei Dritten.

Layout, Kern, Apps, Doku

T-008

T, E

Eine Eingabe im Zahlenfeld wird als Code ausgeführt (eval, Function).

Kern (Zahlen)

T-009

D

Eine Eingabe wie 9^999999 oder ein riesiger Term legt den Tab lahm.

Kern (Zahlen)

T-010

I

Das Kind gibt im Tutor-Chat persönliche Daten preis; claude.ai verarbeitet sie nach eigenen Regeln.

claude.ai (außerhalb)

T-011

R

Nicht nachvollziehbar, wer tutor.md, llms.txt, Kern oder Build wann geändert hat.

Repository

T-012

T, E

Lieferkette: Eine kompromittierte Version von Eleventy, einer transitiven npm-Abhängigkeit oder von docToolchain schleust beim Build Code in die Ausgabe.

Build, Architektur-Doku

T-013

T, E

Ein Beitrag (PR) führt beim Build Code aus: App-Konfigurationen, Generatoren und Zeichenfunktionen laufen in Node.

Build, Tests und CI

T-014

I

Das Kind schickt Fotos seines Rechenwegs in den Chat (ADR-022). Der Chat-Anbieter erhält Handschrift und alles, was sonst auf dem Bild ist: Name, Schulstempel, Umgebung, je nach Gerät Metadaten wie den Aufnahmeort.

Chat-Anbieter (außerhalb)

T-015

S, T, E

tutor.md ist ein Prompt im Chat eines Kindes. Wer das Repository übernimmt oder einen PR durchbringt, gibt der Chat-KI Anweisungen, während sie mit dem Kind spricht. Ein Link auf eine fremde Seite genügt: Deren Inhalt ändert sich nach dem Review und lädt dann fremde Anweisungen in den Chat (ADR-023).

Apps (Tutor-Texte), Mathe-Karte (llms.txt)

T-016

T

Ein präparierter Deep Link setzt URL-Parameter auf Namen geerbter Objekt-Eigenschaften (constructor, toString). Nachschlagetabellen liefern dann eine Funktion statt eines Werts: falscher Titel, abgebrochene Aufgabe (Security-Review S-001).

Kern (URL-Parameter), Apps (Generatoren)

T-017

S, T

Ein Link in tutor.md oder llms.txt umgeht die Allowlist durch eine URL-Form, die der Build nicht erkennt: ein Schrägstrich statt zwei, Backslash, ../ hinter einem erlaubten Präfix, nackter Host mit Pfad, Port oder Query, UNC-Pfad, ein Issue, das jeder nach dem Review ändern kann, oder ein Fork-Commit unter der eigenen Repository-Adresse (Security-Review S-003).

Build (Allowlist), Apps (Tutor-Texte)

T-018

T, E

Ein Workflow bindet eine GitHub Action über ein verschiebbares Tag ein. Wer das Tag umhängt, führt Code im Build oder im Deploy mit pages: write aus (Security-Review S-002).

CI, Deployment

8.2 Sicherheit: Jede Maßnahme schließt eine benannte Bedrohung

ID Maßnahme Schließt Quelle / ADR

M-01

Kein Backend, keine Konten, keine Cookies.

T-006 (teilweise), T-010 (teilweise)

ADR-001

M-02

Der Build prüft die Ausgabe: keine externen src/href/srcset/data in script, link, img, iframe u. a.; keine externen import/@import/url() in JS und CSS. Jeder Fund bricht den Build.

T-007

lib/pruefungen.js:8-22; ADR-008, ADR-017

M-03

Zwei-Klick-Video: Platzhalter mit lokal gezeichnetem, neutralem Play-Symbol, kein Vorschaubild; iframe erst nach Klick, nur youtube-nocookie.com; Hinweis vor dem Klick.

T-005

src/kern/js/video.js:12-15, :61-79; ADR-005, ADR-019

M-04

URL-Parameter werden validiert: Nummer nur Ziffern, Zahlen nur > 0 und < 1e9, Texte nur Kürzel, Modus nur voll/schnell. Nur Generatoren erzeugen HTML.

T-003

src/kern/js/zufall.js:47, src/kern/js/aufgabenlink.js:49-54, src/kern/js/testablauf.js:15-18; ADR-004

M-05

Eigene Parser ohne eval; Exponent höchstens 64.

T-008, T-009

src/kern/js/bruch.js:65; ADR-006, ADR-009

M-06

TalkItOver liegt eingebettet im Repo (vendor/), Version und Lizenz im Dateikopf; Änderungen laufen als Diff durch den PR.

T-004 (teilweise)

src/kern/vendor/talkitover.js

M-07

Im localStorage liegen nur Stufen, Testnummer, Datum und ein Merker, kein Name, keine Antworten.

T-006 (teilweise)

src/kern/js/storage.js; ADR-007

M-08

Änderungen nur über Feature-Branch und PR; Git-Historie und Reviews machen jede Änderung nachvollziehbar.

T-011, T-002 (teilweise), T-001 (teilweise), T-013 (teilweise)

CLAUDE.md Project rules

M-09

tutor.md gibt Claude eine enge Rolle und bleibt unter 120 Zeilen, damit ein Review sie ganz liest.

T-002 (teilweise), T-010 (teilweise)

src/<app>/tutor.njk; CLAUDE.md

M-10

Die Startseite jeder App sagt vor dem Klick, dass „Mit Claude lernen“ claude.ai öffnet.

T-010 (teilweise)

src/start.njk

M-11

Workflows mit minimalen Rechten: pruefen.yml (auch für PRs) nur contents: read; pages.yml läuft nur auf main und hat nur pages: write, id-token: write.

T-001 (teilweise), T-013 (teilweise)

.github/workflows/

M-12

Eleventy exakt gepinnt (3.1.6, ohne ^), transitive Pakete über package-lock.json, in CI npm ci. Nie npx eleventy ohne installiertes Paket.

T-012 (teilweise)

package.json; CLAUDE.md; ADR-012

M-13

docToolchain am festen Commit 6de96fb7…, nicht an einem Zweig; der Cache hängt am SHA.

T-012 (teilweise)

scripts/dtc-v4.sh; .github/workflows/pages.yml

M-14

Die Doku lädt nichts von fremden Hosts: Theme-CSS ohne CDN-Importe, Schriften und Symbole lokal.

T-007

src/site/assets/css/; TD-10

M-15

Branch-Schutz für main, seit 23.09.2026: PR-Pflicht, Pflicht-Check test-und-build, kein Force-Push, kein Löschen. Admins dürfen ihn umgehen. Er verlangt keine Freigabe (required_approving_review_count: 0): Ein einzelner Maintainer kann seinen eigenen PR nicht freigeben.

T-001 (teilweise), T-011, T-013 (teilweise)

gh api repos/lernapps/lernapps.github.io/branches/main/protection; R-017

M-17

Die zweite Erklärung bei serlo.org ist ein reiner Link (<a rel="noopener"> unter #serlo): kein iframe, kein Vorschaubild, keine Anfrage vor dem Klick. Erst der Klick des Kindes öffnet de.serlo.org; dann erfährt serlo.org (Serlo Education e.V., gemeinnützig, werbefrei, ohne Anmeldung) IP-Adresse und Browserdaten wie bei jedem Link. Der Build lässt unter #serlo nur https://de.serlo.org/… zu, ohne Netzabruf.

T-007

src/_includes/kompetenz.njk; lib/pruefungen.js (pruefeSerloLinks)

M-18

Die App überträgt keine Fotos und hat kein Upload-Feld; nur das Kind schickt ein Foto, in seinem eigenen Chat-Werkzeug. tutor.md und llms.txt bitten, nur das Blatt zu fotografieren, ohne Namen. Ob Fotos in den Chat gehen, entscheiden das Kind und seine Eltern.

T-014 (teilweise)

src/<app>/tutor.njk, src/<app>/llms.njk; ADR-022

M-19

GitHub-Sicherheitsschalter seit 24.09.2026: Secret Scanning mit Push Protection (ein Push mit erkanntem Token scheitert), Dependabot-Alerts und -Security-Updates, CodeQL Default Setup für JavaScript und Actions.

T-001 (teilweise), T-012 (teilweise), T-015 (teilweise)

#19; ADR-023

M-20

Im Pflicht-Check test-und-build: npm audit --audit-level=high und ESLint mit no-eval, no-implied-eval und no-unsanitized. Ein Fund verhindert den Merge.

T-003, T-008, T-012 (teilweise)

.github/workflows/pruefen.yml, eslint.config.js; #20, #21; ADR-023

M-21

Zwei-Faktor-Pflicht für die Organisation lernapps, entschieden am 24.09.2026 und vom Product Owner in der GitHub-Oberfläche eingeschaltet. Ein gestohlenes Passwort reicht nicht mehr, um Kern, Apps oder tutor.md zu ändern.

T-001, T-015 (teilweise)

gh api orgs/lernapps (two_factor_requirement_enabled); R-017; ADR-023

M-22

Link-Allowlist für Tutor-Dateien: Der Build lässt in tutor.md und llms.txt jeder App, in /llms.txt und karte/llms.txt nur relative Links und Links auf https://lernapps.github.io/, das eigene Repository, https://de.serlo.org/ und https://www.youtube.com/watch?v= zu. Erkannt werden URLs mit Schema, protokoll-relative //host, www.host und javascript:/data:. Jedes andere Ziel bricht den Build. Seit dem Security-Review vom 25.09.2026 enger und strenger, siehe M-26.

T-015 (teilweise), T-002 (teilweise)

lib/pruefungen.js (pruefeTutorLinks, erlaubteTutorZiele); ADR-023

M-23

KI-Review vor jedem Merge: Ein Reviewer in frischem Kontext prüft den PR nach werkzeuge/review/ki-review.md (Fagan-Checkliste mit OWASP, Tutor-Link-Allowlist, Datenschutz, Mathematik) und postet ## KI-Review mit Stand: und Ergebnis:. Der Check ki-review ist nur für den geprüften Kopf-Commit grün. Pflicht im Branch-Schutz wird er mit gh api -X PATCH repos/lernapps/lernapps.github.io/branches/main/protection/required_status_checks --input - und {"strict":false,"checks":[{"context":"test-und-build","app_id":15368},{"context":"ki-review","app_id":15368}]}.

T-002 (teilweise), T-003 (teilweise), T-015 (teilweise)

.github/workflows/ki-review.yml, scripts/ki-review-pruefen.js; #24; ADR-027

M-24

Lizenzprüfung ohne neue Abhängigkeit: npm test (Pflicht-Check test-und-build) prüft jede Lizenz in package-lock.json gegen eine Allowlist, ausgelieferte Pakete nur permissiv, dev-Pakete zusätzlich MPL-2.0, dazu einen Lizenzkopf in jeder Datei unter src/**/vendor/. In jedem PR prüft abhaengigkeiten.yml (Dependency Review, per SHA gepinnt, seit 25.09.2026 Pflicht-Check abhaengigkeiten) neue Abhängigkeiten auf dieselben Lizenzen und auf Schwachstellen ab „high“. Eine Änderung der Allowlist braucht ein ADR.

T-012 (teilweise), T-004 (teilweise)

lib/pruefe-lizenzen.js, test/build/pruefe-lizenzen.test.js, .github/workflows/abhaengigkeiten.yml; ADR-029

M-25

URL-Parameter nur über eigene Schlüssel: leseModus und testLink prüfen mit Object.hasOwn, leseVorgaben verwirft Texte, die eine Eigenschaft von Object.prototype heißen.

T-016

src/kern/js/testablauf.js, src/kern/js/aufgabenlink.js; test/kern/testablauf.test.js, test/kern/aufgabenlink.test.js

M-26

Gehärtete Tutor-Allowlist: Der Build erkennt auch URLs mit einem Schrägstrich oder Backslash nach dem Schema, mailto:, UNC-Pfade und nackte Hosts mit Pfad, Port, Query oder Anker; .., %2e, %2f, %5c und \ in einer URL brechen den Build. Im eigenen Repository sind nur /blob/main/ und /tree/main/ erlaubt, keine Issues und keine Fork-Commits.

T-017, T-015 (teilweise)

lib/pruefungen.js (pruefeTutorLinks, erlaubteTutorZiele); test/build/pruefungen.test.js; ADR-023

M-27

Jede GitHub Action ist per vollständigem Commit-SHA gepinnt, die Version steht als Kommentar daneben. Dependabot hebt die Pins monatlich per PR, der alle Pflicht-Checks durchläuft.

T-018, T-012 (teilweise)

.github/workflows/*.yml, .github/dependabot.yml

M-28

Referrer-Policy strict-origin-when-cross-origin per Meta-Tag in beiden Layouts (basis.njk, karte.njk) und am YouTube-iframe: YouTube erfährt nur https://lernapps.github.io/, nie Pfad, seed oder von=tutor.

T-005 (teilweise)

src/_includes/basis.njk, src/_includes/karte.njk, src/kern/js/video.js; test/kern/video.test.js

M-16

Rückmeldung nur über einen Link auf GitHub Issues. Die Seite lädt nichts von GitHub; erst der Klick des Kindes oder der Eltern öffnet GitHub. GitHub verlangt ein Konto und ein Mindestalter von 13 Jahren; jüngere Kinder brauchen die Eltern. Entschieden am 23.09.2026, noch nicht umgesetzt.

T-007 (hält den Link frei von Einbettungen)

Kapitel 3

Offene Restrisiken: Admins können den Branch-Schutz umgehen, und eine Freigabe erzwingt er nicht (R-026); T-012 bleibt ein Lieferketten-Risiko (R-015), T-002 hängt am Review; die Allowlist (M-22) hält fremde Links fern, aber nicht fremden Text in einem gemergten PR (R-024). T-014 ist bewusst nur teilweise geschlossen: Was auf dem Foto steht, liegt beim Kind (R-022). T-016 bis T-018 stammen aus dem Security-Review vom 25.09.2026, M-25 bis M-28 beheben dessen Befunde S-001 bis S-004 (Anhang Bewertungen); eine Content Security Policy fehlt noch (R-033).

8.3 Test: Viele schnelle Unit-Tests, ein Vertragstest für alle Apps, der Build als Prüfstand, Browser-Tests obenauf

Reine, DOM-freie Module machen Unit-Tests möglich (ADR-013, ADR-016); die Regeln als Build-Fehler entscheidet ADR-008.

testpyramide
Ebene Was sie prüft Rückverfolgung

Unit-Tests (test/kern/, test/build/, src/<app>/test/, src/karte/test/)

Jedes Kern-Modul, jede Build-Funktion und jeder Generator als reine Funktion; test-first.

Mindestens 90 % der Testdateien nennen im Kopf den Use Case (// Use Case: …; test/build/use-case-verweise.test.js bricht darunter); zahlantwort.test.js nennt BR-1 bis BR-5, variablenterm.test.js/termantwort.test.js BR-T1 bis BR-T9.

Property-Based Tests (test/kern/*.property.test.js)

Eigenschaften der Kern-Mathematik über viele erzeugte Eingaben statt Einzelbeispiele: Round-Trip formatierter Zahlen, monotone Rundung, Stellenregel, „=“ nur bei exakten Werten, Gleichwertigkeit und Form von Binomen, abgelehnte Fehlterme. fast-check mit festem Seed 22 und 300 Läufen je Eigenschaft (zusammen unter 1 s); anderer Seed lokal mit FC_SEED=<Zahl> npm test. Ein gefundener Fehler wird Issue mit risk-radar und bleibt bis zum Fix als todo sichtbar.

Jeder Test nennt QZ-3 und R-004 oder R-012 im Namen, dazu BR-2, BR-5, BR-T7, BR-T8 (Issue #22)

Vertragstest

Für jede Kompetenz jeder App aus lib/apps.js: Generator liefert Aufgaben, die eigene Lösung ist richtig, Unsinn falsch, Seeds 1–20.

UC-1, UC-4 (test/apps/vertrag.test.js:1, :34)

Typprüfung (npm run typecheck)

tsc --checkJs mit strict über src/kern/js (jsconfig.json), im Pflicht-Check nach dem Lint. JSDoc-Typen beschreiben die Verträge zwischen Kern und Apps: Aufgabe, Feld, Generator, Prüfergebnis. Die Apps (src/<app>/js) prüft sie noch nicht.

ADR-023, Issue #25

Build-Prüfungen

Regeln, die kein Unit-Test sieht: externe Ressourcen in der Ausgabe, Dateilänge, vollständige Kompetenz (Seite, Generator, Test), llms.txt nennt jede Seite, Links in Tutor-Dateien nur auf die Allowlist (M-22), Karten-Knoten existieren, Fach bekannt. Dazu prüft npm test die Lizenzen im Lockfile und die Lizenzköpfe unter vendor/ (M-24, test/build/pruefe-lizenzen.test.js).

UC-8, UC-9; QZ-1, QZ-2, QZ-4

Browser-Tests (e2e/, Workflow browser.yml)

Playwright mit Chromium gegen das gebaute _site/ (e2e/server.js, Port 8813), 166 Tests in rund 35 s: jede Seite HTTP 200 und 0 Konsolenfehler; 0 fremde Requests vor der ersten Nutzeraktion auf jeder App-Seite; kein waagerechter Überlauf bei 360 px auf jeder Kompetenzseite, ohne JS und mit Übung samt Lösung; je App eine Übungsrunde per Deep Link nr=42 (neue Aufgabe, falsch, Lösung zeigen, richtig); axe-core ohne schwere oder kritische Verstöße gegen WCAG 2.2 AA (Übersicht, Karte, Start- und Testseiten, jede Kompetenzseite ohne und mit geladener Übung; R-030); „Zurück zu Claude“ (Knopf, Tab schließt, Ersatzmeldung); ohne JS Erklärung und Bild. Die Seitenliste kommt aus lib/apps.js, neue Apps laufen mit. Von Hand bleibt: 1280 px, der Schnelltest und der Video-Klick.

UC-1 bis UC-7; QS-12, QS-13, QS-27; ADR-021 (#26, TD-5 abgebaut, ADR-026)

Tutor-Dialog

Ob Claude mit tutor.md und llms.txt sinnvoll unterrichtet und korrekte Links baut.

UC-7; nicht automatisiert

Unit-, Vertrags- und Property-Based Tests laufen auf Node-Bordmitteln (node:test, node:assert) mit fast-check; die Browser-Tests nutzen @playwright/test und @axe-core/playwright (ADR-026). Alle sind exakt gepinnte devDependencies, ausgeliefert wird keine: Eleventy baut, die anderen prüfen. Lokal: npm run build && npm run test:browser (einmalig npx --package=@playwright/test playwright install chromium).

8.4 Beobachtbarkeit: Die Apps messen absichtlich nichts, der Build meldet alles

Es gibt keine Logs, Metriken, Traces oder Audit-Trails zur Laufzeit. Das folgt aus QZ-1 und ADR-001: Jede Telemetrie wäre ein Request, der etwas über das Kind verrät. An ihre Stelle treten:

  • Build-Ausgabe: [lern-apps] 3 App(s) geprüft, ?v=<Hash> oder die Liste aller Regelverstöße (eleventy.config.js); in CI im Actions-Log von pruefen.yml und pages.yml.

  • Audit-Trail: die Git-Historie für Code, tutor.md, llms.txt, Kartendaten und ADRs.

  • Beim Kind: Ergebniszeile und Aufgabennummer sind die einzige „Telemetrie“; das Kind entscheidet, ob es sie teilt. Auch „Zurück zu Claude“ (ADR-021) sendet nichts: Die Zeile landet nur in der Zwischenablage des Geräts und bleibt lokal, bis das Kind sie selbst bei Claude einfügt. Kein Request, kein postMessage, kein Speichern. Den Rechenweg sieht die App nie: Das Kind schickt ein Foto des Blatts selbst in den Chat (ADR-022); die Fotos liegen dann beim Chat-Anbieter.

  • Beim Tutor: Was nicht funktioniert, zeigt sich im Gespräch.

  • Rückmeldung an die Autor:in: Jede Seite bekommt einen Link auf GitHub Issues (entschieden am 23.09.2026). Nur wer klickt und ein GitHub-Konto hat, meldet etwas; die App selbst sendet nichts (8.2 M-16).

8.5 Fehlerbehandlung: Im Build abbrechen, im Browser zurückfallen

Retry und Circuit Breaker braucht niemand, denn die Apps rufen keinen Dienst auf (ADR-001). Fehler werden an zwei Orten behandelt.

  1. Build: lieber rot als falsch. Jede Regelverletzung, jede fehlerhafte App-Konfiguration, jedes unbekannte Fach und jeder unbekannte Karten-Knoten wirft; npm run build endet mit Fehler, pages.yml deployt nichts, die letzte Fassung bleibt online (ADR-008, ADR-017, ADR-018).

  2. Speicher darf fehlen. Jeder localStorage-Zugriff steht in try/catch; Laden liefert einen leeren Wert, Speichern false, die Seite sagt es dem Kind (ADR-007).

  3. Ungültige Eingaben aus der URL werden zu Zufall (leseSeed, leseVorgaben, leseModus).

  4. Unlesbares ist kein Versuch (wirdGezaehlt, src/kern/js/pruefung.js:73).

  5. Kopieren hat einen Rückfall: ohne Zwischenablage markiert die App den Text.

  6. Schließen wird nachgeprüft: „Zurück zu Claude“ prüft 300 ms nach window.close(), ob der Tab zu ist, und sagt sonst per aria-live, was zu tun ist; ohne Zwischenablage schließt es gar nicht (ADR-021, Szenario 7).

  7. Ohne JavaScript bleibt Inhalt: Text und Bild stehen im HTML; <noscript> sagt, was fehlt (ADR-016).

  8. Alte und neue Module mischen sich nicht: ?v=<Hash> an jeder Referenz (8.10, ADR-014).

Die Szenarien dazu stehen in Szenario 6.

8.6 Barrierefreiheit: Lesbar ohne JavaScript, bedienbar per Tastatur, gut ab 360 px

Grundlage sind ADR-001 (statische Seiten), ADR-016 (Bild ohne JS) und ADR-011 (Kontrast der Fachfarben).

  • lang="de", Skip-Link, nav mit aria-label, aria-current="page".

  • Mobile-first: Grundlayout für 360 px, ab 40rem breiter (src/kern/css/stil.css:2, :223); Systemschriften.

  • „Zurück zu Claude“ ist ein echter <button> mit mindestens 44 px Höhe; Hinweis und Rückfall-Feld (mit aria-label) sind per Tastatur erreichbar, der Hinweis steht in aria-live="polite" (ADR-021).

  • Jedes Feld hat ein <label>; Rückmeldung und Vorschau in aria-live="polite"; der Lösungsknopf trägt aria-expanded.

  • Bilder sind SVG mit role="img" und aria-label; das statische SVG steht schon im HTML. Enthält ein Bild anklickbare Elemente (role="button"), trägt das SVG role="group", denn ein img darf keine Bedienelemente enthalten.

8.7 Zahlen eingeben: Eine Rundungsregel für alle Felder

Entschieden in ADR-006, umgesetzt in src/kern/js/zahlantwort.js, geprüft in test/kern/zahlantwort.test.js. Der Domain Expert hat die Rundungsregel am 23.09.2026 fachlich bestätigt: Geld 2, Prozent 1, sonst 2 Nachkommastellen.

  • BR-1 Jedes Zahlenfeld hat eine Art: geld 2, prozent 1, zahl 2 Stellen (zahlantwort.js:17); stellen überschreibt.

  • BR-2 Die Toleranz folgt den getippten Stellen: richtig ist der exakte Wert oder der exakte Wert kaufmännisch auf die getippten Stellen gerundet, mindestens die geforderten.

  • BR-3 Zu grob, aber richtig gerundet ist ein Hinweis, kein Fehler.

  • BR-4 Brüche und Terme gelten überall als exakt; Felder mit nurZahl: true verlangen das ausgerechnete Ergebnis.

  • BR-5 Tippt das Kind einen Term, zeigt das Feld nach 150 ms den Wert („= 30 €“, „≈ 3,33“).

Irrationale Werte vergleicht der Prüfer mit dem relativen Spielraum 1e-9 (zahlantwort.js:22); Dezimalzahlen mit mehr als 12 Nachkommastellen gelten nicht als gerundet (:23). Generatoren setzen keine eigenen Toleranzen.

8.7.1 Terme mit Variablen: Gleichwertig heißt gleich an festen Prüfstellen

Entschieden in ADR-009, umgesetzt in src/kern/js/variablenterm.js und src/kern/js/termantwort.js, Business Rules BR-T1 bis BR-T9 (Use Case UC-T). Gleichwertig heißt: gleich an 8 festen Belegungen ohne 0 und ±1, mindestens 5 gültig, relative Toleranz 1e-9 (termantwort.js:26). Falsche Form (nicht-ausmultipliziert, nicht-zusammengefasst, nicht-faktorisiert) ist ein Hinweis und zählt nicht. Die Formregeln „ausmultipliziert“ und „faktorisiert“ hat der Domain Expert am 23.09.2026 fachlich bestätigt.

8.8 Konfiguration: Die App reicht sich dem Kern hinein

Entschieden in ADR-013. src/<app>/js/app.config.js ist reine Daten ohne DOM; Build, Seiten und Tests lesen sie, der Kern nie.

Eintrag Bedeutung

APP.id

Präfix aller localStorage-Schlüssel; nach dem Start nie ändern. Getrennt vom Pfad, damit ein Umzug keine Daten verliert (binom-trainer für /binom/).

APP.pfad

Ordner unter src/ und URL-Pfad; lib/apps.js:15 prüft die Gleichheit.

APP.titel, kurzname, beschreibung, intro, klasse

Texte für Übersicht, Kopf, Manifest, Karte.

APP.fach

Wählt die Fachfarbe (8.11); unbekanntes Fach bricht den Build.

APP.kartenEintrag

edugo-Felder für die Mathe-Karte (8.14); optional.

KOMPETENZEN

Reihenfolge = Checklisten-Nummer; je Eintrag id, titel, kurz, seite, generator, optional kartenKnoten.

Adresse, Quellcode-Link, Farbe und Version leitet der Build ab; sie stehen nicht in der Konfiguration (ADR-015).

8.9 Aufgabennummer: Dieselbe Nummer ergibt dieselbe Aufgabe

Entschieden in ADR-004. Jeder Generator bekommt ein Zufallsobjekt aus erzeugeZufall(seed) (mulberry32) und darf keinen anderen Zufall benutzen. Ohne Nummer wählt die App eine aus 1–9999. seed gewinnt vor nr. Im Text für Kinder heißt es „Aufgabe Nr. 42“. Das statische Bild einer Seite entsteht aus bild.seed im Front Matter (Standard 1, lib/bild.js:20).

8.10 Auslieferung und Cache: Jeder Inhalt hat eigene URLs

Entschieden in ADR-014 (ersetzt ADR-010). Nach dem Schreiben von _site bildet der Build einen Hash (8 Hex-Zeichen, SHA-256) über Pfad und Inhalt aller ausgelieferten JS-, CSS- und JSON-Dateien und hängt ?v=<Hash> an jeden relativen Import (statisch, export … from, dynamisch), jedes lokale <script src>, <link href> auf JS/CSS und jeden Import in Inline-Modulen (lib/versionierung.js:9-28). In den Quellen steht nie ein ?v=. Überall steht derselbe Wert; sonst lädt der Browser ein Modul unter zwei URLs doppelt. Die Doku unter /docs/ hat ihre eigene Versionierung durch docToolchain und wird vom Hash nicht erfasst.

8.11 Gestaltung: Die Farbe verrät das Fach

Entschieden in ADR-011. Die Tabelle steht nur in lib/fachfarben.js:6-12; das Layout setzt --farbe-primaer und --farbe-primaer-dunkel (src/_includes/basis.njk:21), theme-color und theme_color im Manifest (src/manifest.njk:15). Weiße Schrift erreicht auf allen Farben WCAG AA (test/build/fachfarben.test.js).

APP.fach Fach primaer / dunkel

mathe

Mathematik

#1d4ed8 / #1e3a8a

physik

Physik

#c2410c / #7c2d12

chemie

Chemie

#6d28d9 / #4c1d95

biologie

Biologie

#15803d / #14532d

informatik

Informatik

#0f766e / #134e4a

Die Übersicht ist fachübergreifend und nutzt #374151 / #1f2937 (src/_data/site.js). Das Menü setzt die Kompetenz-Nummer als Plakette vor den Menütext. Icons (favicon.svg, PNGs) tragen die Fachfarbe; das prüft niemand.

8.12 Bilder: Eine Zeichenfunktion, zwei Laufzeiten

Entschieden in ADR-016. Jede Kompetenz mit Bild hat src/<app>/js/vis/<id>.js mit einer Funktion zeichne…(svg, aufgabe, ergebnis). Beim Build ruft lib/bild.js den Generator mit bild.seed und bild.vorgaben auf und zeichnet in das Mini-DOM aus src/kern/js/svg.js; das SVG steht dann statisch im HTML. Im Browser zeichnet dieselbe Funktion es zu jeder neuen Aufgabe neu. Regel: In js/vis/ nur svgEl, nie document. Handgezeichnete SVGs gibt es nicht.

Wahrscheinlichkeitsbäume stehen hochkant (Wurzel oben, Blätter als 24-px-Kreise mit Kürzel und Legende, Pfadwahrscheinlichkeiten darunter), wenn sie in 279 px passen, sonst liegen sie quer; über quer liegenden Bäumen zeigt zufall.css auf schmalen Hochkant-Bildschirmen einen Dreh-Hinweis. Entschieden in ADR-024.

8.13 Kompetenzseiten: Front Matter statt HTML, festes Gerüst, Regeln im Build

Entschieden in ADR-017. src/<app>/<id>.md enthält nur Front Matter: kompetenz, beschreibung, warum, regel, beispiel (Markdown oder HTML), video: { id, titel, kanal }, sonst videoVerweis (ein Satz mit Link auf das Video einer verwandten Seite) oder gar kein Abschnitt #video, bild: { text, funktion, seed, geloest, vorgaben }, optional serlo: { url, titel } (ein geprüfter serlo.org-Artikel als zweite Erklärung nach dem Video, 8.2 M-17). src/_includes/kompetenz.njk setzt daraus immer dieselben Abschnitte mit denselben Ankern, in dieser Reihenfolge: Warum, Regel, Beispiel, Bild, Video, Übung. Der Build verlangt je Kompetenz Seite, Generator und Test (lib/pruefungen.js:36-49) und dass llms.txt jede Seite nennt (:31-33).

8.14 Mathe-Karte: Flache Markdown-Daten, App-Einträge aus der App

Entschieden in ADR-018. Die Daten liegen als Markdown mit flachem YAML-Front-Matter unter src/karte/daten/ (Leitideen, Kompetenzen = Knoten, Lehrpläne von 16 Ländern, Zuordnungen); Schemas in src/karte/schemas/, Beschreibung in src/karte/daten/README.md. Eine App erscheint auf der Karte, wenn ihre Konfiguration APP.kartenEintrag (edugo-Felder aktiv-level, backend, external-requests, dsgvo, evidence, jahrgaenge, lizenz, stand) trägt und Kompetenzen kartenKnoten nennen (lib/karte/eintraege.js:15-34). Unbekannte Knoten brechen den Build. Apps außerhalb des Repos stehen in src/karte/daten/externe-eintraege.js (heute leer). data.json ist deterministisch und trägt ?v=.

8.15 Adressen: Eine Basis-URL, alles andere abgeleitet

Entschieden in ADR-015. src/_data/site.js:9 enthält https://lernapps.github.io/. leiteAdressenAb leitet daraus pathPrefix, Repository (https://github.com/lernapps/lernapps.github.io), App-Adressen und Quellcode-Links ab (lib/adressen.js:7-24). Vorlagen schreiben nie eine literale Adresse, sondern {{ app.basisUrl }} oder {{ site.basis }}. site.js behält genau einen default-Export.

8.16 Harness für KI-gestützte Entwicklung: 21 von 29 Prüfschichten laufen; die größten Lücken sind Formatter, Dead-Code-Erkennung und Rechtschreibprüfung

Claude-Code-Agenten schreiben fast den ganzen Code; der Product Owner prüft und merged. Der Harness ist alles, was die Agenten dabei führt und was ihre Fehler fängt. Er gehört in dieses Kapitel, weil er jeden Baustein gleich trifft: Er ist ein Querschnittskonzept wie Test (8.3) und Sicherheit (8.2), keine einzelne Qualitätsanforderung für Kapitel 10.

Die Führung besteht aus drei Teilen. CLAUDE.md legt Projektregeln und Semantic Contracts fest, der Skill werkzeuge/skill/lern-app/ mit seinen references/ beschreibt den Weg zu einer neuen App, und die ADRs in Kapitel 9 halten Entscheidungen fest. Die Fehlerkorrektur misst das Rad unten. Es folgt dem Modell der Harness Inventory und des Harness Coverage Wheel (Semantic Anchors, Apache-2.0): 69 Prüfschichten in neun Abschnitten, jede als extrinsisch (Regel kommt von außen, einschalten genügt), hybrid oder intrinsisch (das Projekt definiert richtig und falsch) eingestuft und mit dem kleinsten Risk-Radar-Tier versehen, ab dem sie zählt. Die Einstufung Tier 2 entscheidet ADR-023 (EPIC #18).

Das Bild ist eine eigene Zeichnung aus scripts/harness-rad.js. Das Original-Rad rendert per JavaScript, kennt keinen SVG-Export und neben „abgedeckt“ nur „nicht zutreffend“. Unser Rad zeigt zusätzlich „in Arbeit“ (offener PR), „geplant“ (offenes Issue) und „verworfen“; „entfällt“ heißt, die Schicht trifft auf statische Seiten ohne Server und Daten nicht zu. Unter dem Bild öffnet ein Link denselben Stand im Original; auf Wunsch lädt es sich per Zwei-Klick direkt hier (ADR-025). Die Tests test/build/harness-rad.test.js und harness-rad-original.test.js brechen, wenn Bild, Tabelle oder Link vom Generator abweichen.

Ob „vorhanden“ stimmt, prüfen zwei Wege. Jede vorhandene Schicht nennt in scripts/harness-belege.js maschinenprüfbare Belege (Datei, npm-Skript, Workflow-Job, ESLint-Regel, Export); harness-rad.test.js bricht, sobald einer davon fehlt, etwa wenn jemand e2e/axe.spec.js löscht. Repo-Einstellungen wie Secret Scanning, CodeQL und die Branch Protection sieht kein Unit-Test; sie bestätigt ein Audit über die GitHub-API, das wir jedes Quartal wiederholen. Jedes Audit steht datiert im Anhang Bewertungen.

Harness Coverage Wheel für lernapps: Abdeckung je Abschnitt bis Tier 2

Das Original kennt nur „abgedeckt“ und „nicht zutreffend“. Der Link setzt „vorhanden“ als abgedeckt und „entfällt“ als nicht zutreffend; „in Arbeit“, „geplant“ und „verworfen“ bleiben dort Lücken. Weil auch unser Rad nur Vorhandenes zählt und Entfallenes weglässt, ergibt das denselben Wert. Das Original zählt 21 von 29 (72 %), unser Rad 21 von 29 (72 %). Was das Original nicht zeigt: welche Lücken schon in Arbeit oder geplant sind.

Abschnitt Stufe (Tier 2) Nachweis Lücke

Build & Language

3 von 5 (60 %)

Compiler / Parser: npm test und npm run build laden jedes Modul; Syntaxfehler brechen test-und-build; Type checker: tsc --checkJs mit strict im Pflicht-Check, vorerst nur src/kern (jsconfig.json), #25; Linter: ESLint (eslint.config.js: no-eval, no-implied-eval, no-unsanitized) im Pflicht-Check, #21

Formatter (offen); Import sorter / dead code (offen)

Testing

3 von 3 (100 %)

Unit tests: node:test-Tests in test/ und src/<app>/test/, im Pflicht-Check test-und-build; Property-based / fuzz: fast-check, #22: Zahlen und Terme (test/kern/*.property.test.js, Fund #30 behoben); jede Rechnung im Lösungsweg jeder App wird nachgerechnet (test/apps/rechenweg.property.test.js, L-021); Integration tests: Vertragstest test/apps/vertrag.test.js: jede Kompetenz jeder App, Seeds 1–20; Contract tests: Tutor-Vertrag lib/llms-vertrag.js: Deep Links in llms.txt/tutor.md gegen Generator-Parameter; End-to-end / UI: Playwright (Chromium) in CI, Workflow browser.yml, #26: Übungsrunde je App, „Zurück zu Claude“, 360 px, ohne JS (e2e/); Smoke tests: e2e/smoke.spec.js: jede Seite HTTP 200, 0 Konsolenfehler; e2e/extern.spec.js: 0 fremde Requests, #26

–

Security

5 von 5 (100 %)

Secret scanning: GitHub Secret Scanning mit Push Protection; SCA: Dependabot-Alerts und -Security-Updates; npm audit --audit-level=high im Pflicht-Check, #20; Dependency Review prüft neue Abhängigkeiten jedes PRs auf Schwachstellen ab „high“ (Pflicht-Check abhaengigkeiten, ADR-029); License compliance: lib/pruefe-lizenzen.js in npm test (Allowlist je Verwendung, Lizenzköpfe unter vendor/), Dependency Review in PRs (Pflicht-Check abhaengigkeiten), ADR-029; SAST: CodeQL Default Setup (JavaScript/TypeScript, Actions); LLM security review: OWASP-Teil des KI-Reviews (werkzeuge/review/ki-review.md), Check ki-review, #24, ADR-027; Threat modeling: STRIDE-Tabelle in arc42 8.1 (T-IDs); das KI-Review prüft jedes neue Feature dagegen (werkzeuge/review/ki-review.md, Punkt 3), Check ki-review

–

Architecture

3 von 4 (75 %)

Code review: Der Product Owner merged jeden PR selbst; ein Pflicht-Approval kann GitHub bei nur einem Maintainer nicht verlangen – erzwungen wird das Review über den Pflicht-Check ki-review (ADR-027); LLM code review: KI-Review in frischem Kontext vor jedem Merge (werkzeuge/review/ki-review.md), Check ki-review, #24, ADR-027; ADR enforcement: lib/pruefungen.js bricht den Build bei Regelverstößen (ADR-008), auch bei Links außerhalb der Tutor-Allowlist (pruefeTutorLinks, ADR-023); ATAM: Baseline vom 25.09.2026 mit Utility Tree (arc42 10, 11), Wiederholung je Quartal mit dem Harness-Audit, ADR-030; LLM design review: Abschnitt „Architektur (ATAM)“ im KI-Review, sobald ein PR ADRs oder arc42-Kapitel 1, 4, 5, 9 oder 10 ändert; der Check ki-review erzwingt ihn (ADR-030)

Complexity metrics (verworfen: SonarQube abgelehnt (#28); nur Dateilänge ≤ 500 Zeilen)

Data & Schema

2 von 2 (100 %)

Schema validation: Front Matter der Kompetenzen (pruefeKompetenzen) und Kartendaten (pruefeReferenzen in lib/karte/daten.js: Pflichtfelder, Enum-Werte); die JSON-Schemas in src/karte/schemas/ prüft kein Build; Config validation: App-Konfiguration: unbekanntes Fach bricht den Build (lib/fachfarben.js); Data contract: Karten-Knoten müssen existieren: pruefeReferenzen bricht den Build (lib/karte/laden.js), Tests in test/build/karte-daten.test.js

–

UX & A11y

2 von 3 (67 %)

Accessibility automated: axe-core, WCAG 2.2 AA, 0 schwere/kritische Verstöße auf jeder Seite, jede Kompetenzseite auch mit Übung (e2e/axe.spec.js), #26, R-030; Contrast checker: WCAG-Kontrast je Fachfarbe (test/build/fachfarben.test.js)

UI prose lint (offen)

Operations

0 von 1 (0 %)

–

Runtime assertions (offen)

Formal Methods

entfällt

–

–

Documentation

3 von 6 (50 %)

Markdown / AsciiDoc lint: asciidoc-linter (docToolchain) prüft src/docs in doku.yml, per Commit-SHA gepinnt (ADR-028); Überschriften-, Block-, Bild- und Markdown-Regeln feuern seit docToolchain/asciidoc-linter#62. ERRORs brechen den Doku-Job (scripts/doku-lint.js), WARNINGs sind Annotationen; kein Pflicht-Check, Markdown-Dateien prüft er nicht; Link checker: Jedes interne href/src in _site führt auf eine Datei, jeder #anker auf eine id (pruefeLinks in lib/pruefe-links.js, bricht den Build); Tutor-Deep-Links gegen Seiten, Anker und Parameter (pruefeLink), serlo-Links nur auf die Domain (pruefeSerloLinks), Verweise auf die Doku (test/build/doku-verweise.test.js); externe Links prüft der Build nicht; Diagram build: doku.yml baut PlantUML und prüft die Ausgabe in jedem PR, der Doku oder Doku-Build ändert (kein Pflicht-Check); pages.yml baut sie vor jedem Deployment; Doc-code drift: Tutor-Vertrag: jeder Generator-Parameter steht in llms.txt; Kontext (arc42 3) und Bausteinsicht (arc42 5) nennen dieselben Personen und Fremdsysteme (test/build/kontext-bausteine.test.js)

Code-in-docs validation (offen); Spell check (offen); Prose lint (offen)

Gesamt bis Tier 2: 21 von 29 zutreffenden Schichten (72 %); entfallene Schichten zählen nicht mit.

Linter, npm audit, Property-based Tests und die Typprüfung für src/kern laufen seit #20, #21, #22 und #25 im Pflicht-Check, CodeQL und Dependabot daneben. Die Properties fanden gleich einen Fehler (#30, behoben); seit dem Lektorat rechnet eine weitere jede Rechnung im Lösungsweg nach (L-021). Seit dem 24.09.2026 prüft der Build jeden internen Link und Anker in _site (lib/pruefe-links.js). Seit dem 25.09.2026 prüft npm test die Lizenzen aller Pakete und der Dateien unter vendor/, Dependency Review jeden PR (M-24, ADR-029). asciidoc-linter prüft die Doku (ADR-028); seit dem Fix upstream (docToolchain/asciidoc-linter#62) feuern auch seine Überschriften-, Block- und Bildregeln, und die Schicht zählt wieder als „vorhanden“. Code-Review und Bedrohungsmodell zählen, weil der Pflicht-Check ki-review sie erzwingt; die beiden Prose-Lint-Schichten stehen auf „offen“, denn es gibt nur eine Regex und eine Zeilengrenze und keinen PR dafür. Seit dem 25.09.2026 zählt auch das LLM-Design-Review: Ändert ein PR ADRs oder die Kapitel 1, 4, 5, 9 oder 10, verlangt der Check ki-review einen Abschnitt nach ATAM gegen den Utility Tree; die ATAM-Baseline wiederholen wir mit dem Audit jedes Quartal (ADR-030). Bis Tier 2 fehlen acht Schichten: Formatter, Dead-Code-Erkennung und Rechtschreibprüfung (Tier 1), dazu Komplexitätsmetriken (verworfen, #28), beide Prose-Lint-Schichten, Runtime Assertions und die Prüfung von Code in der Doku. Die größte Lücke liegt also bei billigen extrinsischen Schichten; außerhalb des Rads fehlt noch die Typprüfung der Apps. Stark sind die intrinsischen Schichten, also Vertragstest, Tutor-Vertrag und Build-Prüfungen.

Wer den Stand ändert, passt ROH in scripts/harness-rad.js an und ruft node scripts/harness-rad.js auf; das erneuert Bild, Tabelle und den Link ins Original.

8.17 Übersichtsseite: Kennzahlen entstehen beim Doku-Build, nicht im Repository

Die Seite Übersicht (/docs/uebersicht/) zeigt die wichtigsten Bilder und Kennzahlen auf einer Seite: Kontext und Bausteine als C4-Diagramm, das Harness-Rad, den Utility Tree, eine Risikomatrix und die Risikothemen der ATAM-Bewertung. Jede Kachel führt zu ihrem Kapitel. Die beiden C4-Diagramme bindet die Seite per include::…[tag=…] aus Kapitel 3 und 5 ein; es gibt sie nur einmal. Die Diagramm-Kacheln verlinken ihr SVG in voller Größe, damit man es auf dem Handy zoomen kann.

Kennzahlen, Risikomatrix, Utility Tree und Risikothemen schreibt scripts/dashboard.js in vier git-ignorierte Dateien src/docs/uebersicht/_*.adoc (ADR-031). Es braucht kein Netz: Es liest package.json, die App-Konfigurationen, CLAUDE.md, die arc42-Kapitel und je Art den jüngsten Bericht im Anhang Bewertungen und startet zwei lokale Prozesse. Die Tests zählt es nicht im Quelltext, weil Schleifen viele davon erzeugen (die Browser-Tests je Seite aus e2e/seiten.js): Die Unit-Tests laufen einmal mit node --test (rund 2 Sekunden), die Browser-Tests zählt playwright test --list, ohne einen Browser zu starten. Beides braucht npm ci und für die Browser-Specs ein gebautes _site/. scripts/dtc-v4.sh ruft es vor jedem Doku-Build auf, lokal wie in doku.yml und pages.yml; doku.yml prüft die AsciiDoc-Quellen deshalb erst nach dem Build, wenn es die Includes gibt. test/build/dashboard.test.js rechnet die Zahlen gegen ihre Quellen nach und bricht, wenn sich das Format der Risikotabelle, des ADR-Index oder des Utility Tree so ändert, dass der Generator es nicht mehr liest. Die Pflicht-Checks stammen aus der Doku, nicht aus der Branch Protection, die nur ein Admin lesen kann.