Dokumentation Entwickler
Für Entwickler
1. Tech-Stack#
| Was | Womit | Anmerkung |
|---|---|---|
| Framework | Next.js 16 (App Router) | Achtung: Diese Version hat Breaking Changes gegenüber allem, was ein Modell aus dem Training kennt. Vor dem Schreiben von Code die Doku unter node_modules/next/dist/docs/ lesen - so steht es auch in AGENTS.md. |
| Sprache | TypeScript, React 19 | |
| Datenbank | Postgres 18.6 auf Neon (eu-central-1) | Zugriff über postgres (postgres.js) |
| ORM | Drizzle ORM 0.45 + drizzle-kit 0.31 | Schema in db/schema.ts, Migrationen in db/migrations/ |
| Anmeldung | @neondatabase/auth (Beta) | Gekapselt hinter lib/auth.ts - bewegt sich das Paket, hängt der Wechsel an einer Datei |
| Styling | Tailwind CSS 4 | |
| Validierung | Zod 4 | Vor allem für die MCP-Werkzeugschemata |
| Dateien | @vercel/blob (privater Store) | Store planr-material, Region fra1 |
| MCP | @modelcontextprotocol/sdk 1.30 | stdio und Streamable HTTP aus einer Registrierung |
| Tests | tsx gegen .test.ts-Dateien | Kein Test-Framework, keine Datenbank, kein Mocking |
| Hosting | Vercel (Funktionen in fra1) |
Rund 45.700 Zeilen TypeScript/TSX in app, lib, components, mcp, db.
2. Lokales Setup#
npm installDann .env.local anlegen. Das absolute Minimum ist eine Datenbank:
DATABASE_URL=postgresql://user:pass@host/planrOhne NEON_AUTH_BASE_URL und NEON_AUTH_COOKIE_SECRET antwortet proxy.ts in
Produktion mit 503 - eine vergessene Variable darf nicht bedeuten, dass alles
offensteht. Lokal (NODE_ENV=development) läuft es ohne.
Migrationen einspielen:
npm run db:migrateSchulkalender anlegen (Schuljahr, Ferien, Feiertage für MV):
npm run db:seed-kalenderStarten:
npm run devUmgebungsvariablen#
Betrieb
| Variable | Zweck |
|---|---|
DATABASE_URL | Postgres. Wird auf Vercel von der Neon-Marketplace-Ressource verwaltet - von Hand überschreiben wird beim nächsten Anfassen der Integration zurückgesetzt. |
NEON_AUTH_BASE_URL, NEON_AUTH_COOKIE_SECRET | Anmeldung. Fehlen sie in Produktion: 503. |
PLANR_REGISTRIERCODE | Zugangswort für /registrieren. Fehlt es in Produktion, ist die Registrierung zu, nicht offen. Lokal bleibt sie offen. |
CRON_SECRET | Bearer für den täglichen Cron. Ohne Secret ist der Endpunkt lokal offen. |
BLOB_READ_WRITE_TOKEN | Datei-Store. |
TELEGRAM_TOKEN, TELEGRAM_CHAT_ID | Serverfehler als Nachricht, höchstens alle zehn Minuten je Route. |
PLANR_API_TOKEN | Übergangsweise gültiger globaler Schlüssel, gerechnet als Schlüssel des Kopplungsnutzers. Soll ersatzlos verschwinden. |
PLANR_KOPPLUNGEN_NUTZER | Wem die Zugänge zu selbr, drillr, checkr und dem Experimente-Katalog gehören. |
Kopplungen (alle optional, jede fällt einzeln still aus)
| Variable | Wofür |
|---|---|
SELBR_SUPABASE_URL, SELBR_SUPABASE_SERVICE_ROLE_KEY | Lernstand, Fokus, Hausaufgaben |
DRILLR_API_KEY | Übungsblätter erzeugen |
CHECKR_URL, CHECKR_REPORT_TOKEN | Korrekturaufträge und Rückkanal. Ohne CHECKR_URL gilt https://check-r.org. checkr.org ohne Bindestrich gehört jemand anderem und darf nie angesprochen werden, obwohl beide Domains mit 200 antworten. |
PEK_SUPABASE_URL, PEK_SUPABASE_SERVICE_ROLE_KEY | Experimente-Katalog |
PRUEFR_URL | Arbeiten bauen |
PLANR_MINI_URL, PLANR_MINI_TOKEN | Modellbrücke für den Chat auf der Kursseite |
MAIL_SMTP_URL, MAIL_ABSENDER, NEON_API_KEY | Eigener Mailversand mit deutschen Texten, siehe docs/mail.md |
Fallstricke#
- Skripte laden
.env.localnicht von selbst. Sie brauchen--env-file-if-exists=.env.local, sonst landen sie auf einer lokalen Standard-Instanz statt auf der Datenbank, die die App benutzt. drizzle-kit pushgibt es hier nicht. Es wurde aus den Skripten entfernt: Es ändert die Datenbank sofort, ohne Datei und ohne Spur - und die Datenbank aus.env.localist die Produktion.- Der Build läuft ohne
DATABASE_URL. Alle Seiten sindforce-dynamic, zur Bauzeit wird nichts abgefragt. Deshalb baut auch der Auth-Handler erst beim ersten Aufruf und nicht beim Laden des Moduls.
3. Projektstruktur#
app/
(arbeit)/ Alles hinter der Anmeldung. Die Klammer erzeugt kein
Pfadsegment - sie ist die Grenze: /geteilt und /anmelden
liegen ausserhalb und bekommen die Kursliste damit auch
nicht versteckt im HTML mitgeliefert.
klasse/[id]/ Übersicht, einheiten, stunden, stundenplan, stunde/[seq],
ka/[seq], stunde/[seq]/canva (HTML-Export)
vorlage/[id]/ Vorlagen, vorlage/neu
konto/ Einstellungen, Schlüssel, Export, Löschen
freigabe/ vergleich/ zettel/ hilfe/ schuljahr/
(vollbild)/ Präsentationsansicht für den Beamer
(rechtliches)/ /regeln, /datenschutz, /impressum - ohne Anmeldung
api/ 21 Routen, siehe unten
willkommen/ anmelden/ registrieren/ geteilt/[token]/
actions.ts 41 Server-Actions
lib/ Die Fachlichkeit. Reine Rechnung getrennt von Datenzugriff.
engine/ Der Planungskern: buildPlan, weekPlan, schoolWeeks,
kalender, datum, termine, timelineLayout - alles pur
besitz.ts Die eine Stelle, die "gehört das mir?" beantwortet
betreiber.ts Welche Felder ein fremdes Konto gar nicht erst bekommt
kopplungen.ts Darf der gerade Arbeitende die fremden Zugänge benutzen?
apiZugang.ts Bearer-Schlüssel prüfen, Arbeit im Namen des Inhabers
apiKontext.ts AsyncLocalStorage statt eines Parameters durch 30 Funktionen
*Api.ts Die Prüfungen hinter den HTTP-Routen (rein, testbar)
*.test.ts 51 Testdateien
components/ React-Komponenten, überwiegend Server Components
mcp/ registrieren.ts (Server), werkzeuge.ts (27 Werkzeuge),
routen.ts (Pfad -> Handler), inprozess.ts, server.ts (stdio)
db/ schema.ts (31 Tabellen), migrations/ (0000-0021)
scripts/ Seeds, Importe, Prüfskripte, Nachtlauf
docs/ Diese Dokumentation
paket/ npm-Paket `planr-mcp` (stdio-Hülle)
proxy.ts Optimistische Cookie-Prüfung, nicht die Autorisierung
instrumentation.ts onRequestError -> Protokoll + TelegramZwei Prinzipien, die überall wiederkehren#
Die Rechnung ist rein, der Datenzugriff steht daneben. lib/sequenz.ts,
lib/abschnitte.ts, lib/zeitschiene.ts, lib/vplan.ts, lib/versuchsnote.ts
und die ganze lib/engine/ kennen keine Datenbank. Deshalb sind sie ohne
Aufwand testbar, und deshalb benutzen Oberfläche und Skript dieselbe Rechnung.
Die Absicherung sitzt in der Datenschicht, nicht in der Oberfläche. Ein Knopf,
den man nicht sieht, ist keine Zugangsprüfung. lib/mehrbenutzer.test.ts liest
den Quelltext von app/actions.ts und verlangt, dass jede exportierte Aktion
entweder den Besitz auflöst oder mit Begründung auf einer Ausnahmeliste steht.
4. Datenmodell#
31 Tabellen in public, dazu eine Spalte aus dem Schema neon_auth (dem
Anmeldedienst gehörend, von drizzle-kit über schemaFilter: ["public"]
ausgeschlossen).
Nur drei Tabellen tragen einen Besitzer: classes, plan_templates und
share_links. Alles andere hängt per Fremdschlüssel mit on delete cascade
darunter und erbt ihn. Dazu kommen sperrzeiten, schul_einstellungen,
vplan_uebernahmen und api_keys mit eigenem owner_id, weil sie an keiner
dieser Wurzeln hängen.
erDiagram
neon_auth_user ||--o{ classes : besitzt
neon_auth_user ||--o{ plan_templates : besitzt
neon_auth_user ||--o{ share_links : besitzt
neon_auth_user ||--o{ api_keys : besitzt
rahmenplaene ||--o{ rahmenplan_themen : "Themen mit Richtwert"
rahmenplaene ||--o{ plan_templates : "wird übersetzt von"
plan_templates ||--o{ template_items : "Stundenliste"
plan_templates ||--o{ template_blocks : "Themenblöcke (Altweg)"
plan_templates ||--o{ template_milestones : "Leistungen"
plan_templates ||--o{ template_shares : "weitergegeben an"
template_milestones ||--o{ template_requirements : "Mindestanforderungen"
rahmenplan_themen ||--o{ template_items : "deckt ab"
classes ||--o{ class_slots : "Stundenplan"
classes ||--o{ termine : "Kalender-State"
classes ||--o{ curriculum_items : "Stundenfolge"
classes ||--o{ class_blocks : "Snapshot der Vorlage"
classes ||--o{ class_milestones : "Snapshot der Vorlage"
classes ||--o{ wiederholungs_signale : "was gewackelt hat"
classes ||--o{ vplan_uebernahmen : "Protokoll"
class_milestones ||--o{ class_requirements : "Snapshot der Vorlage"
curriculum_items ||--o| lesson_plans : "Detailplanung (1:1)"
lesson_plans ||--o{ lesson_phases : "Verlauf"
lesson_plans ||--o{ lesson_materials : "Material"
lesson_plans ||--o{ lesson_slides : "Folien"
schuljahre ||--o{ schulferien : "Land"
schuljahre ||--o{ sperrzeiten : "Schule"
schuljahre ||--o{ schul_einstellungen : "A/B-Modus, vplan_url"
feedback ||--o{ feedback_notizen : "Antwortspur"
neon_auth_user ||--o{ planungs_freunde : "einlader / freund"Was man an diesem Modell verstanden haben muss#
curriculum_items ist die Stundenfolge, termine der Kalender. Die Planung
(lesson_plans) hängt am Stundeneintrag, nicht am Termin - fällt eine Stunde aus,
wandert die Planung mit dem Stoff und klebt nicht am Datum.
class_blocks, class_milestones und class_requirements sind Kopien, keine
Joins auf die Vorlage. Die Anforderungen waren bis zum 21.08.2026 ein Live-Join
über classes.template_id (eine Spalte ohne Fremdschlüssel) - wer seine Vorlage
löschte, verlor die Anforderungen aller daraus entstandenen Klassen.
template_items ist der neue Weg, template_blocks der alte. Bis zum
21.08.2026 speicherte eine Vorlage nur "Terme, 9 Doppelstunden" und planr rechnete
"Terme (1/9)" bis "(9/9)". Jetzt trägt jede Zeile ihren eigenen Titel. Während der
Umstellung gibt es beide nebeneinander: erzeugeSequenz liest die Stundenliste,
wenn es sie gibt, und rechnet sonst aus den Blöcken. Die Blöcke tragen weiterhin
die Wochenverteilung und die Meilenstein-Fenster.
Arrays statt Zwischentabellen bei share_links.classIds und
planungs_freunde.classIds, weil daran nichts hängt: Die Liste wird immer
vollständig gelesen und einmal geschrieben. Achtung: Leer bedeutet in beiden
Fällen "alle Kurse des Besitzers" - beim Löschen eines Kurses darf man deshalb
nicht einfach eine Id herausstreichen, sonst wird der Link weiter statt enger.
lesson_slides.tafel und lesson_slides.inhalt sind tot. Das Tafelbild wurde
am 16.08.2026 abgebaut, inhalt von Migration 0003 nach bloecke überführt. Die
Spalten bleiben stehen: Eine Migration für ein totes Feld ist Risiko ohne
Gegenwert, und ein Rückbau des Deployments fände sonst leere Folien vor.
Migrationen#
npm run db:generate # vergleicht db/schema.ts mit dem letzten Stand, schreibt eine Datei
npm run db:migrate # spielt ein, was fehlt, und vermerkt es in drizzle.__drizzle_migrations0000_baseline.sql beschreibt den Stand vom 11.08.2026 und ist durchgehend
idempotent (CREATE TABLE IF NOT EXISTS, Fremdschlüssel in
DO $$ ... EXCEPTION WHEN duplicate_object): Sie fasst eine bestehende Datenbank
nicht an und legt auf einer leeren alles an. Zweimal ausführen ändert nichts.
5. API-Referenz#
21 Routen. Alle Datenrouten sind über einen persönlichen Bearer-Schlüssel
geschützt (lib/apiZugang.ts -> mitSchluessel), anzulegen unter
Einstellungen -> Schnittstellen-Schlüssel. Gespeichert wird nur der SHA-256-Hash;
planr muss einen Schlüssel prüfen können, nie zurücklesen.
Ohne gültigen Schlüssel: 401 {"fehler":"nicht autorisiert"}. Die Arbeit läuft
danach im Namen des Inhabers - alle Abfragen darunter filtern auf ihn, ohne ihn
als Parameter zu kennen (AsyncLocalStorage in lib/apiKontext.ts).
Lerngruppen heißen wie im Stundenplan (9d). Gibt es dieselbe in zwei Fächern,
gehört fach dazu.
Planung#
| Route | Parameter | Antwort |
|---|---|---|
GET /api/stunde?datum=YYYY-MM-DD | Was für den Tag geplant war, was an Rückmeldung fehlt | |
GET /api/stunde?klasse=8d&seq=1 | statt seq geht datum | Eine Stunde vollständig: Verlauf, Material, Hausaufgabe |
POST /api/stunde | {klasse, datum|seq, stundenziel, phasen[], material[], ...} | Planung eintragen und das Soll festschreiben |
POST /api/rueckmeldung | {klasse, datum, gehalten|ausgefallen, zielErreicht?, notiz?, wackelt?[]} | Ist-Rückmeldung. wackelt legt Wiederholungssignale an, geprüft gegen die Themen der Übung |
GET /api/grobplan?klasse=9d | fach? | Einheiten mit Nummern |
POST /api/grobplan | {klasse, werkzeug: "einheit_laenge"|"leistung_verschieben", ...} | Geändert wird über Nummern, nie über Namen |
GET /api/klassenprofil?klasse=9d | fach? | Klassenprofil - nur für den Betreiber |
POST /api/klassenprofil | {klasse, profil, anhaengen?} | dito |
Zweistufig schreiben (Entwurf + Prüfsumme)#
| Route | Modus | Antwort |
|---|---|---|
POST /api/stundenplan | {modus:"entwurf", stundenplan} | {pruefsumme, klartext, warnungen} |
POST /api/stundenplan | {modus:"uebernehmen", stundenplan, pruefsumme} | {classId, termine, klartext} |
GET /api/vorlage | ohne Parameter | Die eigenen Vorlagen |
GET /api/vorlage?fach=&jahrgang= | land? | Der Rahmenplan mit seinen Themen |
POST /api/vorlage | {modus:"anlegen", neu} | {templateId, name, klartext} |
POST /api/vorlage | {modus:"entwurf", vorlage} | {pruefsumme, klartext, zeilen} |
POST /api/vorlage | {modus:"uebernehmen", vorlage, pruefsumme} | {templateId, zeilen, klartext} |
Die Prüfsumme (lib/pruefsumme.ts) ist die Klammer: Geschrieben wird genau das,
was im Entwurf stand. Dazwischen gehört ein Mensch, der den Klartext liest.
Tagesarbeit#
| Route | Parameter |
|---|---|
GET /api/alltag?was=nachtraege | heute? |
GET /api/alltag?was=woche&von=&bis= | |
GET /api/alltag?was=material&klasse=&datum= | fach? |
POST /api/alltag | {was:"material_anlegen", klasse, datum, titel, typ?, link?} |
POST /api/alltag | {was:"material_abhaken", klasse, datum, titel, kopiert?, aufIserv?} |
POST /api/alltag | {was:"tu", klasse, datum} - erzeugt eine echte Übung in drillr |
POST /api/alltag | {was:"hausaufgabe", klasse, datum, aufgaben, hinweis?} |
Material wird über den Titel angesprochen, nicht über eine Id: Wer sagt "das
Arbeitsblatt ist kopiert", hat keine Nummer im Kopf. Ein Teilstück genügt, ein
exakter Treffer gewinnt gegen jedes Teilstück, Mehrdeutigkeit ist ein Fehler mit
Liste statt einer Vermutung (waehleMaterial in lib/material.ts).
Kopplungen nach draußen#
| Route | Für | Was hinausgeht |
|---|---|---|
GET /api/tu-vorschlag | drillr | Nächste Stunde, Übungsthemen, Auffrischung, behandelt (was die Klasse schon hatte), optional die Vorstunde |
GET /api/anforderungen | prüfr | Meilensteine mit Anforderungen. Nur lesend, keine Schülerdaten |
GET /api/stoffverteilung | merkr | Je Kurs die Stundenfolge mit Datum, Thema, Einheit, Stundenziel und Verlauf, dazu Ferien, Halbjahresgrenze und der gerechnete Tagesplan aus dem Vertretungsplan. Format kursbuch-plan-2 |
GET /api/hausaufgaben | merkr | Je Kurs die laufenden Aufträge, je Zugangscode der Stand. Nur der Code geht hinaus, nie ein Name |
GET /api/selbr-focus | Cron | Täglich 5 Uhr: Fokus, Hausaufgaben und Freischaltung nach selbr. Bearer CRON_SECRET, läuft ausdrücklich alsNutzer(inhaber, ...) |
Verwaltung#
| Route | Zweck |
|---|---|
GET/POST /api/zettel | Fehler und Wünsche lesen (alle=1, art=fehler|wunsch), antworten und den Stand setzen |
GET /api/version | Welcher Commit läuft (VERCEL_GIT_COMMIT_SHA). Für den Nachtlauf gebaut |
POST /api/mcp | MCP über Streamable HTTP. GET und DELETE antworten mit 405 |
POST /api/material/upload | Token für den Direkt-Upload aus dem Browser. Diese Route ist die Zugangsprüfung |
GET /api/material/[id] | Datei ausliefern, mit Besitzprüfung unmittelbar neben dem Abruf |
/api/auth/[...path] | Anmeldeweg, vom Paket bereitgestellt |
POST /api/auth-mail | Mail-Webhook von Neon Auth. Signatur als abgetrenntes Ed25519-JWS geprüft - der rohe Rumpf muss roh bleiben. Nicht aktiviert |
6. MCP-Server#
27 Werkzeuge, 15 lesend, 12 schreibend. Zwei Transporte, eine Registrierung
(mcp/registrieren.ts) - sonst laufen die beiden Wege irgendwann auseinander.
# entfernt (der Normalfall)
claude mcp add --transport http planr https://deine-adresse/api/mcp --header "Authorization: Bearer planr_..."
# lokal über stdio
PLANR_URL=http://localhost:3000 PLANR_API_TOKEN=planr_... npm run mcpDer Endpunkt ist zustandslos, und das ist keine Sparsamkeit: Eine
Vercel-Funktion lebt für die Dauer einer Anfrage, eine Sitzung im Arbeitsspeicher
fände die nächste nicht wieder. Also je Anfrage ein frischer Server ohne
Sitzungskennung, und JSON statt Datenstrom (enableJsonResponse) - planr schickt
nichts von sich aus.
Der Endpunkt ruft die Routen auf, nicht die Bibliotheken darunter
(mcp/inprozess.ts, mcp/routen.ts). In den Routen stehen die Prüfung der
Eingaben und die Form der Antwort; daran vorbeizugehen hieße, sie ein zweites Mal
zu schreiben. Statt fetch bekommt der Aufrufer eine Zuordnung Pfad -> Handler
untergeschoben, damit ein Werkzeugaufruf keine zweite Funktionsausführung kostet.
Der Chat auf der Kursseite benutzt dieselbe Zuordnung, führt die Werkzeuge aber in der angemeldeten Sitzung aus statt über einen Schlüssel.
Vollständige Werkzeugliste und der Weg vom Rahmenplan zur Stundenliste:
docs/mcp.md. Die stdio-Hülle als npm-Paket liegt in
paket/.
7. Deployment#
Vercel-Projekt planr, Funktionen in fra1 (vercel.json), Datenbank in
eu-central-1. Beides gehört zusammen: planr stellt viele kleine Abfragen statt
weniger großer und ist damit latenzgebunden. Der Prüfstein ist der Kopf
x-vercel-id - dort muss fra1::fra1 stehen.
Die 18 Neon-Variablen verwaltet die Marketplace-Ressource, nicht die Hand. Datenbank wechseln heißt: Ressource trennen, neue verbinden.
CI (.github/workflows/pruefung.yml) läuft bei jedem Push und PR auf main:
npm test && npm run lint && npm run buildFehlermeldung über instrumentation.ts (onRequestError): Jeder Serverfehler
geht ins Protokoll und, wenn die Telegram-Variablen gesetzt sind, als Nachricht
hinaus. Derselbe Fehler auf derselben Route meldet sich höchstens alle zehn
Minuten. Kopfzeilen des Aufrufs gehen bewusst nicht mit - darin steht das
Sitzungs-Cookie.
Migrationen laufen nie automatisch. db:migrate ist ein Kommando von Hand.
Deployter Code vor einer Spalte, die es nicht gibt, ist der Fehler, den nachts
niemand bemerkt - deshalb rührt auch der nächtliche Zettel-Lauf nichts an, was
eine Migration braucht.
8. Mitarbeit#
Tests#
npm test51 Testdateien, ausgeführt über tsx, ohne Framework und ohne Datenbank. Die
Regel dahinter: Was gerechnet wird, wird rein gerechnet und dann geprüft.
Drei Testarten fallen aus dem Rahmen:
- Wachen über den Quelltext.
lib/freigabe.test.tsliest den Quelltext vonlib/freigabe.tsund verbietet dort bestimmte Feldnamen - wernotizen,reflexionodercheckrin der geteilten Datenschicht ergänzt, fliegt auf.lib/mehrbenutzer.test.tstut dasselbe für die Besitzprüfung in allen 41 Server-Actions,lib/planungsfreunde.test.tsfür die Vergleichsansicht. - Charakterisierungstests.
lib/engine/kalender.test.tshält fest, was der Schulkalender vor dem Umbau gerechnet hat: 39 Schulwochen, letzte am 28.06.2027, 74 Termine bei Di+Do. Beim Umstieg darf sich nichts verschieben. - Doppelte Rechnung.
lib/stundenplan-raster.test.tsmisst alle sechs echten Lerngruppen zweimal - einmal getippt, einmal geklickt -, und die Termine müssen gleich sein. Zwei Rechnungen nebeneinander wären zwei Gelegenheiten, auseinanderzulaufen.
Prüfskripte gegen die echte Umgebung#
| Skript | Was es prüft |
|---|---|
scripts/vplan-pruefen.ts | Adresse, Stand, abgedeckte Tage und jeden Befund des Vertretungsplans |
scripts/blob-pruefen.ts | Legt eine Wegwerf-Datei an und prüft, dass ihre Adresse ohne Anmeldung gesperrt ist |
scripts/upload-pruefen.ts | Die sechs Zugangsentscheidungen der Upload-Route |
scripts/mail-pruefen.ts | SPF, DKIM, DMARC, auf Wunsch eine echte Testmail |
scripts/chat-probe.ts --messen | Ob das kleine Modell die richtigen Werkzeuge greift |
Commit-Stil#
Die Commit-Nachricht sagt in einem deutschen Satz, was danach anders ist - nicht, was geändert wurde:
Oberstufenkurse gelten nicht mehr als ausgefallen
Der Zettel arbeitet sich nachts selbst ab
Ein Kurs lässt sich auch wieder loswerdenÄltere Commits tragen Präfixe (feat:, fix:, docs:), neuere nicht mehr.
Wo Entscheidungen dokumentiert werden#
Im Quelltext, direkt über der Stelle. planr hat ungewöhnlich lange Kommentare, und das ist Absicht: Sie erklären nicht, was der Code tut, sondern warum er nicht anders aussieht, und welche Alternative aus welchem Grund verworfen wurde. Wer etwas ändert, sollte den Kommentar zuerst lesen - meistens steht dort schon, warum die naheliegende Lösung nicht funktioniert hat.
Der größere Bogen steht in der CLAUDE.md im Wurzelverzeichnis; die Etappenpläne
liegen in docs/superpowers/plans/.
Regeln, die nicht verhandelbar sind#
- Keine Schülerdaten. Kein Feld, keine Tabelle, kein Umweg.
- Der Besitz wird in der Datenschicht geprüft, nicht in der Oberfläche.
- Nicht vorhanden und nicht meins geben dieselbe Antwort. Ein "gibt es nicht" gegen ein "gehört dir nicht" verriete, welche Ids belegt sind.
- Kopplungen dürfen still ausfallen, aber nicht still leer aussehen. Nicht konfiguriert und nicht erreichbar sind zwei verschiedene Zustände.
- Schemaänderungen über Migrationen, nie über push.
Weiterlesen