diff --git a/.planning/STATE.md b/.planning/STATE.md index 0ff6a5f..79f64cf 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -4,11 +4,11 @@ milestone: v1.2 current_phase: 17 current_phase_name: eigene-ausschreibungs-quellen-je-nutzer status: verified -stopped_at: "Quick 260910-jab abgeschlossen: T-JTS-02/T-JTS-03/WINDOWS #19 geschlossen (Migration 20260910120000). WINDOWS #24 neu offen (Verwaltungsweg fuer plattformweite Zeilen fehlt)." -last_updated: "2026-09-10T12:35:40.000Z" +stopped_at: "Quick 260910-krx abgeschlossen: Bereich dashboard der Etappe 2 (Mandantentrennung) umgestellt, 12/13 Zugriffe gebunden, WINDOWS #25 neu offen" +last_updated: "2026-09-11T07:05:45.197Z" last_activity: 2026-09-10 last_activity_desc: Quick 260910-jab — drei zu kurz greifende RLS-Regeln geschlossen (GroupMembership beide Seiten, ModuleGrant beide Ziele, TenderRssFeedSource Lese-/Schreibsplit), listForUser gebunden, Aktenstand kohaerent -state_head: e1586a41dd58432c489486abd408b406841e1a7f +state_head: 67b50240d6a3f0dca2c2bfbd0a129269f9a7ab53 progress: total_phases: 17 completed_phases: 3 @@ -118,6 +118,7 @@ Progress: [██████████] 100% | Phase 17 P03 | 62min | 3 tasks | 13 files | | Phase quick-260909-ipc P01 | 55min | 3 tasks | 8 files | | Phase quick-260910-jab P01 | 70min | 3 tasks | 16 files | +| Phase quick-260910-krx P01 | 26min | 3 tasks | 7 files | ## Accumulated Context @@ -295,6 +296,7 @@ Recent decisions affecting current work: - [Phase 17]: [260909-ipc]: resolveEmailForWrite() bleibt dauerhaft ungebunden (Befund A, T-IPC-04) — email/username sind plattformweit @unique - [Phase 17]: [260909-ipc]: getAllActiveConfigs()/onApplicationBootstrap() bleiben dauerhaft ungebunden (Befund B) — Klasse von (ldap-config.service.ts, ldapConfig) korrigiert auf beides - [Phase 17]: [260909-ipc]: Standardgruppen-Uebergabe (groups.service.ts) bewusst nicht angefasst — Reihenfolgebedingung fuer Etappe 4 +- [Phase 17]: 260910-krx: Bereich dashboard vollstaendig umgestellt — 12 gebunden, 1 begruendet ungebunden (Modulkatalog); getLayout/saveLayout gemeinsam gebunden; saveLayout uebersetzt PrismaClientUnknownRequestError (nicht P2002) in deutsche Konfliktmeldung; WINDOWS #25 fuer die beweisvernichtende Schleife offen angelegt ### Pitfalls & Anti-Patterns @@ -422,8 +424,8 @@ sind. Kein Anlass, sie vorher erneut vorzulegen. ## Session Continuity -Last session: 2026-09-10T12:35:40.000Z +Last session: 2026-09-11T07:05:44.595Z Resumed: 2026-09-09 — Sitzung ueber /gsd-resume-work fortgesetzt; Einstiegspunkt Etappe 2 (Bereich ldap), Auswahl des Nutzers steht aus. -Stopped at: Quick 260910-jab abgeschlossen — die drei zu kurz greifenden Datenbankregeln (T-JTS-02, T-JTS-03, WINDOWS #19) sind geschlossen, VORGEZOGEN auf ausdruecklichen Nutzerwunsch vor die restlichen fuenf offenen Bereiche der Etappe 2 (dashboard 13, calendar 12, tenant 8, favorites 7, auth 5, settings 4 — sechs, nicht fuenf, `settings` war bereits vorher als Reihenfolgebedingung fuer Etappe 4 markiert, Befund K). Diese fuenf/sechs Bereiche messen ab jetzt gegen die NEUEN Regeln aus 20260910120000_rls_widen_membership_grant_and_platform_read. WINDOWS #19 fixed, WINDOWS #24 neu offen (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle). Naechster Schritt laut vorheriger Uebergabe: Bereich dashboard (13 Zugriffe), danach wie zuvor geplant. Der User hat am 2026-09-09 gesagt, die restlichen Bereiche sollen ohne Rueckfrage durchlaufen; beim Scharfschalten (Etappe 4) wird ausdruecklich angehalten. ZWEI PRODUKTENTSCHEIDUNGEN DES USERS VOM 2026-09-10, beide fuer Etappe 3 (nach Abschluss von Etappe 2): (1) ANMELDENAMEN PRO MANDANT EINDEUTIG, nicht plattformweit — m.schmidt darf es bei Firma A und Firma B geben. Folge: `User.username`/`User.email` von `@unique` auf `@@unique([tenantId, username])`/`@@unique([tenantId, email])` umstellen, und der Anmeldeweg muss den Mandanten kennen, BEVOR er die Benutzerzeile sucht (heute kommt der Mandant erst AUS der Zeile; die drei SECURITY-DEFINER-Funktionen aus Etappe 1 suchen ueber den Namen allein). Ueblicher Weg: eigene Adresse je Mandant (Subdomain) oder Mandantenwahl beim Login. Loest zugleich den bewusst ungebundenen `resolveEmailForWrite`-Pfad (ldap) und die Kollisionskette unsichtbar->frei->P2002 (tenders, user). (2) KOLLEGEN DERSELBEN FIRMA STRIKT GETRENNT — jeder sieht nur seine eigenen gespeicherten Suchen, Favoriten, Dashboard-Anordnung. Heute trennt das nur der Anwendungscode; alle Regeln lesen `tenantId = current_tenant_id()` ohne Benutzerdimension. Folge: zweite Sitzungsvariable `app.current_user` samt `current_user_id()`, an jeder Bindungsstelle mitgesetzt, und Benutzerdimension in den Regeln der nutzerbezogenen Tabellen (TenderSavedSearch, TenderTriage, TenderNotificationPref, TenderEmailConfig, TenderRssFeedSource persoenliche Zeilen, DashboardLayout, WidgetInstance, SearchProvider, FavoriteLink, CalendarSource). Danach faengt die Datenbank auch einen Programmierfehler ab, der heute unbemerkt bliebe. Beides eigene Umbauten; Reihenfolge: erst Etappe 2 zu Ende, dann diese beiden als Etappe 3, dann Scharfschalten. +Stopped at: Quick 260910-krx abgeschlossen: Bereich dashboard der Etappe 2 (Mandantentrennung) umgestellt, 12/13 Zugriffe gebunden, WINDOWS #25 neu offen Resume file: None Last activity: 2026-09-10 - Completed quick task 260910-jab: Die drei zu kurz greifenden Datenbankregeln (T-JTS-02, T-JTS-03, WINDOWS #19) geschlossen diff --git a/.planning/quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/260910-krx-SUMMARY.md b/.planning/quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/260910-krx-SUMMARY.md new file mode 100644 index 0000000..511a5c1 --- /dev/null +++ b/.planning/quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/260910-krx-SUMMARY.md @@ -0,0 +1,217 @@ +--- +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.