# Entscheidungen (ADR-Kurzform) Festgehaltene Design-Entscheidungen samt Begründung. Bei Änderungen: Eintrag ergänzen, nicht löschen. ## D1 — `|` für Oder-Zerlegung, `-` für Und-Zerlegung `|` bedeutet in Regex und BNF-Grammatiken bereits „oder“ und ist damit selbsterklärend. `-` ist das gewohnte Markdown-Listenzeichen für „normale“ Teilpakete. Das Gate steckt im Aufzählungszeichen der Kinder, nicht in einer Annotation am Parent — lokal lesbar, trivial parsebar. ## D2 — Status als erweiterte Markdown-Task-Checkbox GitHub-Markdown kennt `[ ]`/`[x]`; die Erweiterung auf `[?] [~] [/] [^] [-]` wirkt sofort vertraut. Mnemonik: `?` vage, leeres Kästchen = todo, `~` gängige In-progress-Konvention, `/` = halbes `x`, `x` = erledigt, `^` = „nach oben deployed“, `-` = durchgestrichen. ## D3 — Begriff „Durchstich“ für den Zwischenstatus Zustand zwischen „in Arbeit“ und „fertig“: durchgängig funktionsfähig und vorführbar, Feinarbeiten offen. „Durchstich“ ist als Fachbegriff (Tunnelbau, End-to-End-Slice in der Softwarearchitektur) etabliert. Verworfene Alternativen: „Feinschliff“, „Richtfest“, „vorführbereit“, „Beta“. ## D4 — Begriff „verworfen“ statt „gestrichen“ Beschreibt die bewusste Entscheidung gegen eine Option — besonders passend für Any-of-Alternativen. Default ausgeblendet, Toggle „verworfene einblenden“. ## D5 — Farb-Logik folgt dem Risiko, nicht dem Fortschritt Rosé = Kosten investiert, Risiko hoch (in Arbeit). Pastellgelb = läuft, Restrisiko klein (Durchstich). Flieder = geplant (Absicht, nichts investiert, zwischen Grau der Idee und Rosé der Arbeit). Ergebnis-Skala: grau → flieder → rosé → gelb → grün → blau. ## D6 — Größe in Klammern, URL nackt, Personen mit `@` `(M)` liest sich wie ein Kleidungsetikett. URLs werden ohne Link-Syntax erkannt (einfach einfügbar); die URL wird **vor** den `@`-Tags extrahiert, damit `https://user@host/…` nicht kollidiert. `@` ist die universelle Mention-Konvention (GitHub, Slack, Jira). ## D7 — `%%` als Kommentarzeichen Mermaid-Konvention; hält den Weg zum Mermaid-Plugin frei. `//` scheidet wegen URLs aus, `#` bleibt für Referenzen/Tags reserviert, `` ist zu sperrig. ## D8 — Untergliederungspflicht ab M sichtbar machen Fehlende Zerlegung wird nicht nur gemeldet, sondern **gezeigt**: Geister-Knoten „Untergliederung fehlt“ hängt genau dort, wo der Ast weitergehen müsste. Verworfene Elemente sind ausgenommen. Werden alle Kinder eines M+-Elements verworfen und ausgeblendet, erscheint der Platzhalter wieder (es braucht eine neue Zerlegung) — gewollt. ## D9 — Transponierte Darstellung mit unterschiedlichen Austrittsseiten Im vertikalen Modus tritt all of **rechts** aus dem Parent aus (LR-Baum), any of **unten links**. Austrittsseite + Linienstil (durchgezogen/gestrichelt) codieren das Gate doppelt; Beschriftungen werden nie rotiert. ## D10 — Abzweige zielen auf Knotenmitte, nicht Teilbaummitte Feste Knoten-Zeilenhöhe macht die Anschlusshöhe deterministisch (23 px). Behebt „ins Leere laufende“ Linien bei eingerückten Unterbäumen. ## D11 — Technologie: Vanilla HTML/CSS/JS, eine Datei als Prototyp Keine Frameworks, kein Build-Zwang. Parser ~30 Zeilen, Renderer erzeugt verschachtelte `ul.and`/`ul.or`; Linien via CSS-Pseudo-Elemente. Modularisierung (Parser/Renderer/UI getrennt + Tests) ist der geplante nächste Schritt. ## D12 — Name „Werkbaum" und Marke aus der Notation Name: „Werk" + „Baum" = wörtlich der WBS-Baum; funktioniert als npm-Paket, CLI-Befehl und Mermaid-Schlüsselwort (`werkbaum`, kleingeschrieben in Code-Kontexten). Verworfene Kandidaten: Astrein, undoder, Aufriss, Gabelung. Marke: Miniatur des Diagramms (K2) in zwei Orientierungen analog zu den Darstellungsmodi — Hochformat als Primärzeichen, transponiert als Sekundärzeichen. Nur Tinte + Petrol; durchgezogen = und, gestrichelt = oder; Pastelltöne bleiben Statusfarben. Details: brand/BRAND.md. ## D13 — Backend in Kotlin/Spring Boot, Monorepo-Struktur Backend als Kotlin/Spring-Boot-Anwendung (Gradle Kotlin DSL, JDK 21), entwickelt in IntelliJ IDEA Ultimate. Monorepo mit `frontend/` und `backend/`; CLAUDE.md dreistufig: Wurzel für Querschnitt, je eine pro Teilprojekt (wird von Claude Code pfadbezogen geladen). Kein Node- oder Python-Backend. ## D14 — Parser-Hoheit liegt beim Frontend, SPEC ist normativ Der Notationstext ist das führende Datenformat. Das Backend parst die Notation nicht; es speichert den Text als Ganzes und bedient Integrationen über explizite Metadaten. Wird Backend-Parsen später doch erforderlich, gilt: docs/SPEC.md ist normativ, beide Parser testen gegen dieselben Fixtures (SPEC §10) — es darf keine zweite, abweichende Grammatik entstehen. ## D15 — Kompakter Modus + graue Any-of-Linien in allen Modi Der vertikale Modus (D9) lässt „all of“ rechts austreten — formal sauber, aber breit. Der zusätzliche Modus **kompakt** führt beide Gates nach unten (minimale Breite) und unterscheidet sie nur noch über den Linienstil: durchgezogen = und, gestrichelt = oder. Auswahl dreistufig „horizontal/vertikal/kompakt“ statt Toggle, da es nun drei sich ausschließende Darstellungen sind. Zugleich wird das **Petrol im Diagramm ganz aufgegeben**: die any-of-Linien (Sammelleiste *und* Abzweige) und auch der **Rahmen der Alternative-Knoten** sind in **allen** Modi **gestrichelt bzw. gerahmt in Grau** (`#6B7A8C`). Die frühere durchgezogene Petrol-Sammelleiste und der petrolfarbene Knotenrahmen entfallen. Begründung: Das Gate ist bereits durch den Linienstil (gestrichelt) und die Anordnung codiert; die zusätzliche Signalfarbe wirkte laut und ließ die durchgezogene Hauptlinie optisch mit „all of“ konkurrieren. Grau tritt zurück, die gestrichelte Führung bleibt eindeutig. D12/BRAND — „durchgezogen = und, gestrichelt = oder“ — gilt für das **Markenzeichen** unverändert (Logo behält Petrol); im **Diagramm** trägt allein der Linienstil die Gate-Codierung. ## D16 — Deployment über GitHub Pages via Actions-Workflow Der Editor ist eine einzelne statische Datei (D11) — GitHub Pages genügt, kein eigener Server nötig. Veröffentlicht wird über den offiziellen Actions-Weg (`actions/upload-pages-artifact` + `actions/deploy-pages`, `permissions: pages/​id-token`, `concurrency: pages`) statt über den `gh-pages`-Branch: kein Zusatz­branch, OIDC statt Deploy-Key, Trigger bei Push auf `main` und manuell. Der Test-Step (Vitest, Phase 1) ist als Platzhalter auskommentiert vorbereitet. **Pfad-Entscheidung:** `frontend/index.html` referenziert Favicon und MIT-Lizenz relativ mit `../` (`../docs/brand/favicon.svg`, `../LICENSE`) — von der Wurzel-URL aus zeigten diese über die Site hinaus. Statt die Quelldatei zu ändern (D14/CLAUDE: „Editor nicht refaktorieren“) zieht der Workflow die `../`-Pfade **nur auf der Site-Kopie** gerade (`sed`) und legt die referenzierten Dateien passend ab: `index.html` an die Wurzel, `docs/brand/` und `LICENSE` daneben. So bleibt die Quelle unverändert (lokal weiter per `file://` und Dev-Server nutzbar), und veröffentlicht wird nur das Nötige — `backend/` und die übrigen `docs/` bleiben außen vor. **Versionsnummer:** `Major.Minor` steht in der Datei `VERSION` und wird per bewusstem **Bump-Commit** gepflegt; die **Micro-Stelle** leitet der Build aus der Anzahl der Commits seit dem letzten VERSION-Bump ab (`git rev-list --count ..HEAD`) und ersetzt sie per `sed` nur auf der Site-Kopie im Footer. Bewusst aus der Commit-Historie statt aus der Run-Nummer (Vorentscheidung, verworfen): vollständig aus dem Repo reproduzierbar, monoton, ohne zurückgeschriebenen Commit/Tag und ohne selbstauslösenden Deploy-Zyklus. Die Micro-Stelle beginnt nach jedem Bump wieder bei 0 (SemVer-artig). Nötig ist ein vollständiger Klon (`fetch-depth: 0`), sonst zählt der flache CI-Klon nur einen Commit. `1.0` bleibt die Version beim lokalen Öffnen (Platzhalter im Quelltext). Im Footer sind Name und Nummer **getrennte Links**: „Werkbaum" → Repo-Startseite, die Versionsnummer (``) → exakt der deployte Commit (`…/commit/`, im Build via `git rev-parse HEAD`). **Build-Hinweis (Vorschau / „latest build"):** Nicht-produktive Builds tragen hinter dem Titel ein kleines Symbol samt Tooltip, damit klar ist, dass es nicht die *eigentliche* (stabile) Instanz ist. Drei Zustände, gesteuert per Vite-Env `VITE_BUILD_BADGE` (Auswertung in `app.js`, `mountBuildBadge`): - **Dev-Server** (`import.meta.env.DEV`) → 🔧 „Vorschau – lokaler Entwicklungsstand". - **Default-Build** `npm run build` (Env ungesetzt) → 🚧 „latest build …". Der GitHub-Pages-Deploy nutzt genau diesen Default und trägt den Hinweis dadurch automatisch — **keine** `sed`-Injektion mehr nötig. - **Produktions-Build** `npm run build:prod` (Vite-Modus `prod`, `frontend/.env.prod` setzt `VITE_BUILD_BADGE=none`) → **kein** Badge; esbuild eliminiert den Zweig als toten Code, das Symbol steht dann nicht einmal mehr im Ausgabe-Quelltext. Damit trägt einzig die echte produktive Installation keinen Hinweis. **Warum in die App-Quelle statt per Workflow-`sed` (frühere Lösung):** Nur so sieht der Dev-Server den Hinweis ebenfalls — ein Post-Build-`sed` erreicht den Dev-Server nicht, der die Quelle direkt ausliefert. Die Umkehrung „Hinweis ist der Normalfall, Prod schaltet ab" passt zudem zur Anforderung (nur Prod bleibt sauber) und macht den Default sicher: wer den Prod-Schritt vergisst, veröffentlicht einen sichtbar als Entwicklungsstand markierten Build, nicht versehentlich einen als stabil wirkenden. Bewusst **kein** i18n-UI-Text im `I18N`-Objekt (kein Produkt-Feature, sondern Build-Metainformation; D14) — der `title` ist knapp zweisprachig (DE · EN). **Self-hosted Deploy (`scripts/deploy-prod.sh`):** Für eine eigene produktive Installation (nicht GitHub Pages) baut das Skript per `npm run build:prod` (badge-frei), stellt lokal dasselbe zusammen wie der Pages-Workflow (LICENSE danebenlegen + `../LICENSE`-Link geradeziehen, Footer-Version + Commit-Link) und spiegelt es per `rsync --delete` über SSH ins Zielverzeichnis (nichts Altes bleibt stehen). Die Zusammenstell-Schritte (dieselben drei `sed`-Regeln) liegen bewusst in **beiden** — Workflow und Skript —, weil GitHub Pages die `dist/` selbst baut und der SSH-Weg keinen Pages-Runner hat; bei Änderungen an den `sed`-Regeln beide Stellen nachziehen (Anker `` / `../LICENSE`). (Nummerierung: D15 war bereits für den kompakten Modus vergeben, daher D16.) ## D17 — Kleiner Bildschirm: ein Bereich, kompakte Legende & Sprachwahl Auf schmalen Viewports (≤ 640 px) ist für beide Bereiche kein Platz. Diagramm und Editor werden gestapelt und über den **Splitter stufenlos** geteilt (frei ziehbar, jederzeit erneut verstellbar); die beiden **Titelzeilen bleiben immer stehen** (Grid-Zeilen-Minima = gemessene Kopfhöhen), ein **Tipp auf eine Titelzeile klappt dieses Panel ganz aus**. Das frühere diskrete Modell (Snap in Minimier-Zustände 'a'/'b') führte auf dem Smartphone zu zwei Fehlern: nach dem ersten Ziehen rastete der Splitter in eine feste Aufteilung und war nicht mehr zu bewegen (die Titelzeile im Snap-Rand fing jeden weiteren Zug ab), und der Titelzeilen-Tap funktionierte nur im `collapsed`-Zustand — nach einem freien Zug also gar nicht. Deshalb auf Mobil ein **kontinuierliches** Modell ohne Snap/ Collapse (`--drow` frei geklemmt, Tap setzt sie aufs Extrem); Desktop behält das diskrete Modell samt Fenster-Buttons. Die Legende wird von der Bereichs- Umschaltung **entkoppelt** und über einen eigenen Umschalter im Editor-Kopf gesteuert — das dauerhafte „AGENDA“-Band kostet sonst zu viel Höhe. Die Sprachleiste zeigt eingeklappt nur die **aktive** Sprache; ein Tipp klappt die volle Leiste als **Overlay über die Kopfzeile** auf (rechtsbündig, `z-index`, verdeckt die übrigen Elemente statt die Zeile umzubrechen), nach der Auswahl — oder einem Tipp daneben — klappt sie wieder ein. So bleibt die Kopfzeile schmal und die vollständige Sprachliste dennoch erreichbar (frühere Lösung „EN + aktive + …“ ersetzt). Voreinstellung ist **Vollbild** (maximale Fläche). Umgesetzt über die Klasse `body.mobile` (per `matchMedia`), damit CSS und JS denselben Schwellwert teilen; die Vollbild- Voreinstellung greift nur, wenn noch **keine** gespeicherte Nutzerwahl vorliegt (eine bewusste Abschaltung bleibt so erhalten, siehe localStorage-Persistenz). **Nachtrag — auf Mobil ist jetzt immer genau EIN Bereich zu sehen; der Splitter entfällt dort ganz.** Die oben beschriebene stufenlose Aufteilung hat das eigentliche Problem nur verwaltet, statt es zu lösen: Auf 375 px ist für zwei Bereiche kein Platz. Jede Aufteilung war ein Kompromiss, in dem **beide** Bereiche zu klein waren, und die praktisch einzigen sinnvollen Stellungen waren ohnehin die Extreme — also genau das, was ein Umschalter direkt anbietet. Der Splitter kostete außerdem dauerhaft eine zweite Titelzeile plus 14 px Griff für etwas, das man auf dem Telefon nicht dosiert, sondern wechselt. **Der Umschalter zeigt das Ziel, nicht den Zustand.** Damit weicht er bewusst vom Modus-Wähler daneben ab, der das aktive Icon zeigt und reihum schaltet: Der hat drei Zustände, die man ohne Anzeige nicht auseinanderhält; hier gibt es zwei, und welcher gerade gilt, sieht man am ganzen Bildschirm. Ein Knopf, der den Zustand anzeigt, den man ohnehin vor sich hat, sagt nichts — einer, der das Ziel zeigt, sagt, was passiert. Umgesetzt als **zwei feste Knöpfe**, je einer pro Titelzeile: Weil immer nur eine Zeile sichtbar ist, braucht keiner sein Icon zu wechseln. **Der Dokumenten-Wähler steht neben dem Umschalter** und damit nur im Textbereich. Das ist keine Platzentscheidung, sondern die richtige Zuordnung: Er bestimmt, *welchen Text* man bearbeitet (D22) — im Diagrammbereich wäre er ein Fremdkörper. Die übrigen Aktions-Knöpfe bleiben, wo sie sind, und werden dadurch von selbst mitgeschaltet; ihre komprimierte Mobil-Form (Modus-Wähler als Reihum-Icon, Download als Overlay) bleibt unverändert. **Die Falle beim Bauen: ein `display:none`-Panel misst sich zu null.** Der verborgene Bereich wird wirklich ausgeblendet — nur so bekommt der sichtbare die ganze Höhe. Alles, was aus der Live-Geometrie zeichnet, liefert dann aber Unsinn, und `render()` läuft bei **jedem Tastendruck** im Textbereich. Nachgemessen nach einer Eingabe bei verborgenem Diagramm: Pfad-Linie **weg**, null Stationspunkte, kein `--stem-x`. Das Umschalten zeichnet deshalb neu — zum Diagramm hin dieselben vier Schritte wie ein Moduswechsel (`applyOptStairs`, `alignStems`, `drawCheapPath`, `drawDepLinks`), zum Text hin der Zeilennummern-Streifen, der ebenso am Spiegel misst (D33). Danach wieder 48 px Pfadlänge, 5 Punkte, `--stem-x: 67,4px`. Der sichtbare Bereich wird in `werkbaum-ui` gemerkt — global über alle Dokumente wie der übrige Ansichts-Zustand (D22). Die Sprünge zwischen Diagramm und Text (D25) holen den nötigen Bereich selbst nach vorn: `revealEditor()` schaltet auf Text (wie es auf dem Desktop ein zugeklapptes Panel aufklappt), `focusNodeOfCaret()` auf das Diagramm — und zwar **vor** dem Zentrieren, sonst misst sich der Zielknoten noch zu null. Entfallen sind damit `--drow` auf Mobil, die Grid-Minima `--pmin-d`/`--pmin-e`, `syncPanelMins()`, `setMobileDrow()` und das Titelzeilen-Tippen; Desktop behält Splitter, Presets und Fenster-Buttons unverändert. ## D18 — Günstigsten Pfad per Inversion zeigen, fehlende Größe = M Der kostengünstigste Weg durch den Baum wird hervorgehoben (Umschalter im Diagramm-Kopf, Default an, Zustand persistiert). Nötig sind bei **all of** alle Kinder, bei **any of** nur die günstigste Alternative (kleinste rekursive Kosten; Gleichstand ⇒ erste). **Darstellung per Inversion, nicht per Betonung:** Getestet wurde zuerst ein doppelt dicker Rand an den nötigen Knoten. Da der Baum aber fast überall all-of ist, war damit *fast alles* dick umrandet — das Signal lag ohnehin nur in der *Abwesenheit* des Rands bei den ausgeschlossenen Alternativen. Deshalb wird invertiert: die **nicht benötigten** Knoten (nicht-gewählte any-of-Alternativen samt Teilbaum) treten zurück (blass, entsättigt), der Pfad hebt sich von selbst ab. Kein Zusatzrahmen, weil die Knoten-Ecken (Größe, Tags, ↗) schon dicht sind. **Fehlende T-Shirt-Größe wird für die Kostenschätzung als `M` gewertet** — die kleinste Größe mit Untergliederungspflicht (§5/D8), also die konservative Annahme „mindestens M". Damit sind auch ungrößte Alternativen vergleichbar. Wo `M` nur implizit angenommen ist, zeigt der Knoten ein **invertiertes** Größen-Badge (weiß mit Petrol-Rand statt gefüllt) samt Tooltip, damit die Annahme sichtbar und von einer echten `(M)`-Angabe unterscheidbar bleibt. Die Größen-Semantik in SPEC §5 bleibt unberührt (die Wertung gilt nur der Pfad-Kostenrechnung). **Pfad-Linie + Stationspunkte (U-Bahn-Plan):** Zusätzlich zur Inversion fädelt eine gestrichelte, geschwungene Petrol-Linie durch die **Endknoten** des Pfads. Sie liegt **hinter** den Knoten (kräftig in den Lücken), mit einer **abgetönten Kopie davor** (deutet den Verlauf beim Durchschreiten eines Knotens nur an — Alternative zum harten „über allem"). Problem dabei: läuft die Linie durch Fremd-/Zwischenknoten, ist nicht unterscheidbar, ob diese dazugehören (besonders im kompakten Modus). Lösung: ein **blasser, großer Stationspunkt** nur an den echten Blättern — durchquerte Knoten bleiben punktlos. Blass+groß statt klein+satt, damit der Knotentext lesbar bleibt. Alles auch im Grafikexport (hinten/vorne-Schichtung nachgezeichnet). **Layout: all-of unter any-of (horizontal).** Zerlegt eine any-of-Alternative selbst wieder all-of (z. B. „Web+Nativ" → Web + Android + iOS), würde der breite horizontale Fächer den Alternativknoten zentrieren und samt Elternbaum weit nach rechts schieben. Deshalb wird dieser Teilbaum **nur horizontal** schmal **transponiert** gestapelt (Kinder untereinander, linker solider Verteiler — wie im kompakten Modus), passend zur gestapelten any-of-Spalte darüber. Kompakt führt all-of ohnehin nach unten (Knoten bleibt oben, Abzweig bei 23 px passt). **Vertikal** dagegen behält den zentrierten Rechts-Fächer: `li.has-and` legt den Alternativknoten `align-items:center` **vertikal mittig** zu seiner Kindergruppe. Dann sitzt aber auch der **einkommende** any-of-Abzweig nicht mehr bei 23 px, sondern muss auf die **Knotenmitte (50 %)** zeigen — sonst ist der Knoten von seiner Linie von oben abgetrennt. Fix: `ul.or>li.has-and` bekommt vertikal **symmetrisches** Padding (Mitte bleibt bei 50 %), der Abzweig (`::before`) und die Rail-Endkante (`:last-child::after`) werden auf 50 % gesetzt — analog zu den bereits zentrierten all-of-Zwischenknoten. ## D19 — Modularisierung mit Vite-Bündelung zu einer self-contained Datei Das Nahziel „tragfähige Codebasis" (ROADMAP) verlangt, den Single-HTML- Prototyp in headless nutzbare Module (`parser`, `model`, `render`, `app`) zu zerlegen und gegen SPEC zu testen. Die Zwickmühle: **modulare Einzeldateien**, **`file://`-Tauglichkeit** (D16) und **kein Build** (D11) sind nicht gleichzeitig erfüllbar — ES-`import` über `file://` blockt der Browser (CORS). **Entscheidung: Vite als Bündler/Testrunner.** `frontend/src/*.js` sind echte ES-Module und die Quelle der Wahrheit; `frontend/index.html` lädt im Dev-Server per `