docs(quick-260914-eym): Etappe 3c abgeschlossen — Kritikschrift, Klassifikation, Auftrag, Datenbankrolle, WINDOWS #21/#30 geschlossen, Single-Flight-Riegel als Eintrag
Tessera CI/CD / Lint & Type Check (push) Successful in 50s
Tessera CI/CD / Tests (push) Successful in 53s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s

- Kritikschrift: neuer Abschnitt "## Systemkontext (Etappe 3c, 260914-eym)"
  mit (y1) woertlicher Werkzeugausgabe und pg_policies der lebenden DB,
  (y2) Signaltabelle beider Fehlerrichtungen samt Rueckbau-Belegen (a)-(d),
  (y3) Leere-als-Abwesenheit je Pfad (kein Pfad loescht), (y4) bewusst
  nicht geloest, (y5) bewusst nicht angefasst; Nachtraege in (d4), (s4),
  (b4) und im Abschluss
- Klassifikation: Uebersichtstabelle mit dritter Spalte System, Werte
  nachgerechnet (61/179/5), Stand-Absatz 260914-eym (72 Paare, Klassen
  unveraendert, sieben Staende geaendert), sechs Regelschluesse im
  Hintergrunddienst-Abschnitt, admin-seed-Zeile mit 3c-Befund, 3c-Punkt
  unter "NICHT entscheidet" erledigt
- Auftrag: 3c als Erledigt vermerkt (3d64567/6e2a641), zwei neue Fallen
  unter "Werkzeuge und Fallen"
- Datenbankrolle: dritte Sitzungsvariable, Nachtrag zum Systemkontext und
  zur weiterhin gueltigen Vorher-Pruefung ohne-kontext-leer
