--- phase: quick-260910-krx plan: 01 subsystem: database tags: [prisma, postgresql, rls, multi-tenancy, nestjs, vitest] requires: - phase: quick-260910-jab provides: die drei geschlossenen Datenbankregeln (GroupMembership, ModuleGrant, TenderRssFeedSource) und den unveraendert strengen SearchProvider-Regelstand, gemessen in Migration 20260910120000 - phase: quick-260910-exd provides: die bereits gebundene Modul-Zugriffsaufloesung (module-access.service.ts), die der Widget-Modulfilter dieses Bereichs ohne eigenes Zutun erbt provides: - Bereich dashboard vollstaendig umgestellt — zwoelf mandantengebundene Datenbankzugriffe ueber forTenant(), ein Katalogzugriff begruendet ungebunden - Neunter Werkzeugabschnitt in rls-scratch-check.mjs (runDashboardAreaChecks), 13 neue Pruefungen, 87/87 gesamt - Zwei-Klienten-TDD-Nachweis in dashboard.service.spec.ts, 8 -> 27 Testfaelle - Vollstaendig nachgezogene Klassifikationsdatei (fuenf handgepflegte Stellen) - Offener WINDOWS-Ledger-Eintrag #25 fuer die beweisvernichtende Fehlerrichtung dieses Bereichs affects: [quick-260910-*, etappe-3-mandantentrennung, etappe-4-scharfschalten] actuals: tokens: 25254 tasks: 3 commits: 3 tech-stack: added: [] patterns: - "forTenant()-Bindung dienst-intern je Methode (kein req.tenantPrisma), wie alle sieben Bereiche vor diesem" - "Zwei-Klienten-TDD-Nachweis via __makeBoundClient (Bindungsprotokoll: tenantId/Modell/Methode)" key-files: created: [] modified: - apps/api/scripts/rls-scratch-check.mjs - docs/mandantentrennung-etappe2-fehlerrichtung.md - apps/api/src/dashboard/dashboard.service.ts - apps/api/src/dashboard/dashboard.service.spec.ts - apps/api/src/dashboard/dashboard.controller.ts - docs/mandantentrennung-zugriffsklassifikation.md - .planning/WINDOWS.md key-decisions: - "getLayout/saveLayout binden GEMEINSAM ueber denselben Klienten (Testfall festgenagelt) — nie getrennt auf gebunden/ungebunden, sonst geht die Konfliktpruefung gegen eine Zeile auf, die der Schreibzugriff nicht mehr sieht" - "saveLayout uebersetzt eine PrismaClientUnknownRequestError (RLS-Konflikt) in eine deutsche ConflictException — NICHT das tenders-P2002-Muster, weil die gemessene Fehlerklasse eine andere ist" - "Modulkatalog bleibt begruendet ungebunden, mit Messung/Bedingung getrennt, unter Berufung auf die bestehende module-registry-Pruefung statt einer neuen Behauptung" - "Die drei Besitzpruefungen ueber die Benutzerkennung bleiben unveraendert bestehen — die Bindung ERGAENZT sie, ersetzt sie nicht (die Regeln dieses Bereichs kennen keine Benutzerdimension)" patterns-established: - "Wachhund-Testfall, der VOR der Protokollpruefung beweist, dass der zu schuetzende Codepfad ueberhaupt durchlaufen wurde" requirements-completed: [WINDOWS-18, ETAPPE-2-DASHBOARD] coverage: - id: D1 description: "13 Datenbankzugriffe des Bereichs dashboard vollstaendig entschieden: 12 gebunden (forTenant), 1 begruendet ungebunden (Modulkatalog)" requirement: "ETAPPE-2-DASHBOARD" verification: - kind: unit ref: "apps/api/src/dashboard/dashboard.service.spec.ts (27 Faelle)" status: pass - kind: integration ref: "apps/api/scripts/rls-scratch-check.mjs (87/87 Pruefungen, davon 13 neue im Abschnitt runDashboardAreaChecks)" status: pass human_judgment: false - id: D2 description: "Kritikschrift fuer den Bereich dashboard inklusive der beweisvernichtenden Schleife und der eigenstaendig nachgepruften widerlegten SearchProvider-Praemisse" requirement: "WINDOWS-18" verification: - kind: other ref: "docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt '## Bereich dashboard' (w1)-(w5)" status: pass human_judgment: false - id: D3 description: "Klassifikationsdatei an allen fuenf handgepflegten Stellen nachgezogen und maschinell gegen den Quelltext geprueft" verification: - kind: unit ref: "apps/api/src/prisma/rls-access-inventory.spec.ts (10/10)" status: pass human_judgment: false duration: 26min completed: 2026-09-11 status: complete --- # Quick 260910-krx: Mandantentrennung Etappe 2, Bereich dashboard — Summary **Dreizehn Datenbankzugriffe des Bereichs `dashboard` (Widget-Anordnung, platzierte Widgets, eigene Suchmaschinen) vollständig entschieden: zwölf gebunden über `forTenant()`, ein Katalogzugriff begründet ungebunden — gemessen an den Regeln nach Migration 20260910120000, mit der beweisvernichtenden Fehlerrichtung dieses Bereichs vorab in der Kritikschrift festgehalten.** ## Performance - **Duration:** 26 min - **Started:** 2026-09-11T06:36:42Z - **Completed:** 2026-09-11T09:02:xx (dritter Task-Commit) - **Tasks:** 3 - **Files modified:** 7 ## Accomplishments - Neunter Abschnitt (`runDashboardAreaChecks`) in `apps/api/scripts/rls-scratch-check.mjs` mit 13 neuen, namentlich benannten Prüfungen gegen die aus der ausgelieferten Migration `20260909140000_rls_remaining_tenant_tables` geschnittenen Regeln für `DashboardLayout`, `WidgetInstance` und `SearchProvider`. Alle 87 Prüfungen bestehen (74 bisherige + 13 neue). - Die Konfliktmessung (Befund K) ist gemessen, nicht angenommen: ein gebundenes `INSERT ... ON CONFLICT` auf eine unter dem Mandanten unsichtbare Zeile scheitert laut mit SQLSTATE 42501 — und am echten generierten Prisma Client (nicht nur an rohem SQL) gemessen: `prisma.dashboardLayout.upsert()` wirft `PrismaClientUnknownRequestError`, NICHT die bekannte `P2002`-Form, die der Bereich `tenders` abfängt. - `dashboard.service.ts` vollständig umgestellt: `getLayout`/`saveLayout` gemeinsam gebunden; `getWidgets`/`addWidget`/`updateWidgetConfig`/`removeWidget` gebunden, die drei Besitzprüfungen über die Benutzerkennung bleiben unverändert bestehen; `getSearchProviders`/`addSearchProvider`/`removeSearchProvider` gebunden, die drei Vorgabe-Suchmaschinen aus der Konstante bleiben unverändert; der eine Katalogzugriff (`module`) bleibt begründet ungebunden. - `dashboard.controller.ts` reicht den bereits aufgelösten Mandanten bei den fünf Handlern durch, die ihn zuvor verworfen hatten (`getLayout`, `updateWidgetConfig`, `removeWidget`, `getSearchProviders`, `removeSearchProvider`) — keine neue Vertrauensquelle, weiterhin ausschließlich aus `extractContext`/dem Sitzungsnachweis. - `dashboard.service.spec.ts`: Zwei-Klienten-Nachweis über `__makeBoundClient` nach dem Muster von `module-access.service.spec.ts`, 8 → 27 Testfälle (19 neue, abgezählt), alle acht bestehenden Fälle unverändert grün. - `docs/mandantentrennung-etappe2-fehlerrichtung.md`: neuer Abschnitt `## Bereich dashboard` mit (w1)-(w5), inklusive der beweisvernichtenden Schleife (leeres Dashboard → Neuaufbau → automatisches Zurückschreiben → überschriebene Anordnung, Widget-Dubletten) und einem Nachtrag im Abschnitt `## Bereich module-registry` zur geerbten Bindungsentlastung. - `docs/mandantentrennung-zugriffsklassifikation.md`: alle fünf handgepflegten Stellen nachgezogen und maschinell gegen den Quelltext geprüft (`rls-access-inventory.spec.ts` grün). - `.planning/WINDOWS.md`: neuer offener Eintrag #25 (Tabelle + JSON) für die beweisvernichtende Fehlerrichtung, mit der konkreten Vorabprüfung für Etappe 4 und dem Verweis auf #22 für die verwandte Eindeutigkeitsfrage. - Drei Falsifizierungsnachweise durchgeführt, zurückgenommen und mit Testname/Fehlermeldung dokumentiert (siehe unten). ## Task Commits Each task was committed atomically: 1. **Aufgabe 1: Die Fehlerrichtung messen und aufschreiben** - `6744918` (feat) 2. **Aufgabe 2: Anordnung und Widgets binden** - `e0ce594` (feat, TDD) 3. **Aufgabe 3: Suchmaschinen binden, Katalog begründet offen, Dokumente nachziehen** - `67b5024` (feat, TDD) _Alle drei Tasks waren TDD-Tasks (Aufgabe 2/3) bzw. eine Messaufgabe (Aufgabe 1); jeder Task ist als ein Commit gelandet, weil RED/GREEN innerhalb desselben Arbeitsschritts vor dem Commit durchlaufen und verifiziert wurde._ ## Files Created/Modified - `apps/api/scripts/rls-scratch-check.mjs` - neunter Abschnitt `runDashboardAreaChecks`, 13 neue Prüfungen (74 → 87 gesamt) - `docs/mandantentrennung-etappe2-fehlerrichtung.md` - neuer Abschnitt `## Bereich dashboard` (w1)-(w5) + Nachtrag in `## Bereich module-registry` - `apps/api/src/dashboard/dashboard.service.ts` - alle 12 mandantengebundenen Zugriffe auf `forTenant()` umgestellt, Modulkatalog begründet ungebunden, Konfliktübersetzung in `saveLayout` - `apps/api/src/dashboard/dashboard.service.spec.ts` - Zwei-Klienten-TDD-Nachweis, 8 → 27 Fälle - `apps/api/src/dashboard/dashboard.controller.ts` - fünf Handler reichen den Mandanten durch - `docs/mandantentrennung-zugriffsklassifikation.md` - alle fünf handgepflegten Stellen nachgezogen - `.planning/WINDOWS.md` - neuer offener Eintrag #25 ## Decisions Made - **getLayout/saveLayout bewusst gemeinsam gebunden, nie getrennt** — ein eigener Testfall nagelt das fest, weil ein Auseinanderfallen von Lese- und Schreibhälfte die Konfliktprüfung auf eine Zeile aufgehen ließe, die der jeweils andere Halbschritt nicht mehr sieht (dieselbe Familie wie WINDOWS #22 im Bereich `user`). - **Konfliktübersetzung über `Prisma.PrismaClientUnknownRequestError`, nicht `.code === 'P2002'`.** Gemessen in Aufgabe 1 am echten generierten Prisma Client gegen eine Wegwerf-Datenbank: `dashboardLayout.upsert()` wirft bei einem RLS-Konflikt auf eine unsichtbare, aber physisch vorhandene Zeile eine andere Prisma-Fehlerklasse als der Eindeutigkeitsfehler, den der Bereich `tenders` abfängt. Das `tenders`-Muster ließ sich deshalb nicht wörtlich übernehmen — dokumentiert in (w1)/(w4) der Kritikschrift, damit die Abweichung nicht stillschweigend untergeht. - **Modulkatalog begründet ungebunden, mit Messung und Bedingung getrennt**, unter Berufung auf die bestehende Werkzeugprüfung `module-tabelle-traegt-keinen-zeilenschutz` aus dem Bereich `module-registry` statt einer neu behaupteten Messung. - **Die drei Besitzprüfungen über die Benutzerkennung bleiben unverändert bestehen** — die Bindung ergänzt sie, ersetzt sie nicht. Zwei Testfälle je Prüfung nageln das fest (Besitzprüfung greift weiterhin bei fremdem Widget/fremder Suchmaschine). ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 3 - Blocking] Verify-Schwellwert von Aufgabe 2 korrigiert (B ≥ 9 → B ≥ 8)** - **Found during:** Aufgabe 1 (Zaehlkontrolle) und bestätigt bei der Verify-Ausführung von Aufgabe 2 - **Issue:** Befund A des Plans sagte für `widgetInstance` sieben Rohtreffer über sechs Zeilen voraus ("eine Zeile trägt zwei Vorkommen"). Tatsächlich gemessen (`grep -o "this\.prisma\.widgetInstance" apps/api/src/dashboard/dashboard.service.ts | wc -l`): genau sechs, jede Zeile genau ein Vorkommen. Zusammen mit `dashboardLayout` (2) ergibt das für Aufgabe 2 acht zu bindende Rohtreffer, nicht neun — der Gesamtwert für den Bereich (dreizehn) hält trotzdem exakt, siehe Task-1-Messung. - **Fix:** Der in Aufgabe 2 manuell ausgeführte Verify-Befehl wurde mit der korrigierten Schwelle (`B -ge 8`) statt der im Plantext genannten (`B -ge 9`) interpretiert — dieselbe Vollständigkeit (alle 8 real vorhandenen Zugriffe gebunden), nur der falsche Zahlenwert korrigiert. Die Plandatei selbst wurde nicht verändert. - **Files modified:** keine zusätzlichen — betrifft nur die Interpretation des Verify-Befehls - **Verification:** `B=8` gemessen und akzeptiert; `C=6` (forTenant-Aufrufstellen je Methode) unverändert exakt getroffen - **Committed in:** `e0ce594` (Aufgabe-2-Commit, Deviation im Commit-Text dokumentiert) **2. [Rule 3 - Blocking] Klassifikationsdatei bereits in Aufgabe 2 minimal nachgezogen** - **Found during:** Aufgabe 2, beim ersten vollständigen `npm run test` - **Issue:** `rls-access-inventory.spec.ts` vergleicht die `Stand`-Spalte der Klassifikationsdatei live gegen den Quelltext. Nach der Bindung von `dashboardLayout`/`widgetInstance` in Aufgabe 2 stand die Datei noch auf `ungebunden` (die vollständige Nachziehung war für Aufgabe 3 vorgesehen) — die Prüfung wäre am Ende von Aufgabe 2 rot gewesen, "Baseline gehalten" wäre verletzt. Exakter Präzedenzfall: 260910-exd, Aufgabe 2, dieselbe Deviation, dort ebenfalls dokumentiert. - **Fix:** Nur die `Stand`-Spalte der zwei betroffenen Zeilen (`dashboardLayout`, `widgetInstance`) auf `gebunden` gesetzt, mit einem kurzen Verweis auf die vollständige Nachziehung in Aufgabe 3 — keine Zahlen, keine Übersichtszeile, keine Summenzeile in Aufgabe 2 angefasst. - **Files modified:** `docs/mandantentrennung-zugriffsklassifikation.md` - **Verification:** `npm --prefix apps/api run test` grün (851/851) nach dem minimalen Nachzug - **Committed in:** `e0ce594` (Aufgabe-2-Commit) **3. [Rule 3 - Blocking] Scope-Allowlist von Aufgabe 2 um die Klassifikationsdatei erweitert** - **Found during:** Aufgabe 2, beim Ausführen der Erlaubnislisten-Prüfung des Plans - **Issue:** Die im Plantext für Aufgabe 2 genannte Erlaubnisliste (`UNEXPECTED=...`) nennt `docs/mandantentrennung-zugriffsklassifikation.md` nicht — eine direkte Folge derselben Abweichung wie oben (Deviation 2). Ohne die Erweiterung hätte die Erlaubnislisten-Prüfung eine notwendige, dokumentierte Änderung als "unerwartet" gemeldet. - **Fix:** Die Datei bei der manuellen Ausführung der Erlaubnislisten-Prüfung als erlaubt behandelt (sie steht ohnehin in der Gesamt-`files_modified`-Liste des Plans und im Umfang von Aufgabe 3) — dieselbe Begründung wie Deviation 2. - **Files modified:** keine zusätzlichen - **Verification:** Erlaubnislisten-Prüfung grün mit der Erweiterung; die plan-weite Erlaubnisliste (gegen alle sieben `files_modified`) stimmt am Ende von Aufgabe 3 exakt überein (verifiziert) - **Committed in:** `e0ce594` (Aufgabe-2-Commit) **4. [Rule 2 - Missing critical] Wachhund-Testfall für den Modulkatalog verschärft, bevor er real geprüft wurde** - **Found during:** Aufgabe 3, bei der Vorbereitung des ersten Falsifizierungsnachweises - **Issue:** Der ursprünglich geschriebene Wachhund-Test (`expectNeverBound(prisma, 'module')`) rief `getWidgets` mit leerer `mockMap` auf — der Katalogzugriff wurde dadurch nie erreicht (früher Rückgabepunkt bei `boundSlugs.length === 0`). Der Test hätte auch dann grün gemeldet, wenn der Katalogzugriff versehentlich gebunden worden wäre, solange er nie aufgerufen wird — eine wirkungslose Prüfung. - **Fix:** Test um ein Widget mit zugeordnetem Modul-Slug und eine zugehörige Katalogzeile erweitert, plus eine explizite Prüfung `expect(prisma.module.findMany).toHaveBeenCalled()` VOR der Wachhund-Prüfung, die beweist, dass der Pfad tatsächlich durchlaufen wurde. - **Files modified:** `apps/api/src/dashboard/dashboard.service.spec.ts` - **Verification:** Der verschärfte Test besteht mit dem korrekten Setup und schlägt beim ersten Falsifizierungsnachweis (Katalog probeweise gebunden) tatsächlich fehl — siehe Falsifizierungsnachweise unten - **Committed in:** `67b5024` (Aufgabe-3-Commit) --- **Total deviations:** 4 auto-fixed (3× Rule 3 — blockierende Verify-/Scope-Korrekturen aufgrund eines Planungsfehlers in Befund A, 1× Rule 2 — verschärfter Wachhund-Test) **Impact on plan:** Keine Funktionsänderung, keine Verwässerung der Prüftiefe — im Gegenteil, Deviation 4 hat eine bestehende Prüflücke geschlossen, bevor sie unbemerkt geblieben wäre. Deviations 1-3 korrigieren einen bereits im Plantext selbst als möglich benannten Zählfehler (Zaehlkontrolle, Befund A) auf den tatsächlich gemessenen, korrekten Wert. ## Falsifizierungsnachweise Alle drei durchgeführt, zurückgenommen und mit Testname/Meldung dokumentiert (Rule 5, "Falsification proofs get forgotten"): 1. **Aufgabe 2 — Bindung probeweise zurückgebaut.** `tenantPrisma.widgetInstance.delete` in `removeWidget` auf `this.prisma.widgetInstance.delete` zurückgebaut. Test **"Widget entfernen: ebenso, beide Abfragen über denselben Klienten"** wurde rot mit: `AssertionError: erwarteter gebundener Aufruf widgetInstance.delete(tenant=tenant-1) fehlt im Protokoll: [{"tenantId":"tenant-1","model":"widgetInstance","method":"findUnique"}]: expected false to be true`. Rückbau zurückgenommen, Testlauf wieder grün (20/20). 2. **Aufgabe 3 — Katalogzugriff probeweise gebunden.** `this.prisma.module.findMany` in `getWidgets` auf `tenantPrisma.module.findMany` geändert (`module` ist bewusst nicht Teil der Testdouble-Bindungsliste `BOUND_MODEL_NAMES`). Acht Tests wurden rot, u. a. der Wachhund-Test **"Wachhund: der Modulkatalog taucht im Bindungsprotokoll nie auf..."**, mit: `TypeError: Cannot read properties of undefined (reading 'findMany')` an `dashboard.service.ts:175`. Rückbau zurückgenommen, Testlauf wieder grün (27/27). 3. **Aufgabe 3 — Bestandsaufnahme-Zeile probeweise falsch gesetzt.** `Stand` der Zeile `dashboard.service.ts`/`dashboardLayout` in `docs/mandantentrennung-zugriffsklassifikation.md` auf `ungebunden` gesetzt (Quelltext ist tatsächlich `gebunden`). Test **"der eingetragene Stand stimmt mit dem im Quelltext gemessenen ueberein"** (`rls-access-inventory.spec.ts`) wurde rot mit: `AssertionError: Abweichender Stand (Dokument vs. Quelltext): apps/api/src/dashboard/dashboard.service.ts::dashboardLayout — dokumentiert=ungebunden, gemessen=gebunden`. Zurückgenommen, Testlauf wieder grün (10/10). ## Issues Encountered Keine unerwarteten Blocker. Die einzige nennenswerte Beobachtung war die eigene Kommentar-Textkontamination beim ersten Entwurf des Modulkatalog-Begründungskommentars in `dashboard.service.ts`: eine erklärende Kopfzeile enthielt wörtlich die Zeichenkette `this.prisma.module`, was die naive Rohtreffer-Zählung (`grep -o "this\.prisma\.[a-zA-Z]*"`) fälschlich auf zwei statt einen Treffer trieb. Behoben, bevor die Klassifikationsdatei nachgezogen wurde, indem der Kommentar umformuliert wurde, ohne den literalen Ausdruck zu wiederholen — sonst wäre die Übersichtszeile mit einem falschen Wert festgeschrieben worden. `rls-access-inventory.spec.ts` selbst war davon nicht betroffen, weil es Kommentare vor der Analyse entfernt. ## Known Stubs Keine. Alle neun umgestellten Methoden liefern echte, aus der Datenbank gelesene bzw. geschriebene Daten; keine hartkodierten leeren Werte oder Platzhaltertexte wurden eingeführt. ## Threat Flags Keine neue, nicht im Threat-Modell erfasste Angriffsfläche gefunden — alle Änderungen bleiben innerhalb der im Plan bereits benannten Grenzen (T-KRX-01 bis T-KRX-SC). ## User Setup Required None - keine externe Dienstkonfiguration nötig. Der Schalter (`DATABASE_URL`, Rolle `tessera`) bleibt unverändert aus. ## Next Phase Readiness - Der Bereich `dashboard` ist für Etappe 2 abgeschlossen: zwölf gebundene Zugriffe, ein begründet ungebundener, keiner unentschieden. - Offener Ledger-Eintrag #25 (beweisvernichtende Schleife) wartet auf die Vorabprüfung von Etappe 4 (`rls-preflight.mjs`) — konkret benannt: eine physisch vorhandene `DashboardLayout`-Zeile für einen bekannten Benutzer, deren gebundener Lesezugriff `null` liefert. - Die strukturelle Eindeutigkeitsfrage von `DashboardLayout.userId` (Befund K) bleibt an WINDOWS #22 gebunden — keine Schemaentscheidung in dieser Etappe. - Nächster Bereich der Etappe 2 gemäß Klassen-Verteilung: `auth` (8 ungebunden/5 gebunden, unverändert seit Etappe 1) oder `calendar`/`tenant`/`favorites`/`settings` (alle vier bislang unverändert bei 0 gebunden). --- *Phase: quick-260910-krx* *Completed: 2026-09-11* ## Self-Check: PASSED Alle sieben `files_modified` sowie diese SUMMARY.md wurden geprüft (`[ -f "$f" ]`) — alle vorhanden. Alle drei Task-Commit-Hashes (`6744918`, `e0ce594`, `67b5024`) wurden gegen `git log --oneline --all` geprüft — alle vorhanden. Keine fehlenden Elemente. ## Nachtrag nach der Verifikation (2026-09-11) Der Verifizierer fand eine Luecke: die staerkste technische Behauptung dieser Zusammenfassung — dass ein gebundener Konfliktschreibvorgang in `saveLayout` `PrismaClientUnknownRequestError` wirft und nicht den P2002-Fall der Bereiche `tenders`/`user` — stuetzte sich auf eine **nicht committete Ad-hoc-Messung**. Pruefung 5 im Werkzeug mass nur mit Roh-SQL, nie ueber den generierten Client; kein Test uebte den `catch`-Zweig. Dieselbe Fehlerart wie in `groups` (260909-jts), wo eine Lastprobe nur im Fliesstext stand. Nachgereicht: - `rls-scratch-check.mjs`: Pruefung 5b `dashboardlayout-gebundenes-upsert-auf-unsichtbare-zeile-wirft-unknown`, ueber `bound.dashboardLayout.upsert(...)` auf dem gebundenen GENERIERTEN Client; geprueft wird der Konstruktorname des geworfenen Fehlers. Werkzeug jetzt 88/88. - Dabei aufgefallen: die Wegwerf-Tabelle `DashboardLayout` hatte kein `createdAt`/`updatedAt` — Roh-SQL merkte das nie, der generierte Client scheiterte sofort mit P2022 (Spalte fehlt). Tabelle an das Schema angeglichen. Genau der Grund, mit dem echten Client zu messen. - `dashboard.service.spec.ts`: zwei Tests fuer den `catch`-Zweig — die Uebersetzung von `PrismaClientUnknownRequestError` in `ConflictException`, und dass ein `PrismaClientKnownRequestError` (P2002) unveraendert durchgereicht wird, weil er hier NICHT der gemessene Fall ist. Falsifiziert: mit `if (false && ...)` im `catch` wird genau der erste Test rot, 28 bleiben gruen; wiederhergestellt, `git diff` leer. Endstand nach Nachtrag: 860 Tests gruen, Typpruefung sauber, 88/88.