docs(quick-260911-cwh): Etappe 2 Bereich calendar abgeschlossen und verifiziert
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 53s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s

This commit is contained in:
2026-09-11 10:08:01 +02:00
parent 06038b9dfa
commit f1017fa6e9
3 changed files with 309 additions and 1 deletions
+1
View File
@@ -386,6 +386,7 @@ None yet.
| 260910-exd | Mandantentrennung Etappe 2, Bereich module-registry — der Berechtigungs-Anfrageweg. `module.guard.ts` laeuft bei JEDER Modulanfrage und hat null eigene Datenbankzugriffe; seine Richtigkeit ist vollstaendig eine Funktion dessen, was dieser Bereich liefert. Endstand 7 ungebunden / 10 gebunden. **Eine Vorgabe des Auftrags war falsch und wurde widerlegt:** angeblich wuerde eine Bindung des Modulkatalogs ihn fuer jeden Mandanten unsichtbar machen — gemessen ueber alle 23 `ENABLE ROW LEVEL SECURITY`-Zeilen steht `Module` auf keiner, die Tabelle traegt gar keinen Zeilenschutz und kein `tenantId`. Eine Bindung waere heute WIRKUNGSLOS, nicht katastrophal; katastrophal wird sie erst, wenn Etappe 3 der Tabelle eine Regel gibt. Handlung unveraendert (Katalog bleibt ungebunden), aber Messung und Bedingung sind in Code und Dokument jetzt getrennt — eine richtige Handlung mit falscher Begruendung haelt nur, bis sich jemand auf die Begruendung verlaesst. **Die Kernfrage ehrlich beantwortet:** es gibt KEIN Signal, das 'wirklich keine Freigabe' von 'die Abfrage hat nichts gefunden' unterscheidet. Nach dem Scharfschalten saehe ein Unterlauf nicht wie ein Fehler aus, sondern wie 'du hast keine Module' — leere Seitenleiste, leerer Marktplatz, jeder Modulaufruf abgewiesen, fuer den Betroffenen nicht von einem absichtlichen Entzug zu unterscheiden. Dreifach festgehalten: im Text, als Testfall in `module.guard.spec.ts` (zwei Aufrufe, identische Meldung, gegeneinander gehalten) und als WINDOWS #23 mit konkreter Etappe-4-Vorabpruefung. **Erstmals eine Entlastung, die strukturell haelt:** die Kette unsichtbare Zeile -> falsches 'frei' -> 23505 kann hier nicht auftreten, weil die Eindeutigkeitsschluessel die Mandantenkennung fuehren — erster von sechs Bereichen, in dem sie abwesend statt umgangen ist; entsprechend wurde KEINE Absicherung eingebaut, die nichts absichert. **Wieder zwei Kopfkommentare mit Falschaussagen** (`isModuleActive` 'Used by ModuleGuard', `findActiveForTenant` als Marktplatz-Lieferant), beide Methoden mit null Aufrufern — berichtigt. `module-registry.service.ts` hatte trotz 11 der 17 Zugriffe und aller Schreibwege GAR KEINE Testdatei. Reihenfolge-Entlastung: der Dashboard-Filter wird mitgebunden, ohne dass eine `dashboard`-Datei angefasst wird. Zwei Abweichungen, beide vom eigenen Pruefgatter erzwungen und geprueft: eine Identitaets-Attrappe in `tender-scheduler.service.spec.ts` (zulaessig — jene Datei prueft Planer-Verhalten, die Bindung ist in `module-registry.service.spec.ts` mit zwei Klienten belegt) und eine Stand-Spalte, die eine Aufgabe frueher nachgezogen werden musste. **Verifiziert 9/9** (833/833 Tests, Typpruefung sauber, 66/66 Live-Pruefungen; Klassenverteilung und Summenzeile vom Pruefer eigenhaendig nachgerechnet). Eine Zahl in der Zusammenfassung (7 statt 6 neue Bindungsnachweise) vom Pruefer nachgezaehlt und berichtigt | 2026-09-10 | 7d45e2f,3df7268,9c0eefe | [260910-exd-mandantentrennung-etappe-2-bereich-modul](./quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/) | | 260910-exd | Mandantentrennung Etappe 2, Bereich module-registry — der Berechtigungs-Anfrageweg. `module.guard.ts` laeuft bei JEDER Modulanfrage und hat null eigene Datenbankzugriffe; seine Richtigkeit ist vollstaendig eine Funktion dessen, was dieser Bereich liefert. Endstand 7 ungebunden / 10 gebunden. **Eine Vorgabe des Auftrags war falsch und wurde widerlegt:** angeblich wuerde eine Bindung des Modulkatalogs ihn fuer jeden Mandanten unsichtbar machen — gemessen ueber alle 23 `ENABLE ROW LEVEL SECURITY`-Zeilen steht `Module` auf keiner, die Tabelle traegt gar keinen Zeilenschutz und kein `tenantId`. Eine Bindung waere heute WIRKUNGSLOS, nicht katastrophal; katastrophal wird sie erst, wenn Etappe 3 der Tabelle eine Regel gibt. Handlung unveraendert (Katalog bleibt ungebunden), aber Messung und Bedingung sind in Code und Dokument jetzt getrennt — eine richtige Handlung mit falscher Begruendung haelt nur, bis sich jemand auf die Begruendung verlaesst. **Die Kernfrage ehrlich beantwortet:** es gibt KEIN Signal, das 'wirklich keine Freigabe' von 'die Abfrage hat nichts gefunden' unterscheidet. Nach dem Scharfschalten saehe ein Unterlauf nicht wie ein Fehler aus, sondern wie 'du hast keine Module' — leere Seitenleiste, leerer Marktplatz, jeder Modulaufruf abgewiesen, fuer den Betroffenen nicht von einem absichtlichen Entzug zu unterscheiden. Dreifach festgehalten: im Text, als Testfall in `module.guard.spec.ts` (zwei Aufrufe, identische Meldung, gegeneinander gehalten) und als WINDOWS #23 mit konkreter Etappe-4-Vorabpruefung. **Erstmals eine Entlastung, die strukturell haelt:** die Kette unsichtbare Zeile -> falsches 'frei' -> 23505 kann hier nicht auftreten, weil die Eindeutigkeitsschluessel die Mandantenkennung fuehren — erster von sechs Bereichen, in dem sie abwesend statt umgangen ist; entsprechend wurde KEINE Absicherung eingebaut, die nichts absichert. **Wieder zwei Kopfkommentare mit Falschaussagen** (`isModuleActive` 'Used by ModuleGuard', `findActiveForTenant` als Marktplatz-Lieferant), beide Methoden mit null Aufrufern — berichtigt. `module-registry.service.ts` hatte trotz 11 der 17 Zugriffe und aller Schreibwege GAR KEINE Testdatei. Reihenfolge-Entlastung: der Dashboard-Filter wird mitgebunden, ohne dass eine `dashboard`-Datei angefasst wird. Zwei Abweichungen, beide vom eigenen Pruefgatter erzwungen und geprueft: eine Identitaets-Attrappe in `tender-scheduler.service.spec.ts` (zulaessig — jene Datei prueft Planer-Verhalten, die Bindung ist in `module-registry.service.spec.ts` mit zwei Klienten belegt) und eine Stand-Spalte, die eine Aufgabe frueher nachgezogen werden musste. **Verifiziert 9/9** (833/833 Tests, Typpruefung sauber, 66/66 Live-Pruefungen; Klassenverteilung und Summenzeile vom Pruefer eigenhaendig nachgerechnet). Eine Zahl in der Zusammenfassung (7 statt 6 neue Bindungsnachweise) vom Pruefer nachgezaehlt und berichtigt | 2026-09-10 | 7d45e2f,3df7268,9c0eefe | [260910-exd-mandantentrennung-etappe-2-bereich-modul](./quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/) |
| 260910-jab | **Die drei zu kurz greifenden Datenbankregeln geschlossen** — auf ausdrueckliche Anweisung des Users VORGEZOGEN, entgegen der geplanten Reihenfolge (urspruenglich nach Etappe 2, damit jeder Bereich gegen einen stabilen Regelstand misst; der User entschied anders, weil offene Loecher vergessen werden). Erster Durchlauf dieser Serie, der die DATENBANK aendert statt nur Anwendungscode — neue Migration `20260910120000_rls_widen_membership_grant_and_platform_read`. **T-JTS-02:** `GroupMembership` prueft jetzt BEIDE Seiten (Gruppe UND Benutzer gehoeren zum Mandanten) statt nur die Gruppenseite. **T-JTS-03:** `ModuleGrant` prueft zusaetzlich, dass die referenzierte Gruppe bzw. der referenzierte Benutzer zum selben Mandanten gehoert; `assertTargetBelongsToTenant` bleibt als zweite Verteidigungslinie bestehen. **WINDOWS #19:** `TenderRssFeedSource` bekommt VIER nach Befehl getrennte Regeln — Lesen schliesst plattformweite Zeilen ein, Einfuegen/Aendern/Loeschen verlangen weiter einen Mandanten (eine einzige lockere Regel haette jedem Mandanten erlaubt, gemeinsame Quellen zu aendern und zu loeschen, weil `USING` auch UPDATE und DELETE regelt). **Halbe Praemisse von #19 widerlegt:** bei `SearchProvider` gibt es gar keinen Codeweg, der eine mandantenlose Zeile erzeugt — Schreibweg verlangt den Mandanten, Vorgaben sind Konstanten (05-02); als widerlegte Annahme geschlossen, nicht als geloestes Problem, strenge Regel bleibt. **DREI Pruefungen schrieben die Loecher als erwartetes Verhalten fest** (meine eigene Suche fand nur zwei, der Planer die dritte) — alle drei UMGEDREHT statt geloescht, mit Verweis auf den urspruenglichen Befund: der ausfuehrbare Beleg, dass das Loch existierte, bleibt mit umgekehrtem Vorzeichen erhalten. **Die Reparatur erzeugte an einer Stelle selbst den Fehler, gegen den sie antritt:** `listForUser` haette nach der Regelaenderung die plattformweiten, aber nicht die persoenlichen Quellen geliefert — aus einer leeren Liste, die schreit, waere eine kurze geworden, die luegt; deshalb mitgebunden. **Messfalle abgefangen:** `extractPolicySql()` las nur die alten Migrationsverzeichnisse und haette nach der neuen Migration still die ABGELOESTE Regel weitergemessen. **Werkzeugfalle abgefangen:** der uebliche Aufrufweg haette beim Einspielen eine neue Prisma-Hauptversion nachgeladen; stattdessen die im Projekt festgelegte Fassung benutzt. Neuer offener Ledger-Eintrag #24: plattformweite Zeilen lassen sich unter der Anwendungsrolle weder anlegen noch entfernen — in alter wie neuer Regel. **Verifiziert 11/11 mit vier ZERSTOERENDEN Gegenproben** (jede Regel und die neue Bindung einzeln zurueckgedreht, jedes Mal schlug genau die zustaendige Pruefung fehl, danach byte-identisch wiederhergestellt). 839/839 Tests, Typpruefung sauber, 74/74 Live-Pruefungen; Regeltexte vom Orchestrator in der LAUFENDEN Datenbank gegengelesen | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) | | 260910-jab | **Die drei zu kurz greifenden Datenbankregeln geschlossen** — auf ausdrueckliche Anweisung des Users VORGEZOGEN, entgegen der geplanten Reihenfolge (urspruenglich nach Etappe 2, damit jeder Bereich gegen einen stabilen Regelstand misst; der User entschied anders, weil offene Loecher vergessen werden). Erster Durchlauf dieser Serie, der die DATENBANK aendert statt nur Anwendungscode — neue Migration `20260910120000_rls_widen_membership_grant_and_platform_read`. **T-JTS-02:** `GroupMembership` prueft jetzt BEIDE Seiten (Gruppe UND Benutzer gehoeren zum Mandanten) statt nur die Gruppenseite. **T-JTS-03:** `ModuleGrant` prueft zusaetzlich, dass die referenzierte Gruppe bzw. der referenzierte Benutzer zum selben Mandanten gehoert; `assertTargetBelongsToTenant` bleibt als zweite Verteidigungslinie bestehen. **WINDOWS #19:** `TenderRssFeedSource` bekommt VIER nach Befehl getrennte Regeln — Lesen schliesst plattformweite Zeilen ein, Einfuegen/Aendern/Loeschen verlangen weiter einen Mandanten (eine einzige lockere Regel haette jedem Mandanten erlaubt, gemeinsame Quellen zu aendern und zu loeschen, weil `USING` auch UPDATE und DELETE regelt). **Halbe Praemisse von #19 widerlegt:** bei `SearchProvider` gibt es gar keinen Codeweg, der eine mandantenlose Zeile erzeugt — Schreibweg verlangt den Mandanten, Vorgaben sind Konstanten (05-02); als widerlegte Annahme geschlossen, nicht als geloestes Problem, strenge Regel bleibt. **DREI Pruefungen schrieben die Loecher als erwartetes Verhalten fest** (meine eigene Suche fand nur zwei, der Planer die dritte) — alle drei UMGEDREHT statt geloescht, mit Verweis auf den urspruenglichen Befund: der ausfuehrbare Beleg, dass das Loch existierte, bleibt mit umgekehrtem Vorzeichen erhalten. **Die Reparatur erzeugte an einer Stelle selbst den Fehler, gegen den sie antritt:** `listForUser` haette nach der Regelaenderung die plattformweiten, aber nicht die persoenlichen Quellen geliefert — aus einer leeren Liste, die schreit, waere eine kurze geworden, die luegt; deshalb mitgebunden. **Messfalle abgefangen:** `extractPolicySql()` las nur die alten Migrationsverzeichnisse und haette nach der neuen Migration still die ABGELOESTE Regel weitergemessen. **Werkzeugfalle abgefangen:** der uebliche Aufrufweg haette beim Einspielen eine neue Prisma-Hauptversion nachgeladen; stattdessen die im Projekt festgelegte Fassung benutzt. Neuer offener Ledger-Eintrag #24: plattformweite Zeilen lassen sich unter der Anwendungsrolle weder anlegen noch entfernen — in alter wie neuer Regel. **Verifiziert 11/11 mit vier ZERSTOERENDEN Gegenproben** (jede Regel und die neue Bindung einzeln zurueckgedreht, jedes Mal schlug genau die zustaendige Pruefung fehl, danach byte-identisch wiederhergestellt). 839/839 Tests, Typpruefung sauber, 74/74 Live-Pruefungen; Regeltexte vom Orchestrator in der LAUFENDEN Datenbank gegengelesen | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) |
| 260910-krx | Mandantentrennung Etappe 2, Bereich dashboard — 12 von 13 Zugriffen gebunden, der Modulkatalog bleibt bewusst ungebunden (Messung und Bedingung getrennt: heute ohne Zeilenschutz, daher wirkungslos, katastrophal erst wenn Etappe 3 eine Regel setzt). **Erster Bereich, in dem die Besitzpruefungen von Anfang an richtig waren:** dieselbe Bauform, die in ldap und dkv je eine Luecke riss (nachschlagen, dann loeschen), vergleicht hier dazwischen gegen die angemeldete Person — nichts zu reparieren, nur zu bestaetigen und durch Tests festzunageln. **Beweisvernichtungs-Schleife belegt, nicht vermutet (WINDOWS #25, offen):** nach dem Scharfschalten liefert `getLayout` bei unsichtbarer Zeile die Vorgabe, die Oberflaeche uebernimmt sie ohne Fehlerzustand, und das Verlassen des Bearbeitungsmodus schreibt AUTOMATISCH zurueck — der Nutzer ueberschreibt seine urspruengliche Anordnung selbst, ohne es zu merken; dazu haeufen sich Widget-Dubletten, weil es keine Eindeutigkeit ueber (userId, widgetType) gibt. Gehoert in die Etappe-4-Vorabpruefung, nicht in diesen Umbau. Suchleiste: der Rueckfallzweig feuert nie leer, weil drei Vorgaben immer vorangestellt sind — die eigenen Suchmaschinen verschwinden schlicht. `DashboardLayout.userId` ist plattformweit eindeutig ohne Mandantenanteil (Familie WINDOWS #22). **Der Verifizierer fand eine Luecke der bekannten Art:** die Behauptung, ein gebundener Konfliktschreibvorgang werfe `PrismaClientUnknownRequestError` (nicht den P2002-Fall von tenders/user), stuetzte sich auf eine NICHT committete Ad-hoc-Messung — Pruefung 5 mass nur Roh-SQL, kein Test uebte den catch-Zweig. Nachgereicht (6e71206): Messung ueber den GENERIERTEN Client (Konstruktorname geprueft), dabei die Wegwerf-Tabelle korrigiert, der Roh-SQL nie aufgefallen war (createdAt/updatedAt fehlten, der echte Client scheiterte sofort mit P2022); zwei Tests fuer den catch-Zweig, durch Rueckbau falsifiziert. Klassifikation: fremde Datei `groups.service.ts` mit ungenauem Kopfkommentar bewusst NICHT angefasst, Ungenauigkeit in (w5) festgehalten. **Verifiziert 10/11, Luecke behoben** (860/860 Tests, Typpruefung sauber, 88/88 Live-Pruefungen; alle drei Falsifizierungsnachweise vom Pruefer eigenhaendig reproduziert) | 2026-09-11 | 6744918,e0ce594,67b5024,6e71206 | [260910-krx-mandantentrennung-etappe-2-bereich-dashb](./quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/) | | 260910-krx | Mandantentrennung Etappe 2, Bereich dashboard — 12 von 13 Zugriffen gebunden, der Modulkatalog bleibt bewusst ungebunden (Messung und Bedingung getrennt: heute ohne Zeilenschutz, daher wirkungslos, katastrophal erst wenn Etappe 3 eine Regel setzt). **Erster Bereich, in dem die Besitzpruefungen von Anfang an richtig waren:** dieselbe Bauform, die in ldap und dkv je eine Luecke riss (nachschlagen, dann loeschen), vergleicht hier dazwischen gegen die angemeldete Person — nichts zu reparieren, nur zu bestaetigen und durch Tests festzunageln. **Beweisvernichtungs-Schleife belegt, nicht vermutet (WINDOWS #25, offen):** nach dem Scharfschalten liefert `getLayout` bei unsichtbarer Zeile die Vorgabe, die Oberflaeche uebernimmt sie ohne Fehlerzustand, und das Verlassen des Bearbeitungsmodus schreibt AUTOMATISCH zurueck — der Nutzer ueberschreibt seine urspruengliche Anordnung selbst, ohne es zu merken; dazu haeufen sich Widget-Dubletten, weil es keine Eindeutigkeit ueber (userId, widgetType) gibt. Gehoert in die Etappe-4-Vorabpruefung, nicht in diesen Umbau. Suchleiste: der Rueckfallzweig feuert nie leer, weil drei Vorgaben immer vorangestellt sind — die eigenen Suchmaschinen verschwinden schlicht. `DashboardLayout.userId` ist plattformweit eindeutig ohne Mandantenanteil (Familie WINDOWS #22). **Der Verifizierer fand eine Luecke der bekannten Art:** die Behauptung, ein gebundener Konfliktschreibvorgang werfe `PrismaClientUnknownRequestError` (nicht den P2002-Fall von tenders/user), stuetzte sich auf eine NICHT committete Ad-hoc-Messung — Pruefung 5 mass nur Roh-SQL, kein Test uebte den catch-Zweig. Nachgereicht (6e71206): Messung ueber den GENERIERTEN Client (Konstruktorname geprueft), dabei die Wegwerf-Tabelle korrigiert, der Roh-SQL nie aufgefallen war (createdAt/updatedAt fehlten, der echte Client scheiterte sofort mit P2022); zwei Tests fuer den catch-Zweig, durch Rueckbau falsifiziert. Klassifikation: fremde Datei `groups.service.ts` mit ungenauem Kopfkommentar bewusst NICHT angefasst, Ungenauigkeit in (w5) festgehalten. **Verifiziert 10/11, Luecke behoben** (860/860 Tests, Typpruefung sauber, 88/88 Live-Pruefungen; alle drei Falsifizierungsnachweise vom Pruefer eigenhaendig reproduziert) | 2026-09-11 | 6744918,e0ce594,67b5024,6e71206 | [260910-krx-mandantentrennung-etappe-2-bereich-dashb](./quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/) |
| 260911-cwh | Mandantentrennung Etappe 2, Bereich calendar — alle 12 Zugriffe gebunden, ein Klient je Methode, sechs Methoden. Bereich hatte KEINE Testdatei (dkv-Form); `calendar.service.spec.ts` neu mit 23 Tests. **Gespeicherte Zugangsdaten zu fremden Kalender-Servern** — ein Fremdzugriff waere hier der Schluessel zu einem fremden Exchange/CalDAV. **Der dkv-Passwortverlust-Fall existiert hier NICHT**, in beiden Haelften belegt: der Dienst schreibt `encryptedPassword` nur bei `dto.password !== undefined`, und `calendar-source-form.tsx` laesst ein leeres Feld WEG statt einen leeren Text zu schicken; als drei Tests festgenagelt, weil ein nicht festgenagelter Freispruch still aufhoeren kann zu gelten. **Zwischenspeicher-Schluessel `userId:from:to` ohne Mandantenanteil ist sicher:** `User.id` ist `@default(uuid())`, Kette Schema -> `auth.service.ts sub: user.id` -> `JwtStrategy.validate` -> `extractContext` Glied fuer Glied belegt; die Etappe-3-Entscheidung (Anmeldenamen pro Mandant) beruehrt `username`/`email`, nicht `id`. **Eigener Gefahrenfall — halb gebundene Aggregationsschleife:** `fetchAndCacheEvents` liest Quellen und schreibt den Synchronstatus auf Erfolgs- UND Fehlerpfad zurueck, innerhalb von `Promise.allSettled`; gebundene Lesung mit ungebundenem Rueckschreiben haette Ereignisse still fallen lassen — beide Rueckschreibungen gebunden und als Tests festgenagelt, eines davon vom Pruefer eigenhaendig zurueckgebaut (genau 1 von 23 rot, exakte Meldung). **Frontend macht aus lauten Fehlern stille:** `calendar-widget.tsx` und `calendar-settings-panel.tsx` fangen jeden Fehler in denselben leeren Zustand — ein 403 sieht aus wie ein leerer Kalender; NICHT angefasst, als WINDOWS #26 offen festgehalten. Besitzpruefungen in allen drei Pfaden echt (403, nicht 404). Keine Eindeutigkeitskette, daher keine Konfliktuebersetzung, die nichts uebersetzt. **Lehre aus dashboard angewandt:** 4 der 13 neuen Pruefungen laufen ueber den GENERIERTEN Client, mit Laufzeitvergleich der Wegwerf-Tabelle gegen `schema.prisma` (17 = 17 Spalten). **Verifiziert 12/12** (883/883 Tests, 57 Dateien, Typpruefung sauber, 101/101 Live-Pruefungen; Klassenverteilung 31/17/13/2 = 63 und Ledger-Zaehler vom Pruefer nachgerechnet). Ein Selbstwiderspruch in der Zusammenfassung ('keine Abweichung' vs. 'kein TDD-Zyklus') berichtigt | 2026-09-11 | bf5fc4d,77cb124,e0e163e | [260911-cwh-mandantentrennung-etappe-2-bereich-calen](./quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/) |
| 260909-eor | Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. **Kernfund (#20):** `forTenant()` setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen | 2026-09-09 | da0ac04 | [260909-eor-anmeldeweg-mandantenfaehig-machen-und-al](./quick/260909-eor-anmeldeweg-mandantenfaehig-machen-und-al/) | | 260909-eor | Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. **Kernfund (#20):** `forTenant()` setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen | 2026-09-09 | da0ac04 | [260909-eor-anmeldeweg-mandantenfaehig-machen-und-al](./quick/260909-eor-anmeldeweg-mandantenfaehig-machen-und-al/) |
| 260910-jab | Die drei zu kurz greifenden Datenbankregeln geschlossen — T-JTS-02, T-JTS-03, WINDOWS #19 (bewusste Reihenfolge-Abweichung, vorgezogen auf Nutzerwunsch, statt wie geplant nach Etappe 2). Neue, handgeschriebene, lokal angewandte Migration `20260910120000_rls_widen_membership_grant_and_platform_read`: `GroupMembership` prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), `ModuleGrant` prueft zusaetzlich beide moeglichen Ziele mit Leer-Zulassung (D-04), `TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten — die Trennung ist noetig, weil ein einzelner USING-Ausdruck sonst auch UPDATE/DELETE mitregelt). `SearchProvider` bewusst NICHT angefasst: die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile). Drei loch-behauptende Pruefungen im Wegwerf-Werkzeug UMGEKEHRT statt geloescht (66→74 Pruefungen), mit Verweis auf die alten Pruefungsnamen und Befundkennungen im Meldetext. Genau EIN Anwendungspfad musste mitgebunden werden (`TenderRssFeedSourceService.listForUser`) — sonst haette die Reparatur ihn still von 'liefert nach dem Scharfschalten nichts' auf 'liefert nur die plattformweiten Zeilen, taeuscht Vollstaendigkeit vor' verschlechtert; Falsifizierungsnachweis gefuehrt (Bindung zurueckgenommen, genau ein Test rot, zurueckgesetzt). WINDOWS #19 geschlossen mit Beleg, WINDOWS #24 neu angelegt (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit #19). Aktenstand kohaerent: Klassifikation, Kritikschrift (neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit Signaltabelle beider Fehlerrichtungen je Regel), Betriebsanleitung, WINDOWS.md — fuenf ueberholte Bestandsstellen mit Nachtraegen versehen, alte Messprotokolle bleiben woertlich stehen. Selbst gemessen statt uebernommen: Baseline 833/56 Tests, 66/66 Live-Pruefungen; Endstand 839/56, 74/74; keine zweite Sitzungsvariable fuer den Benutzer gefunden (nur `app.current_tenant`). Rule-1-Fix: implizites `any` in `tenders.controller.ts` nach der Bindung behoben. `npx prisma` versuchte ungefragt Prisma 8 herunterzuladen — abgebrochen, lokale gepinnte 6.19.3 verwendet | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) | | 260910-jab | Die drei zu kurz greifenden Datenbankregeln geschlossen — T-JTS-02, T-JTS-03, WINDOWS #19 (bewusste Reihenfolge-Abweichung, vorgezogen auf Nutzerwunsch, statt wie geplant nach Etappe 2). Neue, handgeschriebene, lokal angewandte Migration `20260910120000_rls_widen_membership_grant_and_platform_read`: `GroupMembership` prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), `ModuleGrant` prueft zusaetzlich beide moeglichen Ziele mit Leer-Zulassung (D-04), `TenderRssFeedSource` bekommt vier nach Befehl getrennte Regeln (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten — die Trennung ist noetig, weil ein einzelner USING-Ausdruck sonst auch UPDATE/DELETE mitregelt). `SearchProvider` bewusst NICHT angefasst: die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile). Drei loch-behauptende Pruefungen im Wegwerf-Werkzeug UMGEKEHRT statt geloescht (66→74 Pruefungen), mit Verweis auf die alten Pruefungsnamen und Befundkennungen im Meldetext. Genau EIN Anwendungspfad musste mitgebunden werden (`TenderRssFeedSourceService.listForUser`) — sonst haette die Reparatur ihn still von 'liefert nach dem Scharfschalten nichts' auf 'liefert nur die plattformweiten Zeilen, taeuscht Vollstaendigkeit vor' verschlechtert; Falsifizierungsnachweis gefuehrt (Bindung zurueckgenommen, genau ein Test rot, zurueckgesetzt). WINDOWS #19 geschlossen mit Beleg, WINDOWS #24 neu angelegt (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit #19). Aktenstand kohaerent: Klassifikation, Kritikschrift (neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit Signaltabelle beider Fehlerrichtungen je Regel), Betriebsanleitung, WINDOWS.md — fuenf ueberholte Bestandsstellen mit Nachtraegen versehen, alte Messprotokolle bleiben woertlich stehen. Selbst gemessen statt uebernommen: Baseline 833/56 Tests, 66/66 Live-Pruefungen; Endstand 839/56, 74/74; keine zweite Sitzungsvariable fuer den Benutzer gefunden (nur `app.current_tenant`). Rule-1-Fix: implizites `any` in `tenders.controller.ts` nach der Bindung behoben. `npx prisma` versuchte ungefragt Prisma 8 herunterzuladen — abgebrochen, lokale gepinnte 6.19.3 verwendet | 2026-09-10 | f4f3115,6b23735,03fb3bf | [260910-jab-mandantentrennung-die-drei-zu-kurz-greif](./quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/) |
@@ -135,7 +135,7 @@ _Kein TDD-Modus fuer diesen Plan — die Testlage wurde in Aufgabe 2 als Vorauss
## Deviations from Plan ## Deviations from Plan
None - plan executed exactly as written. Alle Planungsbefunde (A-L) wurden zur Ausfuehrungszeit nachgeprueft und bestaetigt; keine Abweichung von den Planungsbefunden trat auf. Eine, dokumentarischer Art: Aufgabe 2 war im Plan als `tdd="true"` markiert, wurde aber nicht als RED/GREEN-Zyklus je Testfall entwickelt — die Testdatei entstand als Voraussetzung im Ganzen (siehe Hinweis oben unter der Testtabelle). Inhaltlich ohne Folge: die Falsifizierung durch Rueckbau ersetzt den RED-Nachweis. Dieser Abschnitt sagte zunaechst 'None' und widersprach damit dem eigenen Hinweis weiter oben; vom Verifizierer bemerkt, hier berichtigt. Alle Planungsbefunde (A-L) wurden zur Ausfuehrungszeit nachgeprueft und bestaetigt.
## Falsifizierungsnachweise (drei, alle durchgefuehrt und zurueckgenommen) ## Falsifizierungsnachweise (drei, alle durchgefuehrt und zurueckgenommen)
@@ -0,0 +1,307 @@
---
phase: quick-260911-cwh
verified: 2026-09-11T10:15:00Z
status: passed
score: 12/12 must-haves verified
covered_files:
- ".planning/WINDOWS.md"
- ".planning/quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/260911-cwh-PLAN.md"
- ".planning/quick/260911-cwh-mandantentrennung-etappe-2-bereich-calen/260911-cwh-SUMMARY.md"
- "apps/api/scripts/rls-scratch-check.mjs"
- "apps/api/src/calendar/calendar.controller.ts"
- "apps/api/src/calendar/calendar.service.spec.ts"
- "apps/api/src/calendar/calendar.service.ts"
- "docs/mandantentrennung-etappe2-fehlerrichtung.md"
- "docs/mandantentrennung-zugriffsklassifikation.md"
covered_digest: "v1:sha256:f092c2eea8be58064da21108d78ef37879ab4183bdc6c622c699cee603c0f434"
behavior_unverified: 0
overrides_applied: 0
---
# Quick Task 260911-cwh: Mandantentrennung Etappe 2, Bereich `calendar` — Verification Report
**Task Goal:** Bind all 12 `calendarSource` access sites in `calendar.service.ts`, create the area's missing spec coverage, pin the credential-path exoneration and the cache-key verdict as tests, and keep the classification document's five hand-maintained sections in sync.
**Verified:** 2026-09-11T10:15:00Z
**Status:** passed
**Commits reviewed:** bf5fc4d, 77cb124, e0e163e, 06038b9 (base 508d9e4), all present in `git log`, working tree clean before and after this review.
## Re-verification Checklist Results
### 1. Coverage — all 12 sites bound, one `tenantPrisma` per method, six methods
Independently counted (not taken from SUMMARY):
```
grep -ro "tenantPrisma\.calendarSource\." apps/api/src/calendar/calendar.service.ts | wc -l → 12
grep -ro "this\.prisma\.[a-zA-Z]*" apps/api/src/calendar/calendar.service.ts | wc -l → 0
grep -o "forTenant(this\.prisma" (non-comment source) → 6
```
Distribution matches the plan's Befund A exactly: `getSources` (1), `addSource` (1),
`updateSource` (2), `deleteSource` (2), `testConnection` (3),
`fetchAndCacheEvents` (3) = 12. `aggregateEvents`/`refreshCacheInBackground`
create no client of their own (confirmed by reading both bodies — they only
pass `tenantId` through to `fetchAndCacheEvents`).
**VERIFIED.**
### 2. Credential exoneration pinned, not asserted
Three test cases exist in `calendar.service.spec.ts` (lines 289, 303, 313):
"Feld FEHLT" (missing), "Feld LEER" (empty → `null`), "Feld GESETZT" (set →
re-encrypted). The "Feld FEHLT" case asserts `crypto.decrypt`/`crypto.encrypt`
were **not called** and that `update(...).data` does **not** have an
`encryptedPassword` key at all — this is a real pin, not a shape check. A
regression that reintroduced the `dkv` read-decrypt-re-encrypt form would
fail this test on both the decrypt-not-called assertion and the
data-shape assertion.
**VERIFIED.**
### 3. Cache-key verdict
Code comment directly above `eventCache = new Map(...)` in
`calendar.service.ts` (lines 125-142) states the full four-link chain
(`User.id @id @default(uuid())` → `auth.service.ts` `sub: user.id` →
`JwtStrategy.validate` `id: payload.sub` → `extractContext`) and concludes
the key stays without a tenant component because Etappe-3-Entscheidung (1)
only touches `username`/`email`, not `id`. Independently confirmed against
`apps/api/prisma/schema.prisma:30` (`id String @id @default(uuid())`),
`apps/api/src/auth/auth.service.ts:143,332` (`sub: user.id`), and
`apps/api/src/auth/strategies/jwt.strategy.ts:29` (`id: payload.sub`) — the
chain in the comment matches the actual source. The identical verdict is
also written in `docs/mandantentrennung-etappe2-fehlerrichtung.md` (k4)(a),
naming the same four links.
**VERIFIED.**
### 4. Both write-backs in `fetchAndCacheEvents` bound and pinned
Success path (line 429, inside the `try`) and catch path (line 440, inside
the `catch`) both call `tenantPrisma.calendarSource.update(...)`. Two named
tests cover this (`aggregateEvents, Erfolgspfad...` line 428,
`aggregateEvents, Fehlerpfad (Providerfehler)...` line 439), both asserting
`expectBoundCall(..., 'update')`.
**Falsification performed by this verifier** (not just re-reading the
SUMMARY's claim): reverted the success-path binding
(`tenantPrisma.calendarSource.update` → `this.prisma.calendarSource.update`
at line 429) and ran `npm --prefix apps/api run test -- src/calendar/calendar.service.spec.ts`.
Result: 1 of 23 tests failed —
`aggregateEvents, Erfolgspfad: ... → AssertionError: erwarteter gebundener Aufruf calendarSource.update(tenant=t1) fehlt im Protokoll: [{"tenantId":"t1","model":"calendarSource","method":"findMany"}]`
— exactly the failure mode described in the SUMMARY's own falsification
proof #1. Reverted the change; re-ran the same test file: 23/23 green.
Working tree confirmed clean afterward (`git status --short` empty).
**VERIFIED** (independently reproduced, not merely re-read).
### 5. Ownership checks survived
`updateSource` (line 221), `deleteSource` (line 273), `testConnection`
(line 289) all still compare `existing.userId !== userId` /
`source.userId !== userId` against the session-derived `userId` parameter
and throw `ForbiddenException`. Three ForbiddenException test cases and
three NotFoundException test cases exist and pass.
**VERIFIED.**
### 6. No conflict translation added
`grep` over `calendar.service.ts` shows no `P2002`/unique-constraint
handling anywhere; the only Prisma-error-sensitive code is the generic
`catch` blocks in `testConnection`/`fetchAndCacheEvents`, which existed
before this plan and are unrelated to uniqueness. Matches Befund H (no
uniqueness chain on `CalendarSource` besides the client-generated UUID PK).
**VERIFIED.**
### 7. Frontend NOT touched
`git diff --name-only 508d9e4 HEAD` and `git diff --name-only 50b3a36 HEAD`
both show zero files under `apps/web`. The silent-empty-state behaviour is
recorded, not fixed: `docs/mandantentrennung-etappe2-fehlerrichtung.md` (k3)
names `calendar-widget.tsx`/`calendar-settings-panel.tsx`/`calendar-source-form.tsx`
by file and line, and `.planning/WINDOWS.md` entry #26 (open, table row +
JSON block both present) describes the same silent-empty-state defect with
"das Frontend wird von 260911-cwh NICHT geaendert" stated explicitly.
**VERIFIED.**
### 8. Generated-client measurements — committed and honest
Re-ran `apps/api/scripts/rls-scratch-check.mjs` live against the running
`tessera-ctl-db-1` container (freshly resolved IP `172.19.0.2`, not reused
from any cached value):
```
DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}')
TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs
```
Exit code: 0. Final line: `Alle 101 Pruefungen bestanden.` — matches the
SUMMARY's claimed total (101). All 13 `calendarsource-*` named checks passed
(confirmed by name), including the 4 generated-client checks:
`calendarsource-generierter-client-gebundene-quellenliste-nur-eigener-mandant`,
`calendarsource-generierter-client-ungebundene-quellenliste-null-zeilen`,
`calendarsource-generierter-client-gebundenes-update-auf-unsichtbare-zeile-scheitert-laut`,
`calendarsource-generierter-client-gebundenes-anlegen-eigener-mandant-gelingt`.
The runtime column-set comparison (`calendarsource-wegwerftabelle-deckt-alle-spalten-des-generierten-clients`)
actually executed and printed both sets: schema.prisma yields 17 fields,
the scratch table's `information_schema.columns` yields the same 17 fields,
sorted identically — this is a real comparison, not a hardcoded pass.
Call-order gate confirmed: `runCalendarAreaChecks` is invoked after
`runDashboardAreaChecks` and before `runTransactionShapeMeasurement` in
`main()` (source inspection, line 3335).
**VERIFIED.**
### 9. Five hand-maintained sections — class distribution recomputed independently
Recomputed the class distribution directly from the Bestandsaufnahme rows
with an independent `awk` one-liner (not copy-pasted from the plan's gate):
```
muss-mandantengebunden: 31
keine-mandantengebundene-tabelle: 17
beides: 13
bewusst-uebergreifend: 2
TOTAL: 63
```
Matches the documented table exactly (31/17/13/2, Summe 63, heading "63
Paare"). Also recomputed the overview table's column sums across all 12
area rows (tenders 35/27, groups 0/31, ldap 4/26, dkv 1/22, user 8/14,
module-registry 7/10, dashboard 1/12, auth 8/5, calendar 0/12, tenant 8/0,
favorites 7/0, settings 4/0) → 83/159, matching the documented **Summe**
row. The `calendar` row itself reads `0 | 12` with the required
`**war 12/0**` marker and explicit no-remaining-unbound justification. The
"Stand 260911-cwh" unchanged-marker paragraph is present under
`## Klassen-Verteilung`. The background-service section header reads
"fünf Fälle" (matching its own bullet count) and contains a plain paragraph
naming `refreshCacheInBackground` and `calendar` without adding a sixth
bullet-point case (confirmed: no new `- **\`...\`**` entry and no "Der ...
Fall, anderer Bauart" line was added for this area). `## Was diese Etappe
NICHT entscheidet` mentions `calendar`. WINDOWS #26 present in table row,
JSON block, `open` status, and header counters (`total_count: 26` = 26
table rows, `open_count: 8` = 8 rows with `| open |`, both independently
recomputed and matching).
**VERIFIED (all five sections, independently recomputed, not re-read from
SUMMARY).**
### 10. Falsification proofs — three claimed
1. **Aggregation write-back binding revert** — independently reproduced by
this verifier (see item 4 above). Confirmed identical failure mode and
message pattern to the one quoted in the SUMMARY.
2. **Bestandsaufnahme `Stand` mismatch** — mechanism confirmed present:
`apps/api/src/prisma/rls-access-inventory.spec.ts:321` contains the exact
assertion template `Abweichender Stand (Dokument vs. Quelltext):` that
the SUMMARY's quoted failure message is built from. Not independently
re-triggered (would require editing and reverting the classification doc
under time budget), but the underlying gate genuinely exists and matches
the described mechanism precisely — not a fabricated quote.
3. **Uebersichtszeile wrong-number gate** — the awk gate quoted in the
SUMMARY (`grep -qE "^\| calendar \| ${DU} \| ${DB} \| \*\*war 12/0\*\*"`)
is present verbatim in the plan's Aufgabe 3 `<verify>` block, which
executed successfully as part of the task-3 commit's automated gate (the
task would not have committed otherwise, since these are `autonomous:
true` plans with gated commits).
**VERIFIED** (one directly reproduced by this verifier, two confirmed as
genuinely-wired mechanisms matching the quoted evidence, not fabricated).
### 11. Constraints held
- **Allow-list scope against `50b3a36`:** `git diff --name-only 50b3a36 HEAD`
shows exactly the 7 files in `files_modified` (`rls-scratch-check.mjs`,
`mandantentrennung-etappe2-fehlerrichtung.md`,
`calendar.service.spec.ts`, `calendar.service.ts`,
`calendar.controller.ts`, `mandantentrennung-zugriffsklassifikation.md`,
`.planning/WINDOWS.md`) plus `.planning/STATE.md` and the plan/summary
files themselves under `.planning/`. No `apps/api/prisma`, no
`apps/web`, no compose/env file.
- **Providers not exercised against real endpoints:** confirmed —
`icsProvider`/`caldavProvider`/`exchangeProvider` are `vi.fn()` mocks
throughout the spec file; no network call is made.
- **Switch OFF:** no compose/env file in the diff; nothing under
`apps/api/prisma` changed (`git diff --name-only 50b3a36 -- apps/api/prisma`
is empty).
**VERIFIED.**
## Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | All 12 `calendarSource` accesses bound via `tenantPrisma`, one client per method (6 methods) | ✓ VERIFIED | Independent grep count: 12 bound, 0 unbound, 6 `forTenant()` call sites |
| 2 | Ownership checks (`updateSource`/`deleteSource`/`testConnection`) read+write over the same bound client, pinned as tests | ✓ VERIFIED | Source read + 2 tests per path (ForbiddenException/NotFoundException) + `expectBoundCall` pairs |
| 3 | Reverse error direction measured against the real post-20260910120000 rule, ≥4 checks via generated client on a schema-matching scratch table | ✓ VERIFIED | Live tool run: 101/101, 13 named `calendarsource-*` checks, 4 via generated client, column-set comparison executed with real 17/17 match |
| 4 | Reverse error direction's silent-empty shape named, including frontend swallowing | ✓ VERIFIED | (k3) names both backend spots and 3 frontend files; WINDOWS #26 open entry |
| 5 | Credential-preservation question answered by measurement, pinned as 3 tests | ✓ VERIFIED | 3 tests at lines 289/303/313, one asserting decrypt/encrypt NOT called |
| 6 | Cache-key verdict, 4-link chain, written in code AND critique | ✓ VERIFIED | Comment above `eventCache`, (k4)(a) in critique doc, chain matches schema/auth source |
| 7 | Ownership checks read (not assumed), kept, pinned | ✓ VERIFIED | Same as #2 |
| 8 | Test suite created from nothing, two-client proof, providers not exercised | ✓ VERIFIED | 23 test cases, `__makeBoundClient`, providers are `vi.fn()` |
| 9 | Controller passes resolved tenant through to all 5 (of 6) previously-discarding handlers | ✓ VERIFIED | 6/6 handlers destructure `{ userId, tenantId }`, 0 handlers take `userId` alone |
| 10 | Five hand-maintained classification sections in sync, allow-list gated | ✓ VERIFIED | Independently recomputed sums/class distribution match exactly |
| 11 | Baseline held at end of every task | ✓ VERIFIED (via orchestrator measurement) | 883/883 tests, 57 files, type-check clean — independently measured by orchestrator per task instructions |
**Score:** 12/12 truths verified (0 present-but-behavior-unverified).
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/api/scripts/rls-scratch-check.mjs` | 11th section `runCalendarAreaChecks`, ≥12 named checks, ≥4 via generated client | ✓ VERIFIED | 13 named checks in normal path, 4 generated-client; live-run confirmed |
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | `## Bereich calendar` with (k1)-(k5) | ✓ VERIFIED | All 5 subsections present, content spot-checked |
| `apps/api/src/calendar/calendar.service.spec.ts` | New, two-client proof, mocks for crypto+3 providers | ✓ VERIFIED | 23 tests, `vi.mock` on `prisma-tenant.extension`, `makeFakeCrypto`, `makeFakeProviders` |
| `apps/api/src/calendar/calendar.service.ts` | All 12 accesses bound, cache-key verdict as comment | ✓ VERIFIED | 12/12 bound, comment present and accurate |
| `apps/api/src/calendar/calendar.controller.ts` | 5 discarding handlers pass tenant through | ✓ VERIFIED | 6/6 handlers pass `{ userId, tenantId }` |
| `docs/mandantentrennung-zugriffsklassifikation.md` | Bestandsaufnahme, overview, sum, class distribution, background-service section, "was NICHT entscheidet" | ✓ VERIFIED | All independently recomputed and matched |
| `.planning/WINDOWS.md` | New open entry via `gsd-tools windows append` | ✓ VERIFIED | Entry #26, table+JSON+counters consistent |
### Key Link Verification
| From | To | Via | Status | Details |
|------|-----|-----|--------|---------|
| `calendar.controller.ts extractContext` | `CalendarService` methods | Destructured `{ userId, tenantId }` passed as args | ✓ WIRED | All 6 handlers |
| `fetchAndCacheEvents` success write-back | `tenantPrisma.calendarSource.update` | Same client as the `findMany` load | ✓ WIRED | Falsified and restored by this verifier |
| `fetchAndCacheEvents` catch write-back | `tenantPrisma.calendarSource.update` | Same client as the `findMany` load | ✓ WIRED | Test exists, source confirmed |
| `eventCache` key | `User.id` via `extractContext`→`JwtStrategy`→`auth.service.ts`→`schema.prisma` | Comment chain | ✓ WIRED | All 4 links independently checked against source |
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| Reverting one write-back binding breaks exactly the named test | Edited line 429, ran `vitest run src/calendar/calendar.service.spec.ts`, restored | 22 passed, 1 failed with quoted message; then 23/23 after restore | ✓ PASS |
| `rls-scratch-check.mjs` passes live against running DB | `TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs` | exit 0, "Alle 101 Pruefungen bestanden." | ✓ PASS |
### Anti-Patterns Found
None. Grepped modified files for `TBD|FIXME|XXX|TODO|HACK|PLACEHOLDER` and empty-return stubs — no matches beyond pre-existing, unrelated code.
### Human Verification Required
None. All must-haves resolved to VERIFIED via direct source inspection, independent recomputation, and live tool execution (including one directly reproduced falsification).
### Gaps Summary
None found. Working tree is clean; all commits (bf5fc4d, 77cb124, e0e163e,
06038b9) are present in `git log` on `main`, in the correct order, on top of
base `508d9e4`. Scope is allow-listed against `50b3a36` with zero
unexpected files. The one minor documentation wrinkle — the plan's Aufgabe 2
carries `tdd="true"` while the SUMMARY states "Kein TDD-Modus fuer diesen
Plan" under "Deviations from Plan: None" — is a self-contradiction inside
the SUMMARY's own prose (claims "executed exactly as written" while also
describing a TDD-flag deviation), but it has no bearing on the delivered
artifacts: the test file exists, is substantive, and all pins are real and
falsifiable as demonstrated above. Noted here for completeness, not raised
as a gap since it does not affect goal achievement.
---
*Verified: 2026-09-11T10:15:00Z*
*Verifier: Claude (gsd-verifier)*