- Ledger (ueber gsd-tools windows): #21 fixed, #30 fixed, #37 neu
  (prozessweiter Single-Flight-Riegel processInbox) — open 15 / waived 1 /
  fixed 21 / total 37, aus den Zeilen gezaehlt

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
2026-09-14 11:52:47 +02:00
parent 6e2a641d76
commit 939c8121a1
5 changed files with 435 additions and 38 deletions
@@ -985,6 +985,13 @@ abgeschrieben.
entscheidet sie nicht — er bindet dienst-intern, wie `ldap`, `groups` und
`tenders` es vormachen.
**Nachtrag (260914-eym):** WINDOWS #21 ist GESCHLOSSEN — `loadActiveConfigsForScheduler()`
liest über `forSystem()` (Systemleseregel auf DkvModuleConfig) ALLE aktiven
Konfigurationen, der Planer registriert je Mandant einen eigenen Auftrag
`dkv-inbox-poll:<tenantId>`; die Erwähnung des einen Auftragsnamens
`dkv-inbox-poll` oben bleibt als historischer Stand stehen. Siehe
`## Systemkontext (Etappe 3c, 260914-eym)`.
### (d5) Was dieser Durchlauf bewusst nicht anfasst
- **Die beiden mehrschrittigen Stellen bleiben unatomar (Befund C, TEIL
@@ -3096,6 +3103,15 @@ unverändert und steht nicht in der Erlaubnisliste.
`SmtpConfig`-Zeile über die Wartungsrolle lesen und den gebundenen
`findUnique` daneben halten — dieselbe Form wie bei `dashboard`/`calendar`.
**Nachtrag (260914-eym):** WINDOWS #30 ist GESCHLOSSEN — nicht durch einen
Systemkontext, sondern durch ENTFERNEN des Startpfads: die Mailer-Fabrik in
`mail.module.ts` und die Startpfad-Methode in `settings.service.ts` sind
gelöscht, `MailService` baut je Versand einen Transport aus
`getDecryptedSmtpConfig(tenantId)` des Empfänger-Mandanten
(`requestPasswordReset` reicht `user.tenantId` durch). Deshalb trägt
SmtpConfig keine `system_read_policy`. Siehe
`## Systemkontext (Etappe 3c, 260914-eym)`.
### (s5) Was dieser Durchlauf bewusst nicht anfasst
- `settings.controller.ts` — nur gelesen (siehe (s4)(d)).
@@ -3214,6 +3230,11 @@ Forbidden zu NotFound.
nichts, `tenderrssfeed-gemeinsame-zeile-ohne-benutzer-weiterhin-entfernbar`
bestätigt das lediglich erneut.
**Nachtrag (260914-eym):** der erste Punkt ist eingelöst — der Systemkontext
für Hintergrunddienste ist gebaut (`## Systemkontext (Etappe 3c, 260914-eym)`),
`tender-digest.scheduler.ts` liest seine Kandidaten über `forSystem()` und
bleibt in der Schleife gebunden.
### (b5) Was dieser Durchlauf bewusst nicht anfasst
- Die vier Tabellen mit `userId`-Spalte, die KEINE persönlichen Daten tragen
@@ -3226,6 +3247,221 @@ Forbidden zu NotFound.
- Der Schalter (`DATABASE_URL` → Rolle `tessera`, BYPASSRLS) — bleibt AUS.
- Compose-/Umgebungsdateien — unangetastet.
## Systemkontext (Etappe 3c, 260914-eym)
Helfer-, Datenbank- und Dienstumbau: Migration `20260914120000_rls_system_context_read`
bringt die Funktion `is_system_context()` und je betroffener Tabelle eine
zusätzliche, NUR lesende Regel `system_read_policy … FOR SELECT` auf
DkvModuleConfig, LdapConfig, LdapFieldMapping, TenderMatch und
TenderSavedSearch. `forSystem(prisma)` ist der Schwesterhelfer von
`forTenant()` (gleiche Array-Form-Bauart, setzt `app.system_context = 'true'`
und die beiden anderen Sitzungsvariablen ausdrücklich leer; `forTenant()`
und `withTenantTransaction()` setzen umgekehrt `app.system_context = ''`).
Vier Dateien rufen ihn an fünf Stellen (Erlaubnisliste
`FORSYSTEM_ALLOWED_CALL_SITES` im Detektor, exakte Zahl je Datei): der
DKV-Planer-Startpfad (jetzt ein Cron-Auftrag je aktivem Mandanten,
WINDOWS #21), beide Leser in `ldap-config.service.ts`, die Kandidatenabfrage
des Digest, die Profilabfrage des Abgleichs. Der Mail-Startpfad ist nicht
umgestellt, sondern ENTFERNT (Transport je Versand nach Mandant des
Empfängers, WINDOWS #30); `admin-seed.service.ts` liest außerhalb seiner
Schleife nur `Tenant` (keine Regel) und ist unverändert. Der Schalter bleibt
AUS — nichts hiervon wirkt, bis Etappe 4 scharfschaltet.
### (y1) Die Messung
Wörtliche Werkzeugausgabe der vier Funktionsfälle (`rls-scratch-check.mjs`,
Wegwerf-Rolle ohne BYPASSRLS, Funktion aus der Migration geschnitten):
```
is-system-context-ungesetzt-false: bestanden — ohne gesetzte Variable: is_system_context() = false (Rohwert null) — die Vorher-Pruefung ohne-kontext-leer in rls-preflight.mjs bleibt gueltig
is-system-context-leer-false: bestanden — nach set_config('app.system_context', '', true): false
is-system-context-true-true: bestanden — nach set_config('app.system_context', 'true', true): true
is-system-context-fremdwert-false: bestanden — nach set_config('app.system_context', 'yes', true): false
```
Je Tabelle die drei Kern-Kennungen (zu wenig / zu viel / Erben) über den
GENERIERTEN Client, Wegwerf-Tabellen mit allen skalaren Spalten, Regeln
wortgleich aus ihren Migrationen geschnitten:
```
dkvmoduleconfig-systemkontext-sieht-beide-mandanten: bestanden — system.dkvModuleConfig.findMany() liefert 2 Zeile(n) aus Mandanten ["TENANT-A","TENANT-B"]
dkvmoduleconfig-systemkontext-insert-abgewiesen-42501: bestanden — system.dkvModuleConfig.create wirft PrismaClientUnknownRequestError, SQLSTATE 42501: ConnectorError(ConnectorError { user_facing_error: None, kind: QueryError(Post […]
dkvmoduleconfig-fortenant-a-nach-systemkontext-nur-a: bestanden — bound(TENANT-A).dkvModuleConfig.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 1 Zeile(n) aus ["TENANT-A"]
ldapconfig-systemkontext-sieht-beide-mandanten: bestanden — system.ldapConfig.findMany() liefert 2 Zeile(n) aus Mandanten ["TENANT-A","TENANT-B"]
ldapconfig-systemkontext-insert-abgewiesen-42501: bestanden — system.ldapConfig.create wirft PrismaClientUnknownRequestError, SQLSTATE 42501: ConnectorError(ConnectorError { user_facing_error: None, kind: QueryError(PostgresError […]
ldapconfig-fortenant-a-nach-systemkontext-nur-a: bestanden — bound(TENANT-A).ldapConfig.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 1 Zeile(n) aus ["TENANT-A"]
ldapfieldmapping-systemkontext-sieht-beide-mandanten: bestanden — system.ldapFieldMapping.findMany() liefert 2 Zeile(n) aus Mandanten ["TENANT-A","TENANT-B"]
ldapfieldmapping-systemkontext-insert-abgewiesen-42501: bestanden — system.ldapFieldMapping.create wirft PrismaClientUnknownRequestError, SQLSTATE 42501: ConnectorError(ConnectorError { user_facing_error: None, kind: QueryError(Po […]
ldapfieldmapping-fortenant-a-nach-systemkontext-nur-a: bestanden — bound(TENANT-A).ldapFieldMapping.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 1 Zeile(n) aus ["TENANT-A"]
tendermatch-systemkontext-sieht-beide-mandanten: bestanden — system.tenderMatch.findMany() liefert 2 Zeile(n) aus Mandanten ["TENANT-A","TENANT-B"]
tendermatch-systemkontext-insert-abgewiesen-42501: bestanden — system.tenderMatch.create wirft PrismaClientUnknownRequestError, SQLSTATE 42501: ConnectorError(ConnectorError { user_facing_error: None, kind: QueryError(PostgresErro […]
tendermatch-fortenant-a-nach-systemkontext-nur-a: bestanden — bound(TENANT-A).tenderMatch.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 1 Zeile(n) aus ["TENANT-A"]
tendersavedsearch-systemkontext-sieht-beide-mandanten: bestanden — system.tenderSavedSearch.findMany() liefert 2 Zeile(n) aus Mandanten ["TENANT-A","TENANT-B"]
tendersavedsearch-systemkontext-insert-abgewiesen-42501: bestanden — system.tenderSavedSearch.create wirft PrismaClientUnknownRequestError, SQLSTATE 42501: ConnectorError(ConnectorError { user_facing_error: None, kind: QueryError( […]
tendersavedsearch-fortenant-a-nach-systemkontext-nur-a: bestanden — bound(TENANT-A).tenderSavedSearch.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 1 Zeile(n) aus ["TENANT-A"]
```
Die #27-Form unter Systemkontext (der Pfad von `getAllActiveConfigs()`):
```
ldapconfig-systemkontext-include-fieldmappings-beider-mandanten: bestanden — system.ldapConfig.findMany({ where: { isActive: true }, include: { fieldMappings: true } }) liefert 2 Zeile(n): ["TENANT-A:1","TENANT-B:1"] (Mandant:Anzahl Zuordnungen)
```
Schlusszeile: `Alle 253 Pruefungen bestanden.` (Baseline vor diesem Lauf: 203; nach Aufgabe 1: 216).
`pg_policies` der LEBENDEN Datenbank nach `prisma migrate deploy` (36
Migrationen, `pg_proc` kennt `is_system_context`, 34 Regeln gesamt, alle
PERMISSIVE; Form Tabelle#Regelname#Befehl#USING#WITH CHECK, beide Regeln je
Tabelle):
```
DkvModuleConfig#system_read_policy#SELECT#is_system_context()#
DkvModuleConfig#tenant_isolation_policy#ALL#("tenantId" = current_tenant_id())#
LdapConfig#system_read_policy#SELECT#is_system_context()#
LdapConfig#tenant_isolation_policy#ALL#("tenantId" = current_tenant_id())#
LdapFieldMapping#system_read_policy#SELECT#is_system_context()#
LdapFieldMapping#tenant_isolation_policy#ALL#("ldapConfigId" IN ( SELECT "LdapConfig".id FROM "LdapConfig" WHERE ("LdapConfig"."tenantId" = current_tenant_id())))#
TenderMatch#system_read_policy#SELECT#is_system_context()#
TenderMatch#tenant_isolation_policy#ALL#("tenantId" = current_tenant_id())#
TenderSavedSearch#system_read_policy#SELECT#is_system_context()#
TenderSavedSearch#tenant_isolation_policy#ALL#(("tenantId" = current_tenant_id()) AND ((current_user_id() IS NULL) OR ("userId" = current_user_id())))#
```
Endstand nach Aufgabe 2: Tests 1054 bestanden / 64 Dateien (Baseline
1028 / 62), Typprüfung sauber, Werkzeug 253.
### (y2) Signaltabelle — beide Fehlerrichtungen je Regel
| Fehlerrichtung | Erwartung | Gemessen | Befund |
|---|---|---|---|
| Zu streng: der Systemkontext sähe nichts (ein Systemleser OHNE Regel liefert nach dem Scharfschalten 0 Zeilen und schweigt — T-EYM-04) | Systemkontext liefert beide Mandanten | `<tabelle>-ungebunden-null-zeilen` UND `<tabelle>-systemkontext-sieht-beide-mandanten` als Paar (fünf Tabellen), dazu `ldapconfig-systemkontext-include-fieldmappings-beider-mandanten` | NICHT der Fall — jede der fünf Tabellen ist geöffnet; Rückbau (b) unten zeigt, dass das Werkzeug der Datei folgt |
| Zu locker: der Systemkontext könnte schreiben (T-EYM-02) | INSERT/UPDATE/DELETE scheitern an der Mandantenregel | `<tabelle>-systemkontext-insert-abgewiesen-42501`, `…-updatemany-count-0`, `…-deletemany-count-0` (fünf Tabellen) | NICHT der Fall — die Regel ist `FOR SELECT`; Rückbau (a): `FOR SELECT` entfernt → der Insert GELINGT, `cmd` wird `ALL` |
| Zu locker: ein Anfrageweg ruft `forSystem` (T-EYM-01) | Spec rot | `FORSYSTEM_ALLOWED_CALL_SITES` mit exakter Zahl je Datei; Rückbau (d): Zahl 0 → zwei Zusicherungen rot, Fremddatei → drei rot | Wachhund greift; keine Ausnahmeliste für die Zuweisungsform |
| Erben: eine Verbindung trägt `app.system_context` in eine spätere `forTenant`-Abfrage (T-EYM-03) | `forTenant(A)` nach `forSystem` sieht nur A; `is_system_context()` unter `forTenant` ist false | `<tabelle>-fortenant-a-nach-systemkontext-nur-a`, `<tabelle>-is-system-context-unter-fortenant-false` (fünf Tabellen) | NICHT der Fall — `local=true` (erstes Netz) UND ausdrücklicher Reset (zweites Netz); Rückbau (c) unten belegt, dass der Reset allein trägt |
**Rückbau (a)** — in der Migrationsdatei bei `"TenderMatch"` das `FOR SELECT`
entfernt (Regel wird `ALL`), Werkzeug:
```
tendermatch-systemkontext-insert-abgewiesen-42501: FEHLGESCHLAGEN — system.tenderMatch.create({"id":"tm-system-schreibversuch","tenderId":"tender-2","savedSearchId":"ss-a","userId":"user-a","tenantId":"TENANT-A"}) ist NICHT fehlgeschlagen — angelegt: "tm-system-schreibversuch"
tendermatch-systemkontext-updatemany-count-0: FEHLGESCHLAGEN — system.tenderMatch.updateMany({ where: {}, data: {"notifiedChannel":"SYSTEM-SCHREIBVERSUCH"} }) liefert count=3
tendermatch-systemkontext-deletemany-count-0: FEHLGESCHLAGEN — system.tenderMatch.deleteMany({}) liefert count=3; Zeilen danach (Wartungsrolle): 0
tendermatch-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).tenderMatch.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 0 Zeile(n) aus []
tendermatch-pg-policies-genau-eine-system-read-policy-select: FEHLGESCHLAGEN — pg_policies fuer "TenderMatch" (system_read_policy): [{"policyname":"system_read_policy","cmd":"ALL","permissive":"PERMISSIVE","qual":"is_system_context()"}]
5 von 253 Pruefungen fehlgeschlagen.
```
**Rückbau (b)** — die `system_read_policy` für `"TenderSavedSearch"` aus der
Migrationsdatei entfernt. Die innere Routine bricht für diese Tabelle mit
einer eigenen roten Kennung ab (Muster `runSingleRulePersonalTableCheck`:
nicht raten, wenn die Regel fehlt) — deshalb 245 statt 253 Prüfungen, nicht
die im Plan erwarteten zwei roten Kennungen `…-sieht-beide-mandanten`/`…-pg-policies-…`;
die lebende Datenbank blieb währenddessen bei 34 Regeln und einer
`system_read_policy` auf TenderSavedSearch (per `pg_policies` gelesen):
```
tendersavedsearch-system-read-policy-aus-migration-gefunden: FEHLGESCHLAGEN — CREATE POLICY system_read_policy ON "TenderSavedSearch" nicht in der Systemkontext-Migration (20260914120000) gefunden
1 von 245 Pruefungen fehlgeschlagen.
```
**Rückbau (c)** — im Werkzeug `forSystemQuery`/`buildInlineSystemClient` auf
`set_config(…, false)` gestellt: `Alle 253 Pruefungen bestanden.` — alle
`…-fortenant-a-nach-systemkontext-nur-a` BLEIBEN grün, weil der Reset in
`buildInlineExtendedClient` trägt. Zusätzlich den Reset dort entfernt:
```
dkvmoduleconfig-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).dkvModuleConfig.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 2 Zeile(n) aus ["TENANT-A","TENANT-B"]
ldapconfig-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).ldapConfig.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 2 Zeile(n) aus ["TENANT-A","TENANT-B"]
ldapfieldmapping-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).ldapFieldMapping.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 2 Zeile(n) aus ["TENANT-A","TENANT-B"]
tendermatch-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).tenderMatch.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 2 Zeile(n) aus ["TENANT-A","TENANT-B"]
tendersavedsearch-fortenant-a-nach-systemkontext-nur-a: FEHLGESCHLAGEN — bound(TENANT-A).tenderSavedSearch.findMany() unmittelbar nach system.findMany() auf demselben Client liefert 2 Zeile(n) aus ["TENANT-A","TENANT-B"]
5 von 253 Pruefungen fehlgeschlagen.
```
**Rückbau (d)** — Detektor, Zahl für `tender-matching.service.ts` auf 0:
```
AssertionError: apps/api/src/tenders/tender-matching.service.ts: gemessen 1 forSystem(-Aufruf(e), erlaubt sind genau 0: expected [ Array(1) ] to deeply equal []
AssertionError: apps/api/src/tenders/tender-matching.service.ts: Erlaubnisliste nennt 0, gemessen 1 — der Eintrag ist ueberholt: expected [ Array(1) ] to deeply equal []
```
Fremddatei `admin-seed.service.ts` vorübergehend mit `forSystem(` versehen:
```
AssertionError: apps/api/src/user/admin-seed.service.ts: 2 forSystem(-Aufruf(e), Datei steht NICHT in FORSYSTEM_ALLOWED_CALL_SITES — ein Anfrageweg darf den Systemkontext nie rufen: expected [ Array(1) ] to deeply equal []
AssertionError: apps/api/src/user/admin-seed.service.ts: 1 forSystem(-Aufruf(e) ausserhalb der Zuweisungsform: expected [ Array(1) ] to deeply equal []
AssertionError: apps/api/src/user/admin-seed.service.ts: 1 include:/select:/_count:-Angabe(n) ausserhalb eines erkannten Modellaufrufs: expected [ Array(1) ] to deeply equal []
```
Alle Rückbauten zurückgenommen (`git checkout` bzw. Kopie mit gleichem
Hash), Werkzeug danach erneut 253, Detektor 30/30.
### (y3) Welcher Code Leere als Abwesenheit deutet
Die Frage aus dem Auftrag (`docs/mandantentrennung-etappe3-auftrag.md`, 3c):
deutet einer der sechs Pfade eine LEERE Systemkontext-Antwort als "es gibt
nichts" und löscht oder deaktiviert daraufhin? Je Pfad mit Datei und Stelle:
- **dkv-scheduler** (`apps/api/src/dkv/dkv-scheduler.service.ts`,
`onModuleInit`): leere Liste → Protokollzeile `no active config found`,
`return` — kein Auftrag, nichts gelöscht, nichts deaktiviert.
- **ldap-sync.scheduler** (`apps/api/src/ldap/ldap-sync.scheduler.ts`, ruft
`getAllActiveConfigs()`): leere Liste → kein Sync-Lauf. Der gefährliche
Löschzweig in `apps/api/src/ldap/ldap.service.ts` (Deaktivieren/Entfernen
nicht mehr im Verzeichnis gefundener Nutzer) liegt INNERHALB eines je
Mandant gebundenen Sync-Laufs (`forTenant(this.prisma, tenantId)`), den
eine leere Konfigurationsliste gar nicht erst startet — Leere auf der
Systemkontext-Ebene erreicht diesen Zweig strukturell nicht.
- **ldap onApplicationBootstrap** (`ldap-config.service.ts`): leere Liste →
`legacy.length === 0` → `return` — die Nachverschlüsselung ist Nichtstun.
- **tender-digest** (`tender-digest.scheduler.ts`, `runDigest`): leere
Kandidatenliste → `if (!candidates.length) return;` — kein Versand,
`notifiedAt` bleibt NULL (wiederholbar).
- **tender-matching** (`tender-matching.service.ts`, `matchDelta`): leere
Profilliste → die Schleife läuft nicht, keine Treffer, keine Sofortmeldung.
- **admin-seed** (`apps/api/src/user/admin-seed.service.ts`): leere
Mandantenliste → keine Reparatur; `Tenant` trägt keine Regel, ein
Systemkontext ist dort gar nicht nötig (gemessen: einziger Lesezugriff
außerhalb der Schleife ist `tenant.findMany`).
Fazit: KEIN Pfad löscht oder deaktiviert auf Leere. Die verbleibende Gefahr
war das STUMME Nichtstun nach dem Scharfschalten — genau die schließt die
Systemleseregel (Paar `…-ungebunden-null-zeilen` / `…-sieht-beide-mandanten`).
### (y4) Was dieser Durchlauf bewusst nicht löst
- Der Single-Flight-Riegel `processing` in `DkvService.processInbox` ist EIN
prozessweites Boolean, nicht je Mandant. Seit je aktivem Mandanten ein
eigener Cron-Auftrag läuft, können sich zwei Ticks verschiedener Mandanten
überschneiden — der zweite bricht still ab und wartet bis zum nächsten
Intervall (Verzögerung, kein Datenverlust; mit einem Mandanten
unverändert). Neuer WINDOWS-Eintrag (#37) mit Lösungsweg (Riegel je
Mandant, `Set<tenantId>`, Test "zwei Mandanten gleichzeitig, beide werden
bedient"). Der Tick bleibt in diesem Durchlauf unangetastet (Auftrag).
- `sendWelcomeEmail` hat weiterhin null Aufrufer; `@nestjs-modules/mailer`
bleibt in `package.json`/Lockfile installiert, ist aber unbenutzt —
Aufräumen, kein Defekt, kein Lockfile-Eingriff in diesem Durchlauf.
- Der Sonderfall "Nutzer mit Treffern unter zwei Mandanten" im Digest
(`distinct: ['userId']` liefert nur eine tenantId je Nutzer) bleibt wie in
(t4) beschrieben ungelöst.
- `rls-preflight.mjs` bekommt in Etappe 4 eine Prüfung
`mit-systemkontext-sichtbar` — hier nicht gebaut, weil das Werkzeug unter
der Wegwerf-Rolle dasselbe bereits misst (`…-sieht-beide-mandanten`).
### (y5) Was dieser Durchlauf bewusst nicht anfasst
- Der Schalter (`DATABASE_URL` → Rolle `tessera`, BYPASSRLS) — bleibt AUS;
Compose-/Umgebungsdateien unangetastet (Gate gegen `5e0e408` in jeder
Aufgabe).
- `schema.prisma`, bestehende Migrationen (Prüfsumme), `package.json`,
Lockfile.
- Die drei SECURITY-DEFINER-Anmeldefunktionen — unangetastet.
- `admin-seed.service.ts` — unverändert (nur dokumentiert, siehe (y3)).
- `rls-preflight.mjs` — `ohne-kontext-leer` bleibt gültig, weil
`is_system_context()` ohne Variable false ist (`is-system-context-ungesetzt-false`).
- Der Tick `DkvService.processInbox` (je Mandant gebunden seit 260909-mir)
und die 3b-Regeln der zehn persönlichen Tabellen.
## Etappe 2 — Abschluss
Etappe 2 der Mandantentrennung ist mit diesem Lauf (260911-gwh) vollständig:
@@ -3353,8 +3589,17 @@ jetzt erfüllte Befund-K-Bedingung, die entfällt):
- Das Verstummen des Mail-Startpfads (dieser Lauf, (s4)(a)) — EIGENES
Signal, NICHT an #21 angeschlossen.
**Nachtrag (260914-eym):** Etappe 3c ist abgeschlossen — Systemkontext für
die Hintergrunddienste (`## Systemkontext (Etappe 3c, 260914-eym)`); die
beiden Signale #21 (DKV-Planer) und #30 (Mail-Startpfad) aus dieser Liste
sind geschlossen, das eine durch Auftrag je Mandant über den Systemkontext,
das andere durch Entfernen des Startpfads. Die übrigen Vorher-Prüfungen für
Etappe 4 bleiben wie oben; hinzu kommt `mit-systemkontext-sichtbar` (siehe
(y4)).
## Verweis
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang
bereits vollzogen hat (Stand-Spalte `gebunden`/`ungebunden`/`gemischt`),
steht in `docs/mandantentrennung-zugriffsklassifikation.md`.
steht in `docs/mandantentrennung-zugriffsklassifikation.md` (seit 260914-eym
mit dem vierten Stand-Wert `system-gebunden`).