From 926359b067596b8d627660d926831b885e74d8d9 Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 11 Sep 2026 16:35:05 +0200 Subject: [PATCH] docs: Auftrag fuer Etappe 3 der Mandantentrennung, gemessen statt erinnert --- .planning/HANDOFF.json | 95 ++++++++--- docs/mandantentrennung-etappe3-auftrag.md | 184 ++++++++++++++++++++++ 2 files changed, 261 insertions(+), 18 deletions(-) create mode 100644 docs/mandantentrennung-etappe3-auftrag.md diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json index ef96bd9..a8743f3 100644 --- a/.planning/HANDOFF.json +++ b/.planning/HANDOFF.json @@ -1,6 +1,6 @@ { "version": "1.0", - "timestamp": "2026-09-11T13:00:00.000Z", + "timestamp": "2026-09-11T14:30:00.000Z", "phase": null, "phase_name": "Mandantentrennung wirksam machen (Etappenarbeit ausserhalb der Phasen, ueber Quick-Tasks)", "phase_dir": null, @@ -9,30 +9,89 @@ "total_tasks": 4, "status": "paused", "completed_tasks": [ - {"id": 1, "name": "Etappe 1 / forTenant() repariert, Anmeldeweg ueber SECURITY DEFINER, 227 Zugriffe klassifiziert", "status": "done", "commit": "5228f28"}, - {"id": 2, "name": "Etappe 2 / alle zwoelf Bereiche gebunden, jeder einzeln verifiziert (260909-ipc .. 260911-gwh)", "status": "done", "commit": "1240932"}, - {"id": 3, "name": "Zwischendurch auf Anweisung des Users: drei Datenbankregeln geschlossen (260910-jab)", "status": "done", "commit": "03fb3bf"} + { + "id": 1, + "name": "Etappe 1 / forTenant() repariert, Anmeldeweg ueber SECURITY DEFINER, 227 Zugriffe klassifiziert", + "status": "done", + "commit": "5228f28" + }, + { + "id": 2, + "name": "Etappe 2 / alle zwoelf Bereiche gebunden, jeder einzeln verifiziert (260909-ipc .. 260911-gwh)", + "status": "done", + "commit": "1240932" + }, + { + "id": 3, + "name": "Zwischendurch auf Anweisung des Users: drei Datenbankregeln geschlossen (260910-jab)", + "status": "done", + "commit": "03fb3bf" + } ], "remaining_tasks": [ - {"id": 4, "name": "WINDOWS #27 schliessen: Relations-Blindstelle der Bestandsaufnahme (include:/_count: in fremde Tabellen) — ZWINGEND vor Etappe 4", "status": "not_started"}, - {"id": 5, "name": "Etappe 3a: Anmeldenamen pro Mandant eindeutig (Produktentscheidung User 2026-09-10) — Schema @@unique([tenantId, username/email]), Anmeldeweg kennt Mandant VOR der Suche, SECURITY-DEFINER-Funktionen mit zwei Gleichheitsbedingungen", "status": "not_started"}, - {"id": 6, "name": "Etappe 3b: Benutzerdimension in den Regeln (Produktentscheidung User 2026-09-10) — app.current_user/current_user_id(), forTenant() um userId erweitern, Regeln der zehn nutzerbezogenen Tabellen", "status": "not_started"}, - {"id": 7, "name": "Etappe 3c: Systemkontext fuer die sechs Hintergrunddienst-Faelle (#21 dkv, #30 settings, vier beides-Uebergaben aus tenders/ldap)", "status": "not_started"}, - {"id": 8, "name": "Etappe 4: Scharfschalten (DATABASE_URL auf tessera_app) mit rls-preflight.mjs — Vorabpruefung muss die stillen Leere-Faelle #23/#25/#26/#28/#31/#32 abdecken. USER WILL HIER GEFRAGT WERDEN.", "status": "not_started"} + { + "id": 4, + "name": "WINDOWS #27: Quick 260911-mkj — Plan 261e736 gueltig und geprueft, Executor gestartet; git log und .planning/quick/260911-mkj-*/ pruefen ob abgeschlossen", + "status": "in_progress" + }, + { + "id": 5, + "name": "Etappe 3b: Benutzerdimension in den Regeln — VOLLSTAENDIGER AUFTRAG in docs/mandantentrennung-etappe3-auftrag.md (Reihenfolge 3b -> 3a -> 3c)", + "status": "not_started" + }, + { + "id": 6, + "name": "Etappe 3a: Anmeldenamen pro Mandant — siehe docs/mandantentrennung-etappe3-auftrag.md; enthaelt EINE offene Produktfrage (Mandant per Subdomain oder Login-Wahl)", + "status": "not_started" + }, + { + "id": 7, + "name": "Etappe 3c: Systemkontext fuer die sechs Hintergrunddienst-Faelle — siehe docs/mandantentrennung-etappe3-auftrag.md", + "status": "not_started" + }, + { + "id": 8, + "name": "Etappe 4: Scharfschalten — USER WILL GEFRAGT WERDEN. Nicht noetig fuer das Live-Gehen am Dienstag 2026-09-15.", + "status": "not_started" + } ], "blockers": [ - {"description": "Etappe 4 darf erst nach Etappe 3 und nach Schliessen von WINDOWS #27 laufen. Der User hat ausdruecklich verlangt, beim Scharfschalten angehalten und gefragt zu werden.", "type": "process", "workaround": "Reihenfolge einhalten"} + { + "description": "Etappe 4 darf erst nach Etappe 3 und nach Schliessen von WINDOWS #27 laufen. Der User hat ausdruecklich verlangt, beim Scharfschalten angehalten und gefragt zu werden.", + "type": "process", + "workaround": "Reihenfolge einhalten" + } ], "async_jobs": [], "human_actions_pending": [], "decisions": [ - {"decision": "Anmeldenamen pro Mandant eindeutig, nicht plattformweit", "rationale": "Produktentscheidung des Users am 2026-09-10 — m.schmidt darf es bei Firma A und B geben", "phase": "Etappe 3"}, - {"decision": "Kollegen derselben Firma strikt getrennt — Benutzerdimension in die Datenbankregeln", "rationale": "Produktentscheidung des Users am 2026-09-10; heute trennt nur der Anwendungscode, die Datenbank kennt nur den Mandanten", "phase": "Etappe 3"}, - {"decision": "req.tenantPrisma entfernt, Middleware geloescht", "rationale": "Bei jeder Anfrage gebaut, nirgends gelesen; Middleware war nirgends registriert; neun Bereiche haben dienst-internes forTenant() als Konvention festgelegt", "phase": "260911-e2s"}, - {"decision": "Datenbankregeln VOR Abschluss von Etappe 2 vorgezogen", "rationale": "Ausdrueckliche Anweisung des Users am 2026-09-10 — offene Loecher werden vergessen", "phase": "260910-jab"}, - {"decision": "Datenverlust in der Datenbank ist derzeit hinnehmbar", "rationale": "User 2026-09-09: nichts laeuft produktiv. Gilt nur solange das so bleibt.", "phase": "Etappe 4 (Vorgriff)"} + { + "decision": "Anmeldenamen pro Mandant eindeutig, nicht plattformweit", + "rationale": "Produktentscheidung des Users am 2026-09-10 — m.schmidt darf es bei Firma A und B geben", + "phase": "Etappe 3" + }, + { + "decision": "Kollegen derselben Firma strikt getrennt — Benutzerdimension in die Datenbankregeln", + "rationale": "Produktentscheidung des Users am 2026-09-10; heute trennt nur der Anwendungscode, die Datenbank kennt nur den Mandanten", + "phase": "Etappe 3" + }, + { + "decision": "req.tenantPrisma entfernt, Middleware geloescht", + "rationale": "Bei jeder Anfrage gebaut, nirgends gelesen; Middleware war nirgends registriert; neun Bereiche haben dienst-internes forTenant() als Konvention festgelegt", + "phase": "260911-e2s" + }, + { + "decision": "Datenbankregeln VOR Abschluss von Etappe 2 vorgezogen", + "rationale": "Ausdrueckliche Anweisung des Users am 2026-09-10 — offene Loecher werden vergessen", + "phase": "260910-jab" + }, + { + "decision": "Datenverlust in der Datenbank ist derzeit hinnehmbar", + "rationale": "User 2026-09-09: nichts laeuft produktiv. Gilt nur solange das so bleibt.", + "phase": "Etappe 4 (Vorgriff)" + } ], "uncommitted_files": [], - "next_action": "WINDOWS #27 schliessen (Relations-Blindstelle), dann Etappe 3 planen. NICHT direkt scharfschalten.", - "context_notes": "Etappe 2 lief ueber zwoelf Quick-Tasks plus einen Regel-Durchlauf, jeder mit Planer, Plan-Pruefer, Executor, Verifizierer. ZEHN Lieferungen wurden vom jeweils NAECHSTEN Schritt gefangen, nie vom eigenen: vier geschrumpfte Zaehlungen, zwei nicht committete Messungen, zwei Zusammenfassungen mit N statt N-1, handgepflegte Dokumentstellen uebersprungen, Falsifizierungsnachweise nur in Commit-Nachrichten, eine Wegwerf-Tabelle ohne createdAt/updatedAt, ein Selbstwiderspruch, ein Pruefer der etwas als plausibel durchwinkte, ein still fehlgeschlagener git add, und zwei Agenten die am Sitzungslimit NACH getaner Arbeit abbrachen (dkv-Executor, gwh-Planer — beide Male lag die Arbeit vollstaendig auf der Platte; git status ist die Wahrheit, nicht der Bericht). Die vollstaendige Liste steht in jedem Planer-Auftrag der spaeten Bereiche. Nebenfunde ohne Mandantenbezug, alle behoben: DKV-Download-Luecke, drohender DKV-Passwortverlust, Selbstloesch-Riegel der nie griff, Startfehler bei Neuinstallation, adminResetPassword ohne Mandanten- und Rollenpruefung, Widget-Besitzriegel bei Favoriten, sechs luegende Kommentare, und fuenf Bereiche ohne jede Testdatei." -} + "next_action": "1. Pruefen ob 260911-mkj fertig ist (git status, git log, SUMMARY vorhanden?) — wenn ja verifizieren, abbuchen, pushen. 2. docs/mandantentrennung-etappe3-auftrag.md lesen. 3. Etappe 3b als /gsd-quick --validate starten. NICHT nachfragen ausser beim Scharfschalten.", + "context_notes": "USER-ANWEISUNG 2026-09-11: 'mach #27 und dann Etappe 3, nicht nachfragen. du machst alles, was ohne mich geht. am dienstag geht eine voll funktionsfaehige version live.' Live-Gehen braucht den Schalter NICHT (ein Mandant, BYPASSRLS-Stand laeuft heute auf alpha). Etappe 3 darf das Live-Gehen nicht blockieren. Nach jedem Durchlauf pushen. Die vorige Sitzung endete mit ~65% Kontext waehrend #27 lief; der Etappe-3-Auftrag wurde deshalb als eigenes Dokument geschrieben, gemessen statt erinnert. Etappe 2 lief ueber zwoelf Quick-Tasks plus einen Regel-Durchlauf, jeder mit Planer, Plan-Pruefer, Executor, Verifizierer. ZEHN Lieferungen wurden vom jeweils NAECHSTEN Schritt gefangen, nie vom eigenen: vier geschrumpfte Zaehlungen, zwei nicht committete Messungen, zwei Zusammenfassungen mit N statt N-1, handgepflegte Dokumentstellen uebersprungen, Falsifizierungsnachweise nur in Commit-Nachrichten, eine Wegwerf-Tabelle ohne createdAt/updatedAt, ein Selbstwiderspruch, ein Pruefer der etwas als plausibel durchwinkte, ein still fehlgeschlagener git add, und zwei Agenten die am Sitzungslimit NACH getaner Arbeit abbrachen (dkv-Executor, gwh-Planer — beide Male lag die Arbeit vollstaendig auf der Platte; git status ist die Wahrheit, nicht der Bericht). Die vollstaendige Liste steht in jedem Planer-Auftrag der spaeten Bereiche. Nebenfunde ohne Mandantenbezug, alle behoben: DKV-Download-Luecke, drohender DKV-Passwortverlust, Selbstloesch-Riegel der nie griff, Startfehler bei Neuinstallation, adminResetPassword ohne Mandanten- und Rollenpruefung, Widget-Besitzriegel bei Favoriten, sechs luegende Kommentare, und fuenf Bereiche ohne jede Testdatei." +} \ No newline at end of file diff --git a/docs/mandantentrennung-etappe3-auftrag.md b/docs/mandantentrennung-etappe3-auftrag.md new file mode 100644 index 0000000..6be6732 --- /dev/null +++ b/docs/mandantentrennung-etappe3-auftrag.md @@ -0,0 +1,184 @@ +# Mandantentrennung — Etappe 3: Auftrag fuer die naechste Sitzung + +Geschrieben am 2026-09-11 am Ende der Sitzung, die Etappe 2 abgeschlossen hat. +Zweck: Eine frische Sitzung soll Etappe 3 ohne Rueckfrage und ohne Neuermittlung +der Grundlagen beginnen koennen. Alles hier ist gemessen, nicht erinnert. + +## Randbedingungen vom User (2026-09-11) + +- **Dienstag, 2026-09-15, geht die erste voll funktionsfaehige Version live.** + Live-Gehen braucht den Schalter NICHT — heute laeuft alpha mit dem + BYPASSRLS-Stand und einem Mandanten, und das ist fuer den Betrieb + unerheblich. Etappe 3 darf das Live-Gehen nicht blockieren. +- **Nicht nachfragen.** Alles machen, was ohne den User geht. Was nicht ohne + ihn geht, am Ende benennen. +- **Beim Scharfschalten (Etappe 4) anhalten und fragen.** Das ist die einzige + Ausnahme, und sie steht seit dem 2026-09-09. +- **Nach jedem abgeschlossenen Durchlauf pushen** (`git push` genuegt, die + Push-URL zeigt auf localhost:3002). + +## Stand beim Einstieg + +- Etappe 2 abgeschlossen, alle zwoelf Bereiche gebunden, jeder einzeln + verifiziert. Endstand 994 Tests / 62 Dateien, 137 Live-Pruefungen, + Klassifikation 65 Paare / 68 ungebunden / 178 gebunden. +- WINDOWS #27 (Relations-Blindstelle der Bestandsaufnahme) ist als Quick-Task + `260911-mkj` in Arbeit oder abgeschlossen — `git log` und + `.planning/quick/260911-mkj-*/` pruefen. Nach dessen Abschluss: 72 Paare. +- Der Schalter ist AUS. `DATABASE_URL` zeigt auf Rolle `tessera`. + +## Die zwei Produktentscheidungen (User, 2026-09-10) + +1. **Anmeldenamen pro Mandant eindeutig**, nicht plattformweit. `m.schmidt` + darf es bei Firma A und Firma B geben. +2. **Kollegen derselben Firma strikt getrennt.** Jeder sieht nur seine eigenen + gespeicherten Suchen, Favoriten, Dashboard-Anordnung. + +## Etappe 3 in drei Teilen — empfohlene Reihenfolge + +### 3b zuerst: Benutzerdimension in den Regeln + +Warum zuerst: reiner Datenbank- und Helfer-Umbau, beruehrt den Anmeldeweg +NICHT, und schliesst die Klasse von Befunden, die in sieben Bereichen als +"Policy hat keine Benutzerdimension" festgehalten wurde. + +Gemessene Grundlagen: +- `forTenant(prisma, tenantId)` in `apps/api/src/prisma/prisma-tenant.extension.ts:111` + setzt genau EINE Sitzungsvariable: `set_config('app.current_tenant', ..., true)`. + `current_tenant_id()` ist in `20260618112133_rls_policies` definiert. +- Es gibt KEIN `app.current_user` und KEIN `current_user_id()` — nirgends in + Migrationen oder Quelltext. +- **14 Modelle tragen eine `userId`-Spalte:** CalendarSource, DashboardLayout, + FavoriteLink, GroupMembership, ModuleGrant, PasswordResetToken, SearchProvider, + TenderEmailConfig, TenderMatch, TenderNotificationPref, TenderRssFeedSource, + TenderSavedSearch, TenderTriage, WidgetInstance. NICHT alle davon sind + "persoenliche Daten": GroupMembership und ModuleGrant sind + Verwaltungsobjekte (ein Admin darf sie fuer andere sehen), + PasswordResetToken ist ein Anmelde-Artefakt, TenderMatch wird vom + Hintergrunddienst je Treffer geschrieben. Die Benutzerdimension gehoert + auf die ZEHN echten Nutzerobjekte; welche das sind, ist je Modell zu + entscheiden und am Ort zu begruenden — Vorgabe aus den Bereichs-Kritiken: + CalendarSource, DashboardLayout, FavoriteLink, SearchProvider (persoenliche + Zeilen), TenderEmailConfig, TenderNotificationPref, TenderRssFeedSource + (persoenliche Zeilen), TenderSavedSearch, TenderTriage, WidgetInstance. +- Der Anwendungscode trennt heute bereits korrekt nach Benutzer (in jedem + Bereich stichprobenartig belegt, Besitzpruefungen in ldap/dkv/dashboard/ + calendar/favorites/auth als Tests festgenagelt). Die Datenbank tut es + nicht. Das zweite Netz fehlt. + +Bauform, an der es sich zu orientieren gilt: +- Zweite Sitzungsvariable `app.current_user`, Funktion `current_user_id()` + nach dem Muster von `current_tenant_id()`. +- `forTenant()` bekommt einen optionalen dritten Parameter `userId` (oder + ein Schwesterhelfer `forTenantAndUser()` — Entscheidung im Plan, mit + Begruendung; Praezedenz fuer "Helfer erweitern statt zweiten bauen" ist + `withTenantTransaction()`). Hintergrunddienste und Verwaltungswege rufen + weiter ohne userId; nur die Nutzer-CRUD-Wege setzen ihn. +- Regeln der zehn Tabellen: Lesen `tenantId = current_tenant_id() AND + (current_user_id() IS NULL OR userId = current_user_id())` — damit ein + Aufruf OHNE gesetzten Benutzer (Admin, Hintergrunddienst) weiter alles + des Mandanten sieht. Schreiben analog. Das IS-NULL-Muster ist die Form aus + der #19-Loesung (260910-jab), dort fuer plattformweite Zeilen. +- Messen, nicht annehmen: `rls-scratch-check.mjs` bekommt einen Abschnitt + je umgestellter Tabelle, ueber den GENERIERTEN Client, mit + schemagleicher Wegwerf-Tabelle (Spaltenvergleich zur Laufzeit — + `createdAt`/`updatedAt`-Falle aus 260910-krx). +- Die drei loch-behauptenden Pruefungen aus `tenders` und `dashboard` + ("Policies haben keine Benutzerdimension", z. B. + `tendersavedsearch-fremder-nutzer-desselben-mandanten-sichtbar`) + UMDREHEN, nicht loeschen — Muster aus 260910-jab. + +### 3a danach: Anmeldenamen pro Mandant + +Warum danach: aendert das Schema UND den Anmeldeweg, ist der riskanteste +Teil, und braucht 3b nicht. + +Gemessene Grundlagen: +- `User.username String @unique` und `User.email String? @unique` + (`schema.prisma:31-32`) — plattformweit. +- Die drei SECURITY-DEFINER-Funktionen in + `20260909160000_auth_lookup_functions`: `auth_lookup_user_by_username(p_username)`, + `auth_lookup_user_by_email(p_email)`, `auth_lookup_reset_token(p_token)`. + Jede sucht ueber EINE Gleichheitsbedingung mit `LIMIT 1`. Der Mandant + kommt erst AUS der gefundenen Zeile. +- Aufrufer: `auth.service.ts:95,110,218` und `user.service.ts:33`. +- Bewusst ungebundene Stellen, die genau an dieser plattformweiten + Eindeutigkeit haengen und mit 3a fallen: `ldap.service.ts` + `resolveEmailForWrite` (260909-ipc, Befund A), die P2002-Kollisionskette in + `tenders`/`user` (unsichtbare Zeile -> falsches "frei" -> harter Fehler), + `user.service.ts` `findByUsername` (null Aufrufer, 260910-das). + +Bauform: +- Schema: `@@unique([tenantId, username])`, `@@unique([tenantId, email])` + statt `@unique`. Migration mit Datenpruefung davor (heute ein Mandant, + also keine Kollision moeglich — trotzdem messen). +- Der Anmeldeweg muss den Mandanten kennen, BEVOR er die Zeile sucht. + Zwei uebliche Wege: (i) eigene Adresse je Mandant (Subdomain + `firma-a.tessera.ctl.de` -> Mandant aus dem Host), (ii) Mandantenwahl + beim Login. **Das ist eine Produktfrage, die der User NICHT beantwortet + hat.** Empfehlung fuer die Planung: (i), weil Tessera hinter Nginx Proxy + Manager laeuft und Subdomains dort trivial sind, und weil (ii) den + Anmeldenamen als Geheimnis schwaecht. Wenn die Planung das anders sieht, + ist es einer der Punkte, die am Ende dem User genannt werden. +- Die drei Funktionen bekommen eine zweite Gleichheitsbedingung + (`p_tenant_id`) — ENGER, nicht weiter. Die Kopfkommentare in + `docs/mandantentrennung-datenbankrolle.md` nachziehen. +- Bis der Mandant vor der Suche bekannt ist, laesst sich 3a NICHT + scharfschalten. Deshalb ist 3a fuer Dienstag NICHT noetig: mit einem + Mandanten ist plattformweit = pro Mandant. + +### 3c zuletzt: Systemkontext fuer die Hintergrunddienste + +Sechs Faelle, alle im Abschnitt "Der Hintergrunddienst als Falle" der +Klassifikation und in den Bereichs-Kritiken: +- `dkv-scheduler` / `loadAnyActiveConfigForScheduler()` (WINDOWS #21) — + heute bereits falsch (willkuerlicher Mandant). +- `mail.module` / `loadAnySmtpConfigForStartupTransport()` (WINDOWS #30) — + dieselbe Form. +- `ldap-config.service` `getAllActiveConfigs()` / `onApplicationBootstrap` + — heute korrekt, verstummt spaeter. +- `tender-digest.scheduler`, `tender-matching.service` — uebergreifende + Haelften an Etappe 3 uebergeben (260909-laa). +- `admin-seed.service` `ensureDefaultGroupsForAllTenants` — Schleife ueber + alle Mandanten, Rumpf bereits gebunden. + +Bauform: Ein benannter Systemkontext (z. B. `forSystem(prisma)`), der eine +DRITTE Sitzungsvariable `app.system_context = 'true'` setzt, und Regeln, +die diesen Kontext fuer LESENDE Zugriffe zulassen (`OR current_setting( +'app.system_context', true) = 'true'`). Schreibzugriffe innerhalb der +Schleife bleiben je Mandant gebunden. Der DKV- und der SMTP-Startpfad +werden dann zu "einmal-abfragen-viele-bedienen" — das ist die in 07-04 +zurueckgestellte Mehrmandanten-Planung und der einzige echte +Funktionsausbau in Etappe 3. + +## Was NICHT ohne den User geht + +- **3a, Weg (i) vs. (ii):** wie der Mandant beim Login bestimmt wird. +- **Etappe 4:** Scharfschalten. Ausdruecklich. + +## Werkzeuge und Fallen (aus Etappe 2, jede mindestens einmal erlebt) + +- `git status` ist die Wahrheit, nicht der Agentenbericht. Zwei Agenten + brachen am Sitzungslimit NACH getaner Arbeit ab. +- Ein `grep -c` ist eine Behauptung. Vier Kopfzahlen schrumpften beim + Hineinsehen. +- Eine nicht committete Messung ist kein Beleg (zweimal passiert). +- Zusammenfassungen behaupten N, wo N-1 geliefert ist (zweimal passiert). +- Handgepflegte Dokumentstellen werden uebersprungen — Gates ABLEITEN. +- Roh-SQL ist nicht der generierte Client (Wegwerf-Tabelle ohne + `createdAt`/`updatedAt`). +- Ein Plan-Pruefer, der "plausibel" sagt, hat nicht geprueft. +- `git add` mit mehreren Pfaden, einer davon geloescht, scheitert still. +- Der Datenbank-Container hat keinen Host-Port: IP per `docker inspect` + frisch ermitteln, `tessera:tessera_dev`. Prisma-Binary aus + `apps/api/node_modules/.bin/prisma`, NICHT `npx prisma` (zieht Prisma 8). +- Kein `mailhog` lokal — `ENOTFOUND mailhog` ist Umgebung, kein Defekt. +- Backticks in Heredoc-Python werden von der Shell ausgewertet — Skripte + in eine Datei schreiben, dann ausfuehren. + +## Einstieg + +`/gsd-resume-work`, dann `/gsd-quick` fuer 3b mit `--validate`. Planer, +Plan-Pruefer, Executor, Verifizierer — die Kette vollstaendig, jede +Lieferung wurde in Etappe 2 vom jeweils naechsten Schritt gefangen, nie +vom eigenen.