docs(quick-260910-exd): Etappe 2 Bereich module-registry abgeschlossen und verifiziert
This commit is contained in:
+3
-2
@@ -378,6 +378,7 @@ None yet.
|
|||||||
| 260909-laa | Mandantentrennung Etappe 2, Bereich tenders — anders geschnitten als die bisherigen: von 23 Paaren werden nur 5 umgestellt (die Nutzer-CRUD-Dienste fuer gespeicherte Suchen, Bearbeitungsstand, Benachrichtigungen, Postfach, RSS), 10 bleiben bewusst ungebunden, weil der Ausschreibungskatalog plattformweit ist (D-03), 2 sind Verteiler, die absichtlich ueber alle Mandanten lesen, und 6 sind Mischfaelle, deren uebergreifende Haelfte zu Etappe 3 gehoert — nur die Je-Treffer-Schleifen wurden gebunden. **Die gefaehrlichste Grenze lag in `tender-rss-feed.service.ts`:** dort haben plattformweite RSS-Quellen ein leeres Mandantenfeld; drei der vier Zugriffe duerfen deshalb NICHT binden, sonst waeren diese Quellen nach dem Scharfschalten fuer JEDEN unsichtbar statt nur fuer fremde (WINDOWS #19). Die Datei endet bewusst auf Stand `gemischt`, und das einzelne bedingte Loeschen blieb eine Anweisung — es aufzuteilen haette das Pruef-/Nutzungsfenster geoeffnet, das der Dateikopf vermeidet. **Neue Gegenrichtung, die die Bindung selbst erzeugt:** drei `upsert`-Pfade laufen auf Eindeutigkeitsschluesseln ohne Mandantendimension; ist die Zeile unter dem gebundenen Kontext unsichtbar, wird aus stillem Ueberschreiben ein harter Fehler — jetzt als deutsche Meldung statt als 500. **Der Verifizierer fand, dass genau eine der drei fehlte** (`setTriage`), obwohl die Zusammenfassung alle drei behauptete; nachgereicht mit 8cbf4c1 samt zwei Tests, deren Rotwerden durch Rueckbau belegt ist. **Befund E, festgehalten statt repariert:** alle fuenf Policies dieses Bereichs lesen nur `tenantId = current_tenant_id()` und haben KEINE Benutzerdimension — zwei Nutzer desselben Mandanten sind auf Datenbankebene fuereinander vollstaendig sichtbar; die Trennung haengt allein am Anwendungscode, der stichprobenartig als korrekt belegt wurde. Produktfrage vor dem zweiten Kunden. **Befund K, neue Reihenfolgebedingung fuer Etappe 4:** der Mailversand holt SMTP aus dem noch nicht umgestellten Bereich `settings` — nach dem Scharfschalten ginge fuer NIEMANDEN mehr eine Mail raus; `settings` muss vor Etappe 4 durch sein. Vierte Zaehlkorrektur des Vorhabens: 62 Rohtreffer sind 61 Modellzugriffe, und von zehn vermeintlichen Controller-Stellen brauchten acht die Durchreichung. **Verifiziert 8/9, Luecke behoben** (772/772 Tests, Typpruefung sauber, 32/32 Live-Pruefungen) | 2026-09-09 | 3498147,3336a6e,df5c5b7,8cbf4c1 | [260909-laa-mandantentrennung-etappe-2-bereich-tende](./quick/260909-laa-mandantentrennung-etappe-2-bereich-tende/) |
|
| 260909-laa | Mandantentrennung Etappe 2, Bereich tenders — anders geschnitten als die bisherigen: von 23 Paaren werden nur 5 umgestellt (die Nutzer-CRUD-Dienste fuer gespeicherte Suchen, Bearbeitungsstand, Benachrichtigungen, Postfach, RSS), 10 bleiben bewusst ungebunden, weil der Ausschreibungskatalog plattformweit ist (D-03), 2 sind Verteiler, die absichtlich ueber alle Mandanten lesen, und 6 sind Mischfaelle, deren uebergreifende Haelfte zu Etappe 3 gehoert — nur die Je-Treffer-Schleifen wurden gebunden. **Die gefaehrlichste Grenze lag in `tender-rss-feed.service.ts`:** dort haben plattformweite RSS-Quellen ein leeres Mandantenfeld; drei der vier Zugriffe duerfen deshalb NICHT binden, sonst waeren diese Quellen nach dem Scharfschalten fuer JEDEN unsichtbar statt nur fuer fremde (WINDOWS #19). Die Datei endet bewusst auf Stand `gemischt`, und das einzelne bedingte Loeschen blieb eine Anweisung — es aufzuteilen haette das Pruef-/Nutzungsfenster geoeffnet, das der Dateikopf vermeidet. **Neue Gegenrichtung, die die Bindung selbst erzeugt:** drei `upsert`-Pfade laufen auf Eindeutigkeitsschluesseln ohne Mandantendimension; ist die Zeile unter dem gebundenen Kontext unsichtbar, wird aus stillem Ueberschreiben ein harter Fehler — jetzt als deutsche Meldung statt als 500. **Der Verifizierer fand, dass genau eine der drei fehlte** (`setTriage`), obwohl die Zusammenfassung alle drei behauptete; nachgereicht mit 8cbf4c1 samt zwei Tests, deren Rotwerden durch Rueckbau belegt ist. **Befund E, festgehalten statt repariert:** alle fuenf Policies dieses Bereichs lesen nur `tenantId = current_tenant_id()` und haben KEINE Benutzerdimension — zwei Nutzer desselben Mandanten sind auf Datenbankebene fuereinander vollstaendig sichtbar; die Trennung haengt allein am Anwendungscode, der stichprobenartig als korrekt belegt wurde. Produktfrage vor dem zweiten Kunden. **Befund K, neue Reihenfolgebedingung fuer Etappe 4:** der Mailversand holt SMTP aus dem noch nicht umgestellten Bereich `settings` — nach dem Scharfschalten ginge fuer NIEMANDEN mehr eine Mail raus; `settings` muss vor Etappe 4 durch sein. Vierte Zaehlkorrektur des Vorhabens: 62 Rohtreffer sind 61 Modellzugriffe, und von zehn vermeintlichen Controller-Stellen brauchten acht die Durchreichung. **Verifiziert 8/9, Luecke behoben** (772/772 Tests, Typpruefung sauber, 32/32 Live-Pruefungen) | 2026-09-09 | 3498147,3336a6e,df5c5b7,8cbf4c1 | [260909-laa-mandantentrennung-etappe-2-bereich-tende](./quick/260909-laa-mandantentrennung-etappe-2-bereich-tende/) |
|
||||||
| 260909-mir | Mandantentrennung Etappe 2, Bereich dkv — Tankkarten-Modul. Alle 21 klassifizierten Zugriffe in `dkv.service.ts` gebunden (am Ende 22, weil der neue Besitzriegel einen Lesezugriff hinzufuegt); genau einer bleibt bewusst ungebunden. **Bereits bestehende Fremdzugriffsluecke geschlossen (T-MIR-03):** `getExportFile(tenantId, filename)` nahm die Mandantenkennung entgegen und benutzte sie nie — die Datei kam allein ueber ihren Namen aus dem gemeinsamen `user-files/`-Verzeichnis, ein Administrator eines beliebigen Mandanten konnte die Tankkarten-Auswertung eines anderen herunterladen. Der Riegel leitet die Zugehoerigkeit jetzt aus `DkvInvoiceHistory.exportFilename` ab; in der Oberflaeche gegengeprueft, dass jeder angebotene Dateiname aus einer Historienzeile stammt, die regulaere Nutzung aendert sich also nicht. Die Luecke ist keine Folge des Umbaus, sie bestand seit jeher. **Zerstoerender Fehler in umgekehrter Richtung behoben:** `saveConfig` verschluckte im Zweig, der ein gespeichertes Passwort erhalten soll, Lese- und Entschluesselungsfehler und machte mit leeren Werten weiter — nach dem Scharfschalten haette er ein vorhandenes Passwort durch ein leeres ersetzt und verschluesselt abgelegt, ohne Meldung, nicht rekonstruierbar. **Dritte Variante der Testluecke:** der Bereich hatte gar keine Testdatei (nicht wie ldap eine, die nichts prueft, nicht wie groups/tenders eine, die abstuerzen wuerde); `dkv.service.spec.ts` neu angelegt. **WINDOWS #21, bewusste Entscheidung:** der Planer-Startpfad bleibt ungebunden und wird als benannte Altlast weitergefuehrt — binden ist unmoeglich (`onModuleInit` hat strukturell keinen Mandanten), Umbau auf einmal-abfragen-viele-bedienen waere die in 07-04 zurueckgestellte Mehrmandanten-Planung. Unsymmetrie zum ldap-Praezedenzfall ausgeschrieben: jener ist heute korrekt und verstummt spaeter, dieser ist HEUTE bereits falsch (bedient einen willkuerlichen Mandanten, bei inaktiver Zeile niemanden) UND verstummt zusaetzlich. Dreifach markiert. Erster Bereich, dessen Kopfzahl beim Hineinsehen NICHT kleiner wurde. **Ausfuehrung brach am 2026-09-09 gegen Ende von Aufgabe 3 an einem Sitzungslimit ab** — zwei Aufgaben committet, die dritte vollstaendig im Arbeitsbaum; am 2026-09-10 nachgetragen. Aufgefallen durch `git status`, nicht durch den Bericht. **Verifiziert 9/9, zwei Dokumentationsluecken danach behoben** (Hintergrunddienst-Abschnitt um den vierten Fall erweitert, Falsifizierungsnachweise nachgetragen). 789/789 Tests, Typpruefung sauber, 41/41 Live-Pruefungen | 2026-09-10 | 761e5e2,222f453,5e8237d | [260909-mir-mandantentrennung-etappe-2-bereich-dkv-a](./quick/260909-mir-mandantentrennung-etappe-2-bereich-dkv-a/) |
|
| 260909-mir | Mandantentrennung Etappe 2, Bereich dkv — Tankkarten-Modul. Alle 21 klassifizierten Zugriffe in `dkv.service.ts` gebunden (am Ende 22, weil der neue Besitzriegel einen Lesezugriff hinzufuegt); genau einer bleibt bewusst ungebunden. **Bereits bestehende Fremdzugriffsluecke geschlossen (T-MIR-03):** `getExportFile(tenantId, filename)` nahm die Mandantenkennung entgegen und benutzte sie nie — die Datei kam allein ueber ihren Namen aus dem gemeinsamen `user-files/`-Verzeichnis, ein Administrator eines beliebigen Mandanten konnte die Tankkarten-Auswertung eines anderen herunterladen. Der Riegel leitet die Zugehoerigkeit jetzt aus `DkvInvoiceHistory.exportFilename` ab; in der Oberflaeche gegengeprueft, dass jeder angebotene Dateiname aus einer Historienzeile stammt, die regulaere Nutzung aendert sich also nicht. Die Luecke ist keine Folge des Umbaus, sie bestand seit jeher. **Zerstoerender Fehler in umgekehrter Richtung behoben:** `saveConfig` verschluckte im Zweig, der ein gespeichertes Passwort erhalten soll, Lese- und Entschluesselungsfehler und machte mit leeren Werten weiter — nach dem Scharfschalten haette er ein vorhandenes Passwort durch ein leeres ersetzt und verschluesselt abgelegt, ohne Meldung, nicht rekonstruierbar. **Dritte Variante der Testluecke:** der Bereich hatte gar keine Testdatei (nicht wie ldap eine, die nichts prueft, nicht wie groups/tenders eine, die abstuerzen wuerde); `dkv.service.spec.ts` neu angelegt. **WINDOWS #21, bewusste Entscheidung:** der Planer-Startpfad bleibt ungebunden und wird als benannte Altlast weitergefuehrt — binden ist unmoeglich (`onModuleInit` hat strukturell keinen Mandanten), Umbau auf einmal-abfragen-viele-bedienen waere die in 07-04 zurueckgestellte Mehrmandanten-Planung. Unsymmetrie zum ldap-Praezedenzfall ausgeschrieben: jener ist heute korrekt und verstummt spaeter, dieser ist HEUTE bereits falsch (bedient einen willkuerlichen Mandanten, bei inaktiver Zeile niemanden) UND verstummt zusaetzlich. Dreifach markiert. Erster Bereich, dessen Kopfzahl beim Hineinsehen NICHT kleiner wurde. **Ausfuehrung brach am 2026-09-09 gegen Ende von Aufgabe 3 an einem Sitzungslimit ab** — zwei Aufgaben committet, die dritte vollstaendig im Arbeitsbaum; am 2026-09-10 nachgetragen. Aufgefallen durch `git status`, nicht durch den Bericht. **Verifiziert 9/9, zwei Dokumentationsluecken danach behoben** (Hintergrunddienst-Abschnitt um den vierten Fall erweitert, Falsifizierungsnachweise nachgetragen). 789/789 Tests, Typpruefung sauber, 41/41 Live-Pruefungen | 2026-09-10 | 761e5e2,222f453,5e8237d | [260909-mir-mandantentrennung-etappe-2-bereich-dkv-a](./quick/260909-mir-mandantentrennung-etappe-2-bereich-dkv-a/) |
|
||||||
| 260910-das | Mandantentrennung Etappe 2, Bereich user — Benutzerverwaltung, die schwerste Fehlerklasse des Vorhabens (Fremdzugriff hier ist Rechteausweitung ueber Mandantengrenzen, nicht blosse Sichtbarkeit). Alle 17 Zugriffe eingeordnet: Verwaltungswege gebunden, Eindeutigkeits- und Suchwege bewusst ungebunden, jede Entscheidung mit Begruendung am Ort. **Startsperre entschaerft — der schwerwiegendste Fund:** nach dem Scharfschalten haette eine FRISCHE Installation ihren ersten Administrator nicht anlegen koennen und die Anwendung waere gar nicht erst gestartet. Kette (Glied fuer Glied belegt): die Startpruefung liefert `null` — nicht weil der Admin fehlt, sondern weil ohne Mandantenkontext keine Zeile sichtbar ist — also wird angelegt, das laeuft in den plattformweit eindeutigen Anmeldenamen, und weil `seedAdmin()` ungekapselt in `onApplicationBootstrap` haengt, bricht der Start ab. Betroffen waere jede Installation mit gesetzten Admin-Umgebungswerten gewesen; auf dem bestehenden System nie aufgefallen, weil dort der Admin laengst existiert. Jetzt bindet die Erstanlage an den eine Anweisung zuvor angelegten Mandanten und faengt GENAU den Doppelanlage-Fall ab — jeder andere Fehler bricht den Start weiterhin ab (beide Haelften einzeln nachgewiesen). **Riegel repariert, der seit seiner Entstehung wirkungslos war:** die Sperre gegen das Loeschen des eigenen Kontos verglich gegen ein Feld, das der Sitzungsnachweis gar nicht traegt (`sub`; er traegt `id`, `username`, `role`, `tenantId`) — ein Administrator konnte sein eigenes Konto loeschen. Kein Mandantenproblem, gefunden weil dieser Durchlauf jede Zeile aufschlaegt. **Zwei Falschaussagen in eigenen Artefakten berichtigt:** der Kopfkommentar von `findByUsername` behauptete, sie muesse fuer den mandantenuebergreifenden Anmeldeweg ungebunden bleiben — der laeuft seit Etappe 1 ueber die SECURITY-DEFINER-Funktionen, und die Methode hat gemessen NULL Aufrufer; und die Klassifikationszeile der Erstanlage behauptete, es gebe strukturell keinen Mandanten zum Binden, obwohl er eine Anweisung vorher entsteht (Klasse auf `beides` korrigiert). **SUPER_ADMIN-Sicht** war nach dem Scharfschalten in JEDER heutigen Form kaputt (ungebunden null Zeilen, gebunden stille Funktionsminderung) — jetzt Schleife ueber alle Mandanten mit gebundenem Rumpf, neues Fundstellenpaar `(user.service.ts, tenant)`. **Steuerungsschicht hatte gar keine Tests** (7 der 17 Zugriffe plus die gesamte Rollenlogik) — `user.controller.spec.ts` neu. Plan-Pruefer fand einen Blocker: vier handgepflegte Dokumentstellen benannt, nur zwei abgesichert — also derselbe Fehler, den der Plan verhindern sollte; nachgebessert mit herleitenden statt fest verdrahteten Pruefungen, in vier Einzelmutationen falsifiziert. **Verifiziert 10/10** (810/810 Tests, Typpruefung sauber, 53/53 Live-Pruefungen; Selbstloesch-Riegel und Klassenverteilung vom Pruefer eigenhaendig nachgerechnet) | 2026-09-10 | b848ba6,888f660,3a9391d | [260910-das-mandantentrennung-etappe-2-bereich-user-](./quick/260910-das-mandantentrennung-etappe-2-bereich-user-/) |
|
| 260910-das | Mandantentrennung Etappe 2, Bereich user — Benutzerverwaltung, die schwerste Fehlerklasse des Vorhabens (Fremdzugriff hier ist Rechteausweitung ueber Mandantengrenzen, nicht blosse Sichtbarkeit). Alle 17 Zugriffe eingeordnet: Verwaltungswege gebunden, Eindeutigkeits- und Suchwege bewusst ungebunden, jede Entscheidung mit Begruendung am Ort. **Startsperre entschaerft — der schwerwiegendste Fund:** nach dem Scharfschalten haette eine FRISCHE Installation ihren ersten Administrator nicht anlegen koennen und die Anwendung waere gar nicht erst gestartet. Kette (Glied fuer Glied belegt): die Startpruefung liefert `null` — nicht weil der Admin fehlt, sondern weil ohne Mandantenkontext keine Zeile sichtbar ist — also wird angelegt, das laeuft in den plattformweit eindeutigen Anmeldenamen, und weil `seedAdmin()` ungekapselt in `onApplicationBootstrap` haengt, bricht der Start ab. Betroffen waere jede Installation mit gesetzten Admin-Umgebungswerten gewesen; auf dem bestehenden System nie aufgefallen, weil dort der Admin laengst existiert. Jetzt bindet die Erstanlage an den eine Anweisung zuvor angelegten Mandanten und faengt GENAU den Doppelanlage-Fall ab — jeder andere Fehler bricht den Start weiterhin ab (beide Haelften einzeln nachgewiesen). **Riegel repariert, der seit seiner Entstehung wirkungslos war:** die Sperre gegen das Loeschen des eigenen Kontos verglich gegen ein Feld, das der Sitzungsnachweis gar nicht traegt (`sub`; er traegt `id`, `username`, `role`, `tenantId`) — ein Administrator konnte sein eigenes Konto loeschen. Kein Mandantenproblem, gefunden weil dieser Durchlauf jede Zeile aufschlaegt. **Zwei Falschaussagen in eigenen Artefakten berichtigt:** der Kopfkommentar von `findByUsername` behauptete, sie muesse fuer den mandantenuebergreifenden Anmeldeweg ungebunden bleiben — der laeuft seit Etappe 1 ueber die SECURITY-DEFINER-Funktionen, und die Methode hat gemessen NULL Aufrufer; und die Klassifikationszeile der Erstanlage behauptete, es gebe strukturell keinen Mandanten zum Binden, obwohl er eine Anweisung vorher entsteht (Klasse auf `beides` korrigiert). **SUPER_ADMIN-Sicht** war nach dem Scharfschalten in JEDER heutigen Form kaputt (ungebunden null Zeilen, gebunden stille Funktionsminderung) — jetzt Schleife ueber alle Mandanten mit gebundenem Rumpf, neues Fundstellenpaar `(user.service.ts, tenant)`. **Steuerungsschicht hatte gar keine Tests** (7 der 17 Zugriffe plus die gesamte Rollenlogik) — `user.controller.spec.ts` neu. Plan-Pruefer fand einen Blocker: vier handgepflegte Dokumentstellen benannt, nur zwei abgesichert — also derselbe Fehler, den der Plan verhindern sollte; nachgebessert mit herleitenden statt fest verdrahteten Pruefungen, in vier Einzelmutationen falsifiziert. **Verifiziert 10/10** (810/810 Tests, Typpruefung sauber, 53/53 Live-Pruefungen; Selbstloesch-Riegel und Klassenverteilung vom Pruefer eigenhaendig nachgerechnet) | 2026-09-10 | b848ba6,888f660,3a9391d | [260910-das-mandantentrennung-etappe-2-bereich-user-](./quick/260910-das-mandantentrennung-etappe-2-bereich-user-/) |
|
||||||
|
| 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/) |
|
||||||
| 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/) |
|
||||||
|
|
||||||
## Deferred Items
|
## Deferred Items
|
||||||
@@ -420,6 +421,6 @@ sind. Kein Anlass, sie vorher erneut vorzulegen.
|
|||||||
|
|
||||||
Last session: 2026-09-09T12:13:05.681Z
|
Last session: 2026-09-09T12:13:05.681Z
|
||||||
Resumed: 2026-09-09 — Sitzung ueber /gsd-resume-work fortgesetzt; Einstiegspunkt Etappe 2 (Bereich ldap), Auswahl des Nutzers steht aus.
|
Resumed: 2026-09-09 — Sitzung ueber /gsd-resume-work fortgesetzt; Einstiegspunkt Etappe 2 (Bereich ldap), Auswahl des Nutzers steht aus.
|
||||||
Stopped at: Etappe 2, fuenf von ZWOELF Bereichen durch (ldap 260909-ipc, groups 260909-jts, tenders 260909-laa, dkv 260909-mir, user 260910-das). KORREKTUR der Bereichsliste: es sind zwoelf, nicht elf — `auth` fehlte in der Uebergabe und hat noch 5 ungebundene Zugriffe (getMe/changePassword/adminResetPassword, in Etappe 1 bewusst ausgelassen). Gegengeprueft: ausserhalb der zwoelf beruehrt KEIN Dienst die Datenbank. Naechster Bereich: module-registry (17), danach dashboard (13), calendar (12), tenant (8), favorites (7), auth (5), settings (4). ACHTUNG settings: Befund K macht ihn zur Reihenfolgebedingung fuer Etappe 4. ACHTUNG settings: Befund K aus 260909-laa macht ihn zur Reihenfolgebedingung fuer Etappe 4 — ohne ihn kein Mailversand nach dem Scharfschalten. Der User hat am 2026-09-09 gesagt, die restlichen Bereiche sollen ohne Rueckfrage durchlaufen; beim Scharfschalten (Etappe 4) wird ausdruecklich angehalten.
|
Stopped at: Etappe 2, sieben von ZWOELF Bereichen durch (ldap 260909-ipc, groups 260909-jts, tenders 260909-laa, dkv 260909-mir, user 260910-das). KORREKTUR der Bereichsliste: es sind zwoelf, nicht elf — `auth` fehlte in der Uebergabe und hat noch 5 ungebundene Zugriffe (getMe/changePassword/adminResetPassword, in Etappe 1 bewusst ausgelassen). Gegengeprueft: ausserhalb der zwoelf beruehrt KEIN Dienst die Datenbank. SIEBEN von zwoelf durch (zuletzt module-registry 260910-exd). Naechster Bereich: dashboard (13), danach calendar (12), tenant (8), favorites (7), auth (5), settings (4). ACHTUNG settings: Befund K macht ihn zur Reihenfolgebedingung fuer Etappe 4. ACHTUNG settings: Befund K aus 260909-laa macht ihn zur Reihenfolgebedingung fuer Etappe 4 — ohne ihn kein Mailversand nach dem Scharfschalten. Der User hat am 2026-09-09 gesagt, die restlichen Bereiche sollen ohne Rueckfrage durchlaufen; beim Scharfschalten (Etappe 4) wird ausdruecklich angehalten.
|
||||||
Resume file: None
|
Resume file: None
|
||||||
Last activity: 2026-09-10 - Completed quick task 260910-das: Mandantentrennung Etappe 2, Bereich user (verifiziert 10/10)
|
Last activity: 2026-09-10 - Completed quick task 260910-exd: Mandantentrennung Etappe 2, Bereich module-registry (verifiziert 9/9)
|
||||||
|
|||||||
+180
@@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
phase: quick-260910-exd
|
||||||
|
plan: 01
|
||||||
|
subsystem: database
|
||||||
|
tags: [prisma, postgresql, row-level-security, multi-tenancy, nestjs, vitest]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: quick-260909-jts
|
||||||
|
provides: forTenant()/withTenantTransaction(), the Group/GroupMembership/ModuleGrant/TenantModuleActivation RLS policies, and module-grants.service.ts as the reference pattern for two-client bound tests
|
||||||
|
provides:
|
||||||
|
- ModuleAccessService (getAccessibleModuleIds, findAccessibleModules, getCatalogFlags) bound to forTenant() on every mandate-scoped access
|
||||||
|
- ModuleRegistryService (findActiveForTenant, activateForTenant, deactivateForTenant, isModuleActive) bound to forTenant()
|
||||||
|
- A new test file for module-registry.service.ts (previously had none) covering 11 of the area's 17 raw accesses, including every write path
|
||||||
|
- Both falsely-worded header comments from Befund G corrected
|
||||||
|
- docs/mandantentrennung-zugriffsklassifikation.md fully reconciled (5 hand-maintained sections) at 108 ungebunden / 134 gebunden
|
||||||
|
- WINDOWS #23: the recorded absence of a signal distinguishing "genuinely no grant" from "query found nothing" in the module-access hot path
|
||||||
|
affects: [module-registry, dashboard (picks up the binding transitively via getAccessibleModuleIds), etappe-3-rls-policy-tightening, etappe-4-cutover]
|
||||||
|
|
||||||
|
actuals:
|
||||||
|
tokens: 25541
|
||||||
|
tasks: 3
|
||||||
|
commits: 3
|
||||||
|
plan_head_before: a2516a9
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns:
|
||||||
|
- "EIN gebundener Klient je Methode unter dem Namen tenantPrisma, existing where-filters kept as a second net (T-JTS-02/T-JTS-03 precedent from module-grants.service.ts)"
|
||||||
|
- "Nested method calls (getCatalogFlags calling getAccessibleModuleIds) each create their own forTenant() client — bound clients are never passed between methods"
|
||||||
|
- "Catalog access (Module model) stays deliberately unbound with a comment separating today's measurement (no RLS on the table) from the future condition (Etappe 3 adding a policy would make binding catastrophic)"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- apps/api/src/module-registry/module-registry.service.spec.ts
|
||||||
|
modified:
|
||||||
|
- apps/api/src/module-registry/module-access.service.ts
|
||||||
|
- apps/api/src/module-registry/module-access.service.spec.ts
|
||||||
|
- apps/api/src/module-registry/module.guard.spec.ts
|
||||||
|
- apps/api/src/module-registry/module-registry.service.ts
|
||||||
|
- apps/api/src/tenders/tender-scheduler.service.spec.ts
|
||||||
|
- apps/api/scripts/rls-scratch-check.mjs
|
||||||
|
- docs/mandantentrennung-etappe2-fehlerrichtung.md
|
||||||
|
- docs/mandantentrennung-zugriffsklassifikation.md
|
||||||
|
- .planning/WINDOWS.md
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Module catalog binding decision separates MEASUREMENT (no RLS on Module today, so binding it would be inert) from CONDITION (it becomes catastrophic once Etappe 3 adds a policy) — corrects the planning brief's premise that binding would be catastrophic today"
|
||||||
|
- "The one absent signal (genuinely-no-grant vs query-found-nothing) is recorded as unsolved, not runtime-warned-around — same reasoning as getAllActiveConfigs in ldap and the five spots in tenders: a warning on a routine empty-result path is noise, not signal"
|
||||||
|
- "A cross-area test break (tender-scheduler.service.spec.ts, unmocked ModuleRegistryService against a $extends-less fake) was fixed with the same identity-mock convention already used in ldap.service.spec.ts, not by reshaping the production code"
|
||||||
|
|
||||||
|
requirements-completed: [WINDOWS-18, ETAPPE-2-MODULE-REGISTRY]
|
||||||
|
|
||||||
|
coverage: []
|
||||||
|
|
||||||
|
duration: ~75min
|
||||||
|
completed: 2026-09-10
|
||||||
|
status: complete
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick Task 260910-exd: Etappe 2, Bereich module-registry Summary
|
||||||
|
|
||||||
|
**The two-stage module-access decision path (TenantModuleActivation + ModuleGrant) is now fully forTenant()-bound in both services of the area, with a machine-verified measurement that the platform module catalog stays deliberately unbound and a recorded absence of a signal distinguishing a real access denial from a silently-broken query.**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** ~75 min
|
||||||
|
- **Tasks:** 3
|
||||||
|
- **Files modified:** 9 (1 created, 8 modified)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- `ModuleAccessService.getAccessibleModuleIds` (ADMIN/SUPER_ADMIN short-circuit, direct grant path, group grant path, D-02 intersection) and `getCatalogFlags`'s own activation read now run through `forTenant()`, one client per method — existing `tenantId` where-filters remain as the second net (T-JTS-02/T-JTS-03).
|
||||||
|
- `ModuleRegistryService.findActiveForTenant`, `activateForTenant`, `deactivateForTenant` (both activation accesses over one client), and `isModuleActive` are bound the same way; the six catalog accesses (`findAll`, `findBySlug`, both existence checks, `isModuleActive`'s catalog lookup, `seedModule`) stay deliberately unbound.
|
||||||
|
- `module-registry.service.spec.ts` created from scratch — the file previously had zero tests despite holding 11 of the area's 17 raw accesses and every write path. Covers both the loud direction (deactivating an unactivated module throws) and the silent direction (`isModuleActive` without an activation returns `false`) as named, deliberately-preserved properties.
|
||||||
|
- `module-access.service.spec.ts` rebuilt onto the two-client proof (`__makeBoundClient`, bound-call log) with a watchdog that fails if the catalog access ever appears in the bound-call log.
|
||||||
|
- One new case in `module.guard.spec.ts` pins down that "genuinely no grant" and "the resolution found nothing" produce the identical `ForbiddenException` message today — the machine record of the area's central finding.
|
||||||
|
- `isModuleActive`'s header comment corrected: it claimed `ModuleGuard` calls it; measured zero callers exist (the guard uses `findBySlug` + `getAccessibleModuleIds` instead).
|
||||||
|
- `rls-scratch-check.mjs` gained an eighth section (`runModuleRegistryAreaChecks`, 13 named checks) run against the real, delivered migrations — 66/66 checks pass. The measurement that the module catalog is genuinely unprotected today (no RLS, `pg_class.relrowsecurity = false`) and that the activation/grant unique keys structurally cannot repeat the tenders/user visible-row-collision chain (both lead with `tenantId`) is now committed evidence, not an assertion.
|
||||||
|
- `docs/mandantentrennung-zugriffsklassifikation.md` fully reconciled: all five hand-maintained sections (inventory rows, overview line 7/10, sum line 108/134, class distribution unchanged at 63 pairs, background-service-trap section recording the absence of a sixth case) — all machine-gated against the source.
|
||||||
|
- `docs/mandantentrennung-etappe2-fehlerrichtung.md` gained the `## Bereich module-registry` section (m1–m5) plus a Task-3 addendum naming both falsification proofs with test name and failure message.
|
||||||
|
- `.planning/WINDOWS.md` #23 records the missing distinguishing signal as an open deviation, with the concrete Etappe-4 preflight check and the reasoned rejection of a runtime warning.
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **Aufgabe 1: measure the error direction, no production code** — `7d45e2f` (test)
|
||||||
|
2. **Aufgabe 2: bind ModuleAccessService** — `3df7268` (feat)
|
||||||
|
3. **Aufgabe 3: bind ModuleRegistryService, finish the classification doc** — `9c0eefe` (feat)
|
||||||
|
|
||||||
|
_Note: no separate docs-only metadata commit yet — the orchestrator adds that after this SUMMARY._
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `apps/api/src/module-registry/module-access.service.ts` — `getAccessibleModuleIds`/`getCatalogFlags` bound to `forTenant()`, catalog access left unbound with a measurement+condition comment
|
||||||
|
- `apps/api/src/module-registry/module-access.service.spec.ts` — rebuilt onto the two-client bound-call-log proof, all 15 pre-existing cases retained plus 6 new binding cases (die Zahl stand hier zunaechst als 7; vom Verifizierer nachgezaehlt und berichtigt — 6 entspricht den im Plan benannten sechs Verhaltensweisen. Dieselbe Fehlerart wie in 260909-laa, wo eine Zusammenfassung drei Uebersetzungen behauptete und zwei geliefert waren)
|
||||||
|
- `apps/api/src/module-registry/module.guard.spec.ts` — one new case pinning the absence of a distinguishing signal
|
||||||
|
- `apps/api/src/module-registry/module-registry.service.ts` — `findActiveForTenant`/`activateForTenant`/`deactivateForTenant`/`isModuleActive` bound; `isModuleActive`'s header comment corrected
|
||||||
|
- `apps/api/src/module-registry/module-registry.service.spec.ts` — new, 16 cases
|
||||||
|
- `apps/api/src/tenders/tender-scheduler.service.spec.ts` — `forTenant()` mocked to identity (same convention as `ldap.service.spec.ts`) to fix a cross-area break caused by the `activateForTenant` conversion
|
||||||
|
- `apps/api/scripts/rls-scratch-check.mjs` — eighth section `runModuleRegistryAreaChecks`, 13 named checks
|
||||||
|
- `docs/mandantentrennung-etappe2-fehlerrichtung.md` — new `## Bereich module-registry` section (m1–m5) plus Task-3 addendum
|
||||||
|
- `docs/mandantentrennung-zugriffsklassifikation.md` — 5 inventory rows updated/reconciled, overview/sum/class-distribution/background-service-trap sections all reconciled
|
||||||
|
- `.planning/WINDOWS.md` — new open entry #23
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- **Module catalog binding stays a two-part statement, not a single claim.** The planning brief's premise ("binding the catalog would be catastrophic today") was measured and found FALSE — `Module` carries no RLS policy at all, so binding it today would be inert. The action (don't bind it) is unchanged, but the written reason now separates the measurement (no policy today) from the condition (it becomes catastrophic once Etappe 3 gives the table a policy).
|
||||||
|
- **The absent distinguishing signal is recorded, not engineered around.** There is no way today to tell "the user genuinely has no grant" from "a query silently found nothing" — both produce the identical 403, the identical empty 200 list, and no log line. A runtime warning at these spots was considered and rejected (same reasoning as `getAllActiveConfigs` in `ldap` and the five spots in `tenders`): a warning on a routine "no access" case would be constant noise on a fresh install.
|
||||||
|
- **Cross-area test break fixed with the existing convention, not a code reshape.** `tender-scheduler.service.spec.ts` drives the real `ModuleRegistryService` against a hand-rolled fake without `$extends`. Rather than adding `$extends`/`$transaction` support to that fake or weakening the production binding, `forTenant` was mocked to identity in that one file — the exact pattern `ldap.service.spec.ts` already established for tests that don't care about RLS binding mechanics.
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 1/3 - Blocking bug in a dependent area] `tender-scheduler.service.spec.ts` broke after `ModuleRegistryService.activateForTenant` started calling `forTenant()`**
|
||||||
|
- **Found during:** Task 3 (full-suite green check after converting `module-registry.service.ts`)
|
||||||
|
- **Issue:** This spec instantiates the real, unmocked `ModuleRegistryService` against a hand-rolled fake prisma object that has no `$extends` method (by design — it predates any RLS binding in this service). `forTenant()` calls `prisma.$extends(...)`, so the test failed with `prisma.$extends is not a function`.
|
||||||
|
- **Fix:** Mocked `forTenant` to an identity function (`vi.fn((p) => p)`) in this one file, matching the exact convention `ldap.service.spec.ts` already uses for the same reason (the test verifies poll-once-fan-out-many scheduler invariants, not RLS binding mechanics).
|
||||||
|
- **Files modified:** `apps/api/src/tenders/tender-scheduler.service.spec.ts`
|
||||||
|
- **Verification:** `npm --prefix apps/api run test -- src/tenders/tender-scheduler.service.spec.ts` green (4/4); full suite green afterward.
|
||||||
|
- **Committed in:** `9c0eefe` (Task 3 commit)
|
||||||
|
|
||||||
|
**2. [Rule 3 - Blocking, full-suite gate] `rls-access-inventory.spec.ts` went red immediately after binding `module-access.service.ts` in Task 2, before Task 3 (which owns the classification doc) had run**
|
||||||
|
- **Found during:** Task 2 (the plan's own verify block runs the full `npm --prefix apps/api run test` suite, which includes this cross-check between the classification doc and the source)
|
||||||
|
- **Issue:** `module-access.service.ts`'s `moduleGrant` and `tenantModuleActivation` rows in `docs/mandantentrennung-zugriffsklassifikation.md` still said `Stand: ungebunden` the moment the service code became bound — the doc and source diverged mid-plan, and Task 2's own verify gate (full test suite) demanded they match.
|
||||||
|
- **Fix:** Updated only the `Stand` column (and a one-sentence addition to the existing Begründung) for those two specific rows — not the overview line, sum line, class distribution, or background-service-trap section, all of which stayed correctly assigned to Task 3's full reconciliation pass.
|
||||||
|
- **Files modified:** `docs/mandantentrennung-zugriffsklassifikation.md`
|
||||||
|
- **Verification:** `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` green; full suite green (817/817) at the end of Task 2.
|
||||||
|
- **Committed in:** `3df7268` (Task 2 commit)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 2 auto-fixed (1 cross-area blocking bug, 1 blocking full-suite-gate correction split across tasks by necessity)
|
||||||
|
**Impact on plan:** Both were forced by the plan's own verify gates (full test suite must stay green after every task) rather than scope creep. No production behavior outside the two converted services was changed; the tender-scheduler fix only affects test wiring.
|
||||||
|
|
||||||
|
## Falsification Proofs (required, per plan)
|
||||||
|
|
||||||
|
**Task 2 — group path binding rolled back and restored:**
|
||||||
|
- Rolled `tenantPrisma.moduleGrant.findMany(...)` (group path inside `getAccessibleModuleIds`) back to `this.prisma.moduleGrant.findMany(...)`.
|
||||||
|
- `module-access.service.spec.ts` test **"ModuleAccessService — Bindung an forTenant() (260910-exd) > USER-Zweig bindet BEIDE Freigabe-Lesezugriffe (Direktweg und Gruppenweg) UND den Schnittmengen-Lesezugriff an DIESELBE Mandantenkennung"** went red: `AssertionError: expected 1 to be 2`.
|
||||||
|
- Reverted the rollback (file byte-identical to before the probe, confirmed via `diff`); the same test run went green again (23/23).
|
||||||
|
|
||||||
|
**Task 3 — deactivation write binding rolled back and restored:**
|
||||||
|
- Rolled `tenantPrisma.tenantModuleActivation.update(...)` (in `deactivateForTenant`) back to `this.prisma.tenantModuleActivation.update(...)`.
|
||||||
|
- `module-registry.service.spec.ts` test **"ModuleRegistryService.deactivateForTenant > bindet beide Aktivierungszugriffe (Lesen, Schreiben) an denselben Mandanten, über einen Klienten"** went red: `erwarteter gebundener Aufruf tenantModuleActivation.update(tenant=t1) fehlt im Protokoll: [{"tenantId":"t1","model":"tenantModuleActivation","method":"findUnique"}]: expected false to be true`.
|
||||||
|
- Reverted the rollback (file byte-identical to before the probe, confirmed via `diff`); the same test run went green again (16/16).
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None beyond the two deviations above — both handled inline without blocking task progress.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None — no external service configuration required. `DATABASE_URL` remains unchanged, pointed at the `tessera` role with `BYPASSRLS`. The switch stays off; this was measurement and application-layer binding work only.
|
||||||
|
|
||||||
|
## Self-Check
|
||||||
|
|
||||||
|
- `apps/api/src/module-registry/module-registry.service.spec.ts` — FOUND
|
||||||
|
- `apps/api/src/module-registry/module-access.service.spec.ts` — FOUND (modified)
|
||||||
|
- `apps/api/src/tenders/tender-scheduler.service.spec.ts` — FOUND (modified)
|
||||||
|
- Commit `7d45e2f` — FOUND in `git log`
|
||||||
|
- Commit `3df7268` — FOUND in `git log`
|
||||||
|
- Commit `9c0eefe` — FOUND in `git log`
|
||||||
|
- `npm --prefix apps/api run test` — 833/833 green, 56 files (was 810/55 at plan start)
|
||||||
|
- `npm --prefix apps/api run type-check` — clean
|
||||||
|
- `node apps/api/scripts/rls-scratch-check.mjs` — 66/66 checks passed, exit 0
|
||||||
|
- WINDOWS #23 present in both the table and the JSON block of `.planning/WINDOWS.md`
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
Five bereiche remain in Etappe 2, by today's raw-hit count: `dashboard` (13), `calendar` (12), `tenant` (8), `favorites` (7), `auth` (5, gemischt), `settings` (4). Two order conditions carried forward from this run: `dashboard`'s module-access filter is already correct because the binding sits in `ModuleAccessService` (no file in `dashboard` needs touching for that); `settings` remains the open order condition for `tenders` (SMTP credentials) and is the smallest remaining area at 4 raw hits.
|
||||||
|
|
||||||
|
## Self-Check: PASSED
|
||||||
|
|
||||||
|
All created/modified files and all three task commits verified present via `[ -f ... ]` and `git log --oneline --all | grep`; no missing items.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: quick-260910-exd*
|
||||||
|
*Completed: 2026-09-10*
|
||||||
+275
@@ -0,0 +1,275 @@
|
|||||||
|
---
|
||||||
|
phase: quick-260910-exd
|
||||||
|
verified: 2026-09-10T00:00:00Z
|
||||||
|
status: passed
|
||||||
|
score: 9/9 must-haves verified
|
||||||
|
covered_files: [".planning/WINDOWS.md", ".planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-PLAN.md", ".planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-SUMMARY.md", "apps/api/scripts/rls-scratch-check.mjs", "apps/api/src/module-registry/module-access.service.spec.ts", "apps/api/src/module-registry/module-access.service.ts", "apps/api/src/module-registry/module-registry.service.spec.ts", "apps/api/src/module-registry/module-registry.service.ts", "apps/api/src/module-registry/module.guard.spec.ts", "apps/api/src/tenders/tender-scheduler.service.spec.ts", "docs/mandantentrennung-etappe2-fehlerrichtung.md", "docs/mandantentrennung-zugriffsklassifikation.md"]
|
||||||
|
covered_digest: "v1:sha256:47e75c553a401d96420242d168f37b0fbc0f89e54dfac382bd3ee80ba1d04a0c"
|
||||||
|
overrides_applied: 0
|
||||||
|
behavior_unverified: 0
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick Task 260910-exd Verification: Mandantentrennung Etappe 2, Bereich `module-registry`
|
||||||
|
|
||||||
|
**Task Goal:** Bind the tenant-bound access sites in `apps/api/src/module-registry/`, leave the
|
||||||
|
platform-wide module catalogue deliberately unbound with the CORRECTED reason, create the missing
|
||||||
|
spec coverage, record the absent denial-signal three ways, and leave the classification document's
|
||||||
|
five hand-maintained sections in sync.
|
||||||
|
|
||||||
|
**Verified:** 2026-09-10
|
||||||
|
**Status:** passed
|
||||||
|
**Commits reviewed:** 7d45e2f, 3df7268, 9c0eefe (base a2516a9)
|
||||||
|
|
||||||
|
This is a re-verification-grade, adversarial re-audit against the codebase — not a re-read of
|
||||||
|
SUMMARY.md. Every claim below was independently reproduced (live DB run, source greps, recomputed
|
||||||
|
class-distribution table, commit-level diffs) rather than accepted from the SUMMARY.
|
||||||
|
|
||||||
|
## Priority Findings (per orchestrator's numbered scrutiny list)
|
||||||
|
|
||||||
|
### 1. THE PRIORITY ITEM — `tender-scheduler.service.spec.ts` identity mock: LEGITIMATE, not a regression
|
||||||
|
|
||||||
|
Verified by reading the file and its neighbors directly:
|
||||||
|
|
||||||
|
- `tender-scheduler.service.spec.ts` mocks `forTenant` to identity (`vi.fn((p) => p)`) — this is
|
||||||
|
true, and by itself would be the exact defect this effort documented in `ldap`.
|
||||||
|
- **But the binding for the method this file exercises (`ModuleRegistryService.activateForTenant`)
|
||||||
|
is proven elsewhere, and proven rigorously.** `apps/api/src/module-registry/module-registry.service.spec.ts`
|
||||||
|
(new file, 344 lines, 16 `it()` cases) uses a genuine **two-client bound-call-log proof**
|
||||||
|
(`__makeBoundClient`, a second, distinguishable wrapper object that logs every call routed
|
||||||
|
through it) — not an identity mock. Its `activateForTenant` describe block
|
||||||
|
(lines 182–207) asserts `expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'upsert')` —
|
||||||
|
this assertion is false (test fails) if the binding is removed. Confirmed live: the SUMMARY's
|
||||||
|
claimed falsification proof for this exact method (Task 3, `deactivateForTenant`'s update call)
|
||||||
|
is reproduced verbatim in `docs/mandantentrennung-etappe2-fehlerrichtung.md` (see below) with a
|
||||||
|
concrete red message, not just a commit-log claim.
|
||||||
|
- Checked whether "the exact `ldap.service.spec.ts` convention" claim holds up: `ldap.service.spec.ts`
|
||||||
|
does carry a **file-level identity mock** at the top (line 56–57, `forTenant: vi.fn((p) => p)`)
|
||||||
|
**and** a separate, dedicated binding-proof block later in the same file (from ~line 2300) that
|
||||||
|
reconfigures the `forTenant` spy per-test and asserts `expect(forTenant).toHaveBeenCalledWith(...)`
|
||||||
|
for `listGroups`, `searchUsers`, etc. `ldap-config.service.spec.ts` follows the identical
|
||||||
|
two-part pattern (identity mock at top, line 9–10; dedicated "Bindung an forTenant()" describe
|
||||||
|
block at line 197 asserting `toHaveBeenCalledWith`). So the convention is real, not a misreading.
|
||||||
|
`tender-scheduler.service.spec.ts` only needed the "identity mock" half of that convention,
|
||||||
|
because — unlike `ldap.service.spec.ts` testing itself — it doesn't test `ModuleRegistryService`'s
|
||||||
|
binding at all; it tests `TenderSchedulerService`'s poll-once-fan-out-many behavior using the real
|
||||||
|
`ModuleRegistryService` as an unmocked collaborator. The binding proof for
|
||||||
|
`ModuleRegistryService.activateForTenant` correctly lives in `module-registry.service.spec.ts`,
|
||||||
|
which is the file that owns that method.
|
||||||
|
- Verified no other unmocked caller of `new ModuleRegistryService(...)` exists
|
||||||
|
(`grep -rn "new ModuleRegistryService" apps/api/src` → only the DI module and this one spec file).
|
||||||
|
|
||||||
|
**Conclusion: legitimate, not a regression in disguise.**
|
||||||
|
|
||||||
|
### 2. The Task-2/Task-3 classification-doc split: HONEST ordering consequence, not gaming
|
||||||
|
|
||||||
|
`git show 3df7268 -- docs/mandantentrennung-zugriffsklassifikation.md` shows the Task-2 commit
|
||||||
|
touched **exactly two lines** of the classification doc: the `Stand` column (ungebunden→gebunden)
|
||||||
|
and a one-sentence addition to the existing `Begründung` for `module-access.service.ts`/`moduleGrant`
|
||||||
|
and `/tenantModuleActivation` — nothing else. The overview line, sum line, class-distribution table,
|
||||||
|
and background-service-trap section were untouched in that commit and only changed in Task 3's
|
||||||
|
commit (`9c0eefe`), exactly as the plan required. This is the minimum edit forced by Task 2's own
|
||||||
|
verify gate (`npm --prefix apps/api run test` includes `rls-access-inventory.spec.ts`, which checks
|
||||||
|
the doc against the live source on every run) — not scope creep or a doc bent to make a check pass.
|
||||||
|
|
||||||
|
### 3. The corrected premise (Befund E) is preserved as corrected
|
||||||
|
|
||||||
|
Both `module-access.service.ts` (lines 101–107) and `module-registry.service.ts` (multiple
|
||||||
|
locations) carry the comment in the required two-part form: **MEASUREMENT** ("die Tabelle
|
||||||
|
traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal") plus
|
||||||
|
**CONDITION** ("katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt"). The
|
||||||
|
classification doc rows (lines 301, 304) and the critique doc's (m4)/(m1) sections repeat this
|
||||||
|
exact framing. No occurrence of the original, now-falsified claim ("a binding would make the
|
||||||
|
catalog invisible today") was found anywhere in the diff.
|
||||||
|
|
||||||
|
### 4. Coverage and the bind/don't-bind line — independently counted
|
||||||
|
|
||||||
|
```
|
||||||
|
this.prisma.module → module-access.service.ts: 1 (unbound)
|
||||||
|
this.prisma.module → module-registry.service.ts: 6 (unbound)
|
||||||
|
tenantPrisma.* → module-access.service.ts: 5 (bound: 2× moduleGrant, 3× tenantModuleActivation)
|
||||||
|
tenantPrisma.* → module-registry.service.ts: 5 (bound: 5× tenantModuleActivation)
|
||||||
|
```
|
||||||
|
|
||||||
|
**7 unbound / 10 bound — matches the SUMMARY's claim exactly**, and both numbers were counted fresh
|
||||||
|
from the current source, not copied from any document. Raw-hit total (17) also matches Befund A.
|
||||||
|
`grep -rn 'tenantPrisma\.module\.'` (the forbidden literal) returns zero hits — the catalog was not
|
||||||
|
accidentally bound.
|
||||||
|
|
||||||
|
### 5. Default-closed cannot fail open
|
||||||
|
|
||||||
|
`module-access.service.spec.ts` line 354 ("Vorgabezustand bleibt geschlossen und ueberlebt die
|
||||||
|
Bindung") pins that with `grantedIds.length === 0` the intersection query (`tenantModuleActivation`)
|
||||||
|
is **never called** — the bound-call log is asserted empty for that model in that path. The
|
||||||
|
ADMIN/SUPER_ADMIN short-circuit condition (`role === 'ADMIN' || role === 'SUPER_ADMIN'`) is
|
||||||
|
unchanged from the pre-existing code — confirmed by reading `module-access.service.ts` line 53 and
|
||||||
|
the git diff, which shows no changes to the role-check line.
|
||||||
|
|
||||||
|
### 6. The absent denial signal — recorded three ways, all confirmed
|
||||||
|
|
||||||
|
1. **Critique text**, unvarnished: `docs/mandantentrennung-etappe2-fehlerrichtung.md` (m3),
|
||||||
|
"Die Antwort lautet **keines** — schlicht, ohne Beschönigung," with the three identical-looking
|
||||||
|
spots named (same `ForbiddenException` message, same empty 200 list, no log line) and the added
|
||||||
|
observation that the affected user has a ready, false explanation.
|
||||||
|
2. **Test case**: `module.guard.spec.ts` lines 137–184, two guard instances (genuinely-no-grant vs.
|
||||||
|
query-found-nothing) held against each other, asserting identical exception messages, with a
|
||||||
|
header comment stating the test is *supposed* to go red once a distinguishing signal is added.
|
||||||
|
3. **WINDOWS #23**: present in both the table (line 40) and the JSON block (lines 309–319) of
|
||||||
|
`.planning/WINDOWS.md`, `status: open`, naming the concrete Etappe-4 preflight
|
||||||
|
(`rls-preflight.mjs`: active activation rows present but resolution empty for a known admin) and
|
||||||
|
the reasoned rejection of a runtime warning. Table-row count (23) matches JSON-entry count (23)
|
||||||
|
— internally consistent.
|
||||||
|
|
||||||
|
### 7. No safeguard that guards nothing
|
||||||
|
|
||||||
|
`grep -rn "P2002" apps/api/src/module-registry` returns zero hits. No unique-constraint-collision
|
||||||
|
translation was added — consistent with Befund H/pruefung 12/13 (both affected unique keys lead
|
||||||
|
with `tenantId`, so the `tenders`/`user` collision chain structurally cannot occur here).
|
||||||
|
|
||||||
|
### 8. The measurements are committed — reproduced live against the real container
|
||||||
|
|
||||||
|
Independently re-ran the tool against the live `tessera-ctl-db-1` container (address resolved
|
||||||
|
fresh via `docker inspect`, `172.19.0.2` at verification time — not copied from any document):
|
||||||
|
|
||||||
|
```
|
||||||
|
$ TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@172.19.0.2:5432/postgres" \
|
||||||
|
node apps/api/scripts/rls-scratch-check.mjs
|
||||||
|
...
|
||||||
|
Alle 66 Pruefungen bestanden.
|
||||||
|
```
|
||||||
|
|
||||||
|
All 13 named module-registry checks (`modulegrant-ungebunden-null-zeilen` through
|
||||||
|
`freigabe-eindeutigkeitsindizes-fuehren-mit-der-mandantenkennung`) passed with output byte-identical
|
||||||
|
to what's recorded in the critique document's (m1) section. 66 = 53 baseline + 13 new — confirmed.
|
||||||
|
|
||||||
|
### 9. The five hand-maintained document sections — recomputed independently
|
||||||
|
|
||||||
|
All five confirmed by independent recomputation, not by trusting the document:
|
||||||
|
|
||||||
|
- **Bestandsaufnahme rows**: all five (file, model) pairs for `module-registry` present with the
|
||||||
|
correct `Stand` (3 gebunden, 2 ungebunden) and MEASUREMENT+CONDITION-separated reasoning.
|
||||||
|
- **Übersichtszeile**: `module-registry | 7 | 10` — matches the independently-counted raw hits.
|
||||||
|
- **Summenzeile**: independently summed all 12 area rows — ungebunden 36+0+4+1+8+7+13+8+12+8+7+4 =
|
||||||
|
**108**, gebunden 26+31+26+22+14+10+0+5+0+0+0+0 = **134** — matches the document's `**108**`/`**134**`
|
||||||
|
exactly.
|
||||||
|
- **Klassen-Verteilung**: ran an independent `awk` pass over every `apps/api/src/...` Bestandsaufnahme
|
||||||
|
row and recomputed class counts from scratch: `muss-mandantengebunden: 31, keine-mandantengebundene-tabelle: 17,
|
||||||
|
beides: 13, bewusst-uebergreifend: 2, TOTAL: 63` — matches the document's table and its "63 Paare"
|
||||||
|
heading exactly.
|
||||||
|
- **Hintergrunddienst-als-Falle**: heading says "fünf Fälle"; four are formatted as
|
||||||
|
`- **\`file\`**` bullets (ldap.service.ts, tender-digest.scheduler.ts, tender-matching.service.ts,
|
||||||
|
admin-seed.service.ts) and the fifth (`dkv-scheduler.service.ts`) is deliberately formatted
|
||||||
|
differently ("anderer Bauart") — count of 5 confirmed by inspection. The section explicitly states
|
||||||
|
`module-registry` adds **no** sixth case, with a measured reason (`seedModule` writes without
|
||||||
|
tenant context but doesn't iterate per-tenant, so it lacks the read-across/bind-within-loop shape).
|
||||||
|
|
||||||
|
### 10. Falsification proofs — both present in the document, not just commit messages
|
||||||
|
|
||||||
|
Confirmed both are written into `docs/mandantentrennung-etappe2-fehlerrichtung.md`'s "Nachtrag
|
||||||
|
(260910-exd, Aufgabe 3)" section (lines 1515–1533), each with the exact test name and exact failure
|
||||||
|
message:
|
||||||
|
|
||||||
|
- Task 2: group-path rollback → `module-access.service.spec.ts` test "USER-Zweig bindet BEIDE..."
|
||||||
|
went red with `expected 1 to be 2`; reverted, 23/23 green again.
|
||||||
|
- Task 3: `deactivateForTenant` write rollback → `module-registry.service.spec.ts` test "bindet
|
||||||
|
beide Aktivierungszugriffe..." went red with the bound-call-log assertion message quoted verbatim;
|
||||||
|
reverted, 16/16 green again.
|
||||||
|
|
||||||
|
### 11. Constraints held
|
||||||
|
|
||||||
|
- No Prisma schema/migration changes: `git diff --name-only a2516a9..9c0eefe -- apps/api/prisma`
|
||||||
|
→ empty.
|
||||||
|
- No compose/env changes: none of `docker-compose*.yml`/`.env*.example` appear in the diff file list.
|
||||||
|
- `assertTargetBelongsToTenant` still present and called twice in
|
||||||
|
`apps/api/src/groups/module-grants.service.ts` (lines 46, 103, 242) — unchanged.
|
||||||
|
- No `dashboard` file touched: `git diff --name-only a2516a9..9c0eefe -- apps/api/src/dashboard`
|
||||||
|
→ empty.
|
||||||
|
- T-JTS-02/T-JTS-03 recorded as open, not fixed, in (m4) of the critique doc, with explicit note
|
||||||
|
that the write-side check in `module-grants.service.ts` remains the only protection until Etappe 4.
|
||||||
|
- Full diff file list (10 files) matches exactly what the plan's `files_modified` frontmatter and
|
||||||
|
the task-level file scopes declared — no unexpected files touched.
|
||||||
|
|
||||||
|
## Goal Achievement
|
||||||
|
|
||||||
|
### Observable Truths
|
||||||
|
|
||||||
|
| # | Truth | Status | Evidence |
|
||||||
|
|---|-------|--------|----------|
|
||||||
|
| 1 | Both stages (activation, grant) bound; no method binds one stage and leaves the other unbound; no method mixes two tenants in one resolution | ✓ VERIFIED | `module-access.service.ts`/`module-registry.service.ts` read in full; two-client bound-call-log tests pin single-tenant-per-resolution property (e.g. "Rollen-Kurzschluss waechst..." test) |
|
||||||
|
| 2 | Catalog stays unbound with MEASURED (not assumed) reasoning, condition stated as condition | ✓ VERIFIED | Comments in both service files + classification doc rows 301/304 use the exact measurement+condition framing; live DB run confirms `module-tabelle-traegt-keinen-zeilenschutz` and `katalog-bindung-aendert-heute-nichts-an-der-ergebnismenge` |
|
||||||
|
| 3 | Reverse error direction measured against the real delivered rule, per-path signal described | ✓ VERIFIED | (m2) signal table in critique doc covers all named paths incl. the two deliberately-unbound catalog paths as boundary |
|
||||||
|
| 4 | Absence of a distinguishing denial signal explicitly handled, three ways | ✓ VERIFIED | Critique text (m3), `module.guard.spec.ts` test, WINDOWS #23 — all three confirmed present and consistent |
|
||||||
|
| 5 | Follows `module-grants.service.ts` convention: one client per method named `tenantPrisma`, existing where-filters retained, app-layer check not replaced by DB | ✓ VERIFIED | Code read directly; `assertTargetBelongsToTenant` untouched; where-filters retained (e.g. `getAccessibleModuleIds` still filters `tenantId` in every query) |
|
||||||
|
| 6 | Test coverage established BEFORE conversion; forgotten binding call goes red | ✓ VERIFIED | Two falsification proofs reproduced in the doc with exact red messages; two-client proof (not identity mock) used throughout |
|
||||||
|
| 7 | Both false header comments corrected at the measurement | ✓ VERIFIED | `isModuleActive`/`findActiveForTenant` comments in `module-registry.service.ts` now state "Richtiggestellt... Gemessen... kein Aufrufer" with reference to TEIL 3 |
|
||||||
|
| 8 | Baseline held: 810+ tests green, type-check clean, scratch tool all-pass, at end of every task | ✓ VERIFIED | Orchestrator confirmed 833/833 green (56 files, baseline 810/55), type-check exit 0; independently reran scratch tool live: 66/66 |
|
||||||
|
| 9 | Switch stays off: `DATABASE_URL` on `tessera` role, schema/migrations unchanged, T-JTS-02/03 recorded not fixed | ✓ VERIFIED | No prisma/migration diff; T-JTS-02/03 explicitly recorded as open in (m4) |
|
||||||
|
|
||||||
|
**Score:** 9/9 truths verified, 0 present-but-behavior-unverified.
|
||||||
|
|
||||||
|
### Required Artifacts
|
||||||
|
|
||||||
|
| Artifact | Expected | Status | Details |
|
||||||
|
|----------|----------|--------|---------|
|
||||||
|
| `apps/api/scripts/rls-scratch-check.mjs` | 8th section, 13 named checks | ✓ VERIFIED | `runModuleRegistryAreaChecks` present, called between `runUserAreaChecks`/`runTransactionShapeMeasurement`; 66/66 pass live |
|
||||||
|
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | `## Bereich module-registry`, (m1)-(m5) | ✓ VERIFIED | All 5 subsections present with required content; Nachtrag with both falsification proofs |
|
||||||
|
| `apps/api/src/module-registry/module-access.service.ts` | 3 activation + 2 grant accesses bound, catalog unbound | ✓ VERIFIED | 5 bound call sites, 1 unbound, matches |
|
||||||
|
| `apps/api/src/module-registry/module-access.service.spec.ts` | two-client proof, all cases retained | ✓ VERIFIED | 23 cases total, `__makeBoundClient` two-client log used |
|
||||||
|
| `apps/api/src/module-registry/module.guard.spec.ts` | denial-signal absence case | ✓ VERIFIED | Present, lines 137-184 |
|
||||||
|
| `apps/api/src/module-registry/module-registry.service.ts` | 5 activation accesses bound, 6 catalog unbound, both comments corrected | ✓ VERIFIED | Matches exactly |
|
||||||
|
| `apps/api/src/module-registry/module-registry.service.spec.ts` | NEW file | ✓ VERIFIED | 344 lines, 16 cases, two-client proof |
|
||||||
|
| `docs/mandantentrennung-zugriffsklassifikation.md` | 5 sections reconciled | ✓ VERIFIED | Independently recomputed, matches exactly |
|
||||||
|
| `.planning/WINDOWS.md` | open entry, table + JSON | ✓ VERIFIED | #23 present both places, internally consistent (23=23) |
|
||||||
|
|
||||||
|
### Behavioral Spot-Checks
|
||||||
|
|
||||||
|
| Behavior | Command | Result | Status |
|
||||||
|
|----------|---------|--------|--------|
|
||||||
|
| Scratch tool passes against live DB | `node apps/api/scripts/rls-scratch-check.mjs` (against `172.19.0.2`) | "Alle 66 Pruefungen bestanden." | ✓ PASS |
|
||||||
|
| `activateForTenant` binding provable | Read `module-registry.service.spec.ts` line 193 assertion | `expectBoundCall(..., 'tenantModuleActivation', 'upsert')` | ✓ PASS |
|
||||||
|
| No forbidden literal `tenantPrisma.module.` | `grep -rn 'tenantPrisma\.module\.' apps/api/src/module-registry` | 0 hits | ✓ PASS |
|
||||||
|
| No debt markers in modified files | `grep -nE "TBD\|FIXME\|XXX\|TODO\|HACK\|PLACEHOLDER"` across all 10 diffed files | 0 hits | ✓ PASS |
|
||||||
|
| Constraint boundaries held | `git diff --name-only a2516a9..9c0eefe` | exactly the 10 expected files | ✓ PASS |
|
||||||
|
|
||||||
|
### Anti-Patterns Found
|
||||||
|
|
||||||
|
None. No debt markers, no stub returns, no hardcoded empty data flowing to output in the reviewed files.
|
||||||
|
|
||||||
|
### Minor Informational Notes (not gaps)
|
||||||
|
|
||||||
|
- SUMMARY.md states "module-access.service.spec.ts rebuilt... plus 7 new binding cases." Independent
|
||||||
|
count of the file's final "Bindung an forTenant() (260910-exd)" describe block shows **6** new
|
||||||
|
cases (lines 327, 336, 354, 370, 387, 400), matching the plan's own behavior spec (6 bullet
|
||||||
|
points). This is a minor off-by-one in the SUMMARY narrative, not a must-have and not affecting
|
||||||
|
any verified artifact or test outcome — recorded here for completeness, not as a gap.
|
||||||
|
|
||||||
|
## Requirements Coverage
|
||||||
|
|
||||||
|
| Requirement | Description | Status | Evidence |
|
||||||
|
|-------------|-------------|--------|----------|
|
||||||
|
| WINDOWS-18 | Broken-windows tracking for RLS bypass deviations | ✓ SATISFIED | WINDOWS #23 added |
|
||||||
|
| ETAPPE-2-MODULE-REGISTRY | Bind module-registry area per Etappe 2 pattern | ✓ SATISFIED | All 17 raw accesses decided (10 bound, 7 unbound-with-reason) |
|
||||||
|
|
||||||
|
(These are quick-task-local requirement tags, not entries in `.planning/REQUIREMENTS.md` — expected
|
||||||
|
for a quick task, not an orphan.)
|
||||||
|
|
||||||
|
## Human Verification Required
|
||||||
|
|
||||||
|
None. All must-haves were verifiable programmatically (source review, live DB run, independent
|
||||||
|
recomputation of hand-maintained document sections).
|
||||||
|
|
||||||
|
## Gaps Summary
|
||||||
|
|
||||||
|
No gaps found. This is one of the most rigorously self-falsifying deliveries in this series: the
|
||||||
|
priority-scrutiny item (the tender-scheduler identity mock) held up under adversarial re-check
|
||||||
|
because the actual binding proof lives in the correct file with a genuine two-client log, the
|
||||||
|
Task-2/Task-3 document split was the minimum forced edit (verified via commit-level diff, not
|
||||||
|
narrative), the corrected catalog-binding premise is preserved as a measurement+condition pair
|
||||||
|
everywhere it appears, all coverage counts were independently reproduced from source (not copied
|
||||||
|
from the SUMMARY), the scratch tool was re-run live against the real database and matched the
|
||||||
|
committed measurement byte-for-byte, and both falsification proofs are recorded in the permanent
|
||||||
|
document with exact test names and failure messages, not left only in commit messages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Verified: 2026-09-10*
|
||||||
|
*Verifier: Claude (gsd-verifier)*
|
||||||
Reference in New Issue
Block a user