feat(quick-260909-mir): dkv-Fehlerform messen und Kritikschrift erweitern

- rls-scratch-check.mjs: runDkvAreaChecks() misst die drei ausgelieferten
  Policies (DkvInvoiceHistory/DkvModuleConfig/DkvVehicleMaster) wortgleich
  aus der Migration, plus die Nebenlaeufigkeitsform von getHistory()
  (Promise.all ueber zwei gebundene Einzelabfragen); alle 9 neuen plus
  32 bestehende Pruefungen bestehen (41 gesamt)
- Belegt Befund H (kein P2002-Fall, Mandant ist Teil des zusammengesetzten
  Schluessels) und Befund I (gebundenes INSERT mit fremder tenantId wird
  ohne eigene WITH-CHECK-Klausel trotzdem abgewiesen) an der echten
  Datenbank statt am Policy-Text
- docs/mandantentrennung-etappe2-fehlerrichtung.md: neuer Abschnitt
  "Bereich dkv" mit der dritten Fehlerform der Etappe (ein Einzelobjekt
  wird null, wo null bereits "nicht eingerichtet" bedeutet), der
  Signaltabelle je umzustellendem Pfad, den sieben Stellen aus Befund K
  (zerstoerend/lautlos/irrefuehrend) und der ausgeschriebenen
  Planer-Entscheidung (Form c, mit Unsymmetrie zum ldap-Praezedenzfall)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
This commit is contained in:
2026-09-09 16:38:57 +02:00
parent 748f0b513e
commit 761e5e2c36
2 changed files with 568 additions and 0 deletions
@@ -617,6 +617,301 @@ nennen — die Datei selbst steht ohnehin auf der Nicht-Anfassen-Liste, hier
nur festgehalten, damit niemand sie beim Nachziehen der Begründungen
"freundlich kommentiert" und den Lauf rot macht.
## Bereich dkv
Dieser Abschnitt erweitert die Kritikschrift um den Bereich `dkv`
(Quick-Task 260909-mir) und beschreibt ihn zum Zeitpunkt seiner Umstellung.
Die Leitfrage aus Abschnitt (a) gilt unverändert weiter — dieser Abschnitt
beantwortet sie erneut, für einen Bereich mit einer dritten, eigenen
Fehlerform: nicht "eine Liste ist leer" (wie bei `ldap`/`groups`) und nicht
"ein Benachrichtigungsweg handelt gar nicht" (wie bei `tenders`), sondern
"ein einzelnes Objekt wird `null`, und `null` hat an dieser Stelle bereits
eine gültige, harmlose Bedeutung". Ein eingerichtetes Modul sieht danach
aus wie ein nie eingerichtetes.
### (d1) Die Messung
Aufgabe 1 hat `apps/api/scripts/rls-scratch-check.mjs` um einen sechsten
Abschnitt (`runDkvAreaChecks`) erweitert, mit den drei Policies für
`DkvInvoiceHistory`, `DkvModuleConfig` und `DkvVehicleMaster` (alle aus der
ausgelieferten Migration `20260909140000_rls_remaining_tenant_tables`)
WORTGLEICH extrahiert, nicht im Werkzeug nachgetippt. Tatsächlich
beobachtete Ausgabe dieses Laufs (2026-09-09, gegen `tessera-ctl-db-1`,
Adresse `172.19.0.2`):
```
dkvmoduleconfig-gebunden-nur-eigene-zeile: bestanden — forTenant(TENANT-A) liefert 1 Zeile(n): ["TENANT-A"]
dkvmoduleconfig-ungebunden-null-zeilen: bestanden — ungebundener SELECT auf "DkvModuleConfig" liefert 0 Zeile(n)
dkvmoduleconfig-ungebundene-einzelabfrage-liefert-keine-zeile: bestanden — ungebundenes SELECT ... LIMIT 1 ohne jede Bedingung liefert 0 Zeile(n), obwohl 2 existieren — die Form, die der Planer-Startpfad heute benutzt: aus einer beliebigen-aber-vorhandenen Zeile wird KEINE Zeile, und der aufrufende Code liest das als "dieses Modul ist nicht eingerichtet"
dkvinvoicehistory-gebunden-nur-eigener-mandant: bestanden — forTenant(TENANT-A) liefert 1 Zeile(n): ["TENANT-A"]
dkvvehiclemaster-gebunden-nur-eigener-mandant: bestanden — forTenant(TENANT-A) liefert 2 Zeile(n): ["TENANT-A","TENANT-A"]
dkvvehiclemaster-gebundenes-einfuegen-fremder-mandant-abgelehnt: bestanden — gebundenes INSERT unter TENANT-A mit tenantId=TENANT-B abgewiesen: ERROR: new row violates row-level security policy for table "DkvVehicleMaster"
dkvvehiclemaster-gebundenes-update-ueber-kennung-allein-trifft-null-zeilen: bestanden — gebundenes UPDATE unter TENANT-A ueber die Kennung 'veh-b1' (gehoert TENANT-B) allein betrifft 0 Zeile(n) — Folge fuer Aufgabe 3 (Befund G): die vorgeschaltete Besitzpruefung bleibt deshalb erhalten und wird nicht durch die Datenbank ersetzt
dkvvehiclemaster-schluessel-traegt-mandant-keine-fremdkollision: bestanden — gebundenes INSERT unter TENANT-A auf das bereits unter TENANT-B vorhandene Kennzeichen 'B-ONLY-1' gelingt — der Mandant ist Teil des zusammengesetzten Schluessels, keine Kollision auf einer unsichtbaren fremden Zeile, keine P2002-Uebersetzung noetig
dkv-zwei-parallele-gebundene-einzelabfragen-je-eigener-kontext: bestanden — TENANT-A: pid=286680, t="TENANT-A", rows=1; TENANT-B: pid=286679, t="TENANT-B", rows=1 — Nebenlaeufigkeitsform von getHistory(): zwei ueber Promise.all gleichzeitig gestartete gebundene Einzelabfragen ueber denselben Klienten, jede unter ihrem eigenen Kontext
Alle 41 Pruefungen bestanden.
```
Die Belegzeile, die diesen Abschnitt der Kritikschrift trägt, ist
`dkvmoduleconfig-ungebunden-null-zeilen`: der IDENTISCHE `SELECT "tenantId"
FROM "DkvModuleConfig"` ohne vorheriges `set_config` liefert **0 Zeilen**,
nicht etwa die 2 tatsächlich vorhandenen — an der echten, ausgelieferten
Policy gemessen, nicht an einer im Werkzeug nachgebauten Hilfstabelle.
Daneben trägt `dkvmoduleconfig-ungebundene-einzelabfrage-liefert-keine-zeile`
diesen Abschnitt zusätzlich, weil dieser Bereich an der entscheidenden
Stelle kein Mengenergebnis liest, sondern ein Einzelobjekt: dieselbe
Tabelle, dasselbe fehlende `set_config`, aber diesmal ein `SELECT ...
LIMIT 1` ohne jede Bedingung — exakt die Form, die `loadConfig()` ohne
Mandant (der Planer-Startpfad) heute über `findFirst()` benutzt. Auch
diese Abfrage liefert **0 Zeilen**, obwohl 2 existieren. Der Unterschied
zur ersten Belegzeile ist nicht die Zahl (beide sind 0), sondern die
Lesart: ein leeres `findMany`-Ergebnis ist im aufrufenden Code sichtbar
leer, ein leeres `findFirst`-Ergebnis wird zu `null`, und `null` hat in
`loadConfig()`/`onModuleInit()` bereits eine gültige, harmlose Bedeutung
("kein aktives Modul konfiguriert") — siehe (d3).
TEIL 2 hat zusätzlich die Nebenläufigkeitsform gemessen, auf die sich
`getHistory()` stützt (Befund C): zwei über `Promise.all` gleichzeitig
gestartete gebundene Einzelabfragen über DENSELBEN Klienten, hier für zwei
verschiedene Mandanten nachgebaut
(`dkv-zwei-parallele-gebundene-einzelabfragen-je-eigener-kontext`). Jede
Abfrage sah den Kontext, unter dem sie gestartet wurde (`TENANT-A`/
`TENANT-B`), und jede lieferte die richtige Zeilenzahl (je 1) — keine
Verletzung, kein Abbruch.
TEIL 3 hat nachgemessen, dass dieser Bereich keine mandantengebundene
Transaktion enthält (Befund C):
`grep -rn '\$transaction(' apps/api/src/dkv --include=*.ts | grep -v spec`
liefert **null Treffer** (Rückgabewert 1, keine Ausgabe). Der im Kopf von
`prisma-tenant.extension.ts` verlangte erneute Test vor jedem neuen
`forTenant()`-Fall mit eigener Transaktion ist damit für diesen Bereich
beantwortet: es fällt kein neuer Fall an, `withTenantTransaction()` wird
hier nicht gebraucht und in Aufgabe 2/3 nicht eingeführt. Die beiden
mehrschrittigen Stellen des Bereichs — der Ersetzen-Modus des
Fahrzeug-Imports (`deleteMany` gefolgt von `createMany`) und die
Zugangsdaten-Erhaltung in `saveConfig` (lesen, entschlüsseln, neu
verschlüsseln, schreiben) — bleiben deshalb so unatomar wie heute; sie in
eine Transaktion zu heben wäre eine Verhaltensänderung jenseits dieses
Auftrags, siehe (d5).
Zwei Messungen dieses Laufs tragen eine Entscheidung, die kein bisheriger
Bereich in dieser Form brauchte:
- `dkvvehiclemaster-gebundenes-einfuegen-fremder-mandant-abgelehnt`
(Befund I) — dieselbe Frage wie bei `TenderRssFeedSource` in `tenders`,
hier mit demselben Ergebnis: die ausgelieferten Policies dieses Bereichs
tragen keine eigene `WITH CHECK`-Klausel, also verwendet PostgreSQL
denselben `USING`-Ausdruck auch für neu geschriebene Zeilen — ein
gebundenes `INSERT` unter TENANT-A mit `tenantId = TENANT-B` wird
abgewiesen. Was PostgreSQL daraus für ein `INSERT` ableitet, ist eine
Eigenschaft der Datenbank, keine des Policy-Textes, deshalb gemessen und
nicht aus dem Text geschlossen.
- `dkvvehiclemaster-schluessel-traegt-mandant-keine-fremdkollision`
(Befund H) — der Gegenbefund zu `tenders`-Befund F: weil
`DkvVehicleMaster` die zusammengesetzte Eindeutigkeit `@@unique([tenantId,
kennzeichen])` trägt, gibt es hier KEINE Kollision auf einer unsichtbaren
fremden Zeile. Ein gebundenes `INSERT` unter TENANT-A auf ein
Kennzeichen, das unter TENANT-B bereits existiert, GELINGT — das
bestandene Ergebnis ist das Gelingen, nicht die Abweisung. Aufgabe 2/3
bauen deshalb keine P2002-Übersetzung für diesen Bereich; anders als bei
`TenderTriage` in `tenders` ist hier keine gebaut, weil keine gebraucht
wird — nachgemessen statt unterstellt.
### (d2) Signaltabelle je umgestelltem Pfad
| Pfad | Verhalten bei zu wenig Ergebnis | Konkretes Signal |
|---|---|---|
| `DkvService.getConfigForApi` | Der gebundene erste Lesezugriff (`loadConfig(tenantId)`) liefert `null` statt der eigenen Konfiguration | `GET /dkv/config` liefert `404 DKV module not yet configured`; die Oberfläche zeigt das Einrichtungsformular für ein Modul, das tatsächlich eingerichtet ist |
| `DkvService.getConfigForApi`, der zweite (rohe) Lesezugriff auf die Zugangsdaten | Läuft dieser gebundene `findUnique` leer (während der erste — sicher ausgewählte — noch träfe), bleibt `raw` `null`, der `try`-Block liefert `hasPassword=false` | Die Oberfläche meldet "kein Passwort hinterlegt" für ein Modul mit tatsächlich hinterlegtem Passwort — ohne Fehlermeldung, siehe (d3) Stelle 4 |
| `DkvService.saveConfig`, die Zugangsdaten-Erhaltung | Der gebundene erhaltende Lesezugriff liefert `null` statt der bestehenden Zeile, der `try/catch` schluckt das | Ein gespeichertes Passwort wird mit dem LEEREN Wert neu verschlüsselt — die zerstoerende Stelle, siehe (d3) Stelle 5 und T-MIR-07 |
| `DkvService.testConnection`, der Rückgriff auf gespeicherte Zugangsdaten | Der gebundene Lesezugriff liefert `null` statt der bestehenden Zeile | Der Verbindungstest schlägt mit einem Anmeldefehler des Postfachs fehl — die Meldung zeigt auf das Postfach, nicht auf die Datenbank |
| `DkvService._runPipeline`, der Konfigurations-Lesezugriff | Der gebundene `findUnique` liefert `null` statt der bestehenden Konfiguration | `_runPipeline` protokolliert `no config for tenant ...` als Warnung und `return`et — die Rechnungsverarbeitung stellt für diesen Mandanten die Arbeit ein, ohne Fehlermeldung |
| `DkvService.listVehicles`/`createVehicle` | Der gebundene Zugriff liefert 0 Zeilen statt der tatsächlich vorhandenen bzw. schreibt nicht | Die Fahrzeugliste ist leer für einen Mandanten mit tatsächlich vorhandenen Fahrzeugen |
| `DkvService.updateVehicle`/`deleteVehicle`, die Besitzprüfung | Der gebundene `findFirst` liefert `null` statt der eigenen Zeile | `PUT`/`DELETE /dkv/vehicles/:id` scheitert mit der vorhandenen `NotFoundException`, obwohl das Fahrzeug existiert — siehe (d2)-Zeile zum neuen Riegel unten für die spiegelbildliche Fehlerform |
| `DkvService.importVehiclesCsv` (Ersetzen-Modus) | Der gebundene `deleteMany` löscht 0 Zeilen statt der tatsächlich vorhandenen (harmlos: dann bleiben Alt-Fahrzeuge stehen, `createMany` legt zusätzlich an) | Nach einem Ersetzen-Import bestehen alte UND neue Fahrzeugzeilen nebeneinander — kein Datenverlust, aber ein Zustand, der als "Ersetzen" nicht mehr stimmt |
| `DkvService.getHistory` | Beide gebundenen Parallelabfragen liefern 0 Zeilen bzw. Zählung 0 statt der tatsächlich vorhandenen | Die Rechnungshistorie-Tabelle ist leer für einen Mandanten mit tatsächlich vorhandener Historie |
| `DkvService._buildExportRows`, der gebündelte Lesezugriff auf die Fahrzeugstammdaten | Der gebundene `findMany` liefert 0 Zeilen statt der tatsächlich vorhandenen | Eine vollständige Ausfuhrdatei OHNE einen einzigen Fahrer entsteht — kein Fehler, keine Warnung, eine Datei, die plausibel aussieht und falsch ist, siehe (d3) Stelle 7 |
| `DkvService.getExportFile`, neuer Riegel (Aufgabe 3, Befund E) | Der gebundene Lesezugriff auf `DkvInvoiceHistory.exportFilename` liefert keinen Treffer, obwohl die Datei existiert und das Namensmuster besteht | `GET /dkv/exports/:filename` liefert `404`, obwohl die Datei auf der Platte liegt — die Absicht der Umstellung: Fehlen und Fremdbesitz kollabieren bewusst zur selben Antwort |
| `loadConfig(tenantId)` ohne Mandant (bewusst ungebunden, Planer-Startpfad) | Betrifft nicht die Bindung selbst — die Methode bindet niemals. Nach dem Scharfschalten liefert dieselbe Abfrage `null` statt einer beliebigen Zeile | `onModuleInit()` protokolliert `DKV scheduler: no active config found — cron job not registered` und richtet für JEDEN Mandanten nichts ein — siehe (d4) |
### (d3) Welcher Code Leere als Abwesenheit deutet
Die Form, die diesen Bereich von `ldap`, `groups` und `tenders`
unterscheidet: nicht "eine Liste ist leer" und nicht "ein
Benachrichtigungsweg handelt gar nicht", sondern "ein einzelnes Objekt
wird `null`, und `null` hat an dieser Stelle bereits eine gültige,
harmlose Bedeutung". Ein eingerichtetes Modul sieht danach aus wie ein nie
eingerichtetes: ein leeres Formular, eine unauffällige Protokollzeile,
kein Alarm.
**Zerstörend (eine Stelle, der gefährlichste Punkt des Bereichs):**
5. `DkvService.saveConfig`, die Erhaltung der nicht ausgefüllten
Zugangsdaten — liest die bestehende Zeile, um Benutzername oder
Passwort zu übernehmen, wenn das Formularfeld leer gelassen wurde.
Läuft dieser Lesezugriff nach dem Scharfschalten leer (weil ungebunden
oder unter falschem Kontext gebunden), wird das Feld mit dem LEEREN
Wert neu verschlüsselt: aus einem gespeicherten Passwort wird ein
leeres. Die Stelle liegt hinter einem `try/catch`, das ausdrücklich
sagt, dass es Fehler ignoriert und mit dem Übergebenen überschreibt —
T-MIR-07 im Bedrohungsregister dieses Plans. Aufgabe 2 bindet Lese- UND
Schreibzugriff dieser Methode gemeinsam an denselben Mandanten, sodass
ein leerer Lesezugriff nicht mit einem erfolgreichen Schreibzugriff
unter einem anderen Kontext kombiniert werden kann.
**Lautlos (drei Stellen, die Rechnungsverarbeitung bekommt eigenen
Raum):**
1. `DkvSchedulerService.onModuleInit()`, gefolgt von
`config?.isActive && config.tenantId` — `null` heißt "kein aktives
Modul konfiguriert", der Planer richtet nichts ein und protokolliert
das als Normalfall (`DKV scheduler: no active config found — cron job
not registered`). Kein Fehler, keine Warnung, keine sichtbare
Änderung — siehe (d4) für die volle Begründung, warum dieser Pfad
bewusst ungebunden bleibt.
2. `DkvService._runPipeline`, `if (!config) { warn; return; }` — `null`
heißt "dieser Mandant hat DKV nicht eingerichtet". Die
Rechnungsverarbeitung stellt die Arbeit ein: Rechnungen laufen im
Postfach weiter auf, es entsteht keine Historienzeile, keine
Ausfuhrdatei, kein Versand — und keine Fehlermeldung. Anders als beim
Planer-Startpfad ist dieser Lesezugriff in Aufgabe 2 vollständig
gebunden; die Stelle bleibt hier festgehalten, weil sie die Folge einer
still verschwundenen Konfiguration ist, nicht weil sie ungebunden
bliebe.
7. `DkvService._buildExportRows`, der gebündelte Lesezugriff auf die
Fahrzeugstammdaten — ein fehlender Treffer je Kennzeichen ist nach D-13
bereits ein GÜLTIGER Zustand (unbekanntes Kennzeichen, leeres
Fahrerfeld). Läuft der Lesezugriff selbst ganz leer (0 Fahrzeuge statt
der tatsächlich vorhandenen), entsteht eine vollständige Ausfuhrdatei
OHNE einen einzigen Fahrer — kein Fehler, keine Warnung, eine Datei,
die plausibel aussieht und falsch ist. Diese Stelle ist der Grund,
warum es nicht genügt, nur die Anzeigepfade zu binden.
**Irreführend (drei Stellen, die auf die falsche Ursache zeigen oder eine
falsche Vergangenheit nahelegen):**
3. `DkvService.getConfigForApi`, `if (!safe) return null` — die
Oberfläche zeigt daraufhin ein leeres Einrichtungsformular. Ein
Administrator sieht "noch nicht eingerichtet" für ein Modul, das
eingerichtet IST — und würde beim Neu-Ausfüllen die vorhandenen
Zugangsdaten überschreiben. Diese Stelle bleibt bewusst unter
"irreführend" und nicht unter "zerstörend": die Zerstörung selbst
passiert erst in `saveConfig` (Stelle 5), falls der Administrator
tatsächlich neu ausfüllt und speichert — hier liegt nur die
irreführende Voraussetzung dafür.
4. `DkvService.getConfigForApi`, der `try/catch` um die Entschlüsselung —
fängt heute Entschlüsselungsfehler ab und liefert einen leeren
Benutzernamen. Nach dem Scharfschalten fällt der Lesezugriff selbst
leer aus, `raw` ist `null`, und der Zweig läuft ohne Fehler durch:
`hasPassword` bleibt `false`. Die Oberfläche meldet "kein Passwort
hinterlegt" für ein hinterlegtes Passwort.
6. `DkvService.testConnection`, der Rückgriff auf das gespeicherte
Passwort — läuft leer, der Test schlägt mit einem Anmeldefehler des
Postfachs fehl. Harmlos in der Richtung, aber irreführend: die Meldung
zeigt auf das Postfach, nicht auf die Datenbank.
**Gegenrichtung, ebenfalls nachgesehen statt geschlossen behauptet:** die
laut werfenden Stellen sind `updateVehicle`/`deleteVehicle`
(`NotFoundException` bei Leere), `importVehiclesCsv` (wirft bei leerem CSV)
und `getExportFile` (wirft bei fehlender Datei, jetzt zusätzlich bei
fehlendem gebundenen Historientreffer, Aufgabe 3). Diese Stellen sind
harmlos, weil ein zu kleines Ergebnis dort bereits heute einen Fehler
auslöst, der nicht mit dem Scharfschalten neu entsteht.
### (d4) Was dieser Durchlauf bewusst nicht löst
**Der Planer-Startpfad — die eigentliche Aufgabe dieses Plans, ausgeschrieben statt still getroffen.**
`DkvSchedulerService.onModuleInit()` ruft `DkvService.loadConfig()` ohne
Mandant auf und übernimmt `config.tenantId` als den einen Mandanten, den
der eine benannte Cron-Auftrag `dkv-inbox-poll` fortan bedient
(`this.dkvService.loadConfig()` → `findFirst()` ganz ohne Bedingung). Zwei
Zustände, beide gehören benannt, sonst liest sich die Markierung wie eine
Entwarnung:
- **Heute** ist die Abfrage bereits FALSCH, nicht nur ungenau: bei mehreren
Mandanten bedient sie einen BELIEBIGEN und die übrigen NIE. Die Prüfung
`config?.isActive && config.tenantId` verschärft das — ist ausgerechnet
die gezogene beliebige Zeile inaktiv, registriert der Planer gar nichts,
obwohl ein zweiter Mandant aktiv wäre.
- **Nach dem Scharfschalten** verstummt sie zusätzlich: dieselbe Abfrage
liefert `null`, der Planer protokolliert `DKV scheduler: no active
config found — cron job not registered` und richtet für JEDEN Mandanten
nichts ein — eine Zeile, die auf einer frischen Installation der
Normalfall ist und deshalb niemanden alarmiert.
Von den drei im Auftrag genannten Formen wurde geprüft:
- **(a) An einen konkret aufgelösten Mandanten binden** — nicht möglich.
`onModuleInit()` hat keine Anfrage, keinen Sitzungsnachweis und keinen
Konfigurationswert, aus dem ein Mandant käme. Einen einzuführen wäre eine
neue Einstellung, also eine Funktionsänderung.
- **(b) Umbau auf einmal-abfragen-viele-bedienen** — abgelehnt, mit
Begründung. Das ist genau die Mehrmandanten-Planung, die 07-04
zurückgestellt hat: alle aktiven Konfigurationen lesen, je Mandant einen
Auftrag führen, deren Lebenszyklus bei jeder Konfigurationsänderung
nachziehen (heute verwaltet `setInterval` GENAU EINEN Auftrag unter
einem festen Namen), und entscheiden, was bei unterschiedlichen
Intervallen je Mandant gilt. Das ist eine Funktion, kein Bindungsumbau.
- **(c) Als benannte Altlast weiterführen, mit Markierung** — GEWÄHLT. Der
unmittelbare Präzedenzfall ist `getAllActiveConfigs` im Bereich `ldap`
(260909-ipc, Befund B): ein bewusst übergreifender Planer-Lesezugriff,
der ungebunden bleibt, einen eigenen Kopfkommentar trägt, und dessen
Verstummen nach dem Scharfschalten an die Vorabprüfung von Etappe 4
übergeben wird.
**Die Unsymmetrie, die dieser Präzedenzfall NICHT deckt:**
`getAllActiveConfigs` ist HEUTE korrekt und verstummt erst später. Der
DKV-Planer ist HEUTE bereits falsch — er bedient bei mehreren Mandanten
einen beliebigen und die übrigen nie — und verstummt zusätzlich später.
Die Markierung in Aufgabe 2 sagt beides, sonst läse sie sich wie eine
Entwarnung. Die gewählte Form hat drei Teile, alle umgesetzt: die
übergreifende Abfrage ist eine EIGENE, benannte Methode (kein Zweig hinter
einem optionalen Parameter), sie und der Planer tragen einen Kopfkommentar,
der beide Zustände benennt, und die Altlast steht als offener Eintrag im
Broken-Windows-Register (siehe SUMMARY dieses Plans für die genaue
Eintragskennung). Das Signal für das Verstummen gehört in die
Vorabprüfung von Etappe 4 (`apps/api/scripts/rls-preflight.mjs`), NICHT in
diesen Durchlauf.
**Die gemeinsame Ablage der Ausfuhrdateien samt Verdrängung über
Mandantengrenzen (Befund F).** `DkvExportService.writeAndPrune` behält die
letzten zehn Dateien des GEMEINSAMEN Verzeichnisses `user-files/`.
Verarbeitet ein Mandant zehn Rechnungen, verdrängt er damit die Dateien
aller anderen; deren Historienzeilen nennen dann einen Dateinamen, der
nicht mehr existiert. Das ist keine Bindungsfrage — es ist die
Ablagestruktur, und sie zu ändern (Unterverzeichnisse je Mandant, Umzug der
Bestandsdateien) ist ein eigener Auftrag. Siehe auch (d5) und T-MIR-08.
**Die Übergaben in die noch nicht umgestellten Bereiche.**
`dkv-mail.service.ts` hängt an `SettingsService.getDecryptedSmtpConfig`
(`settings.service.ts` ist noch vollständig unbound, siehe
`docs/mandantentrennung-zugriffsklassifikation.md`) — dieselbe
Reihenfolgebedingung, die der `tenders`-Durchlauf für dieselbe Abhängigkeit
als Befund K festhielt. `dkv.seed.ts` hängt an `module-registry`, ebenfalls
noch unbound. Nach dem Scharfschalten fände die ungebundene SMTP-Abfrage
keine Zeile mehr — Ergebnis: kein Versand für niemanden, mit Wiederholung
bei jedem Lauf (die Datei bleibt lokal verfügbar, D-16). Reihenfolgebedingung
für Etappe 4, hier festgehalten, nicht gelöst.
**Die offene Architekturfrage `req.tenantPrisma`.** Auch der Bereich `dkv`
entscheidet sie nicht — er bindet dienst-intern, wie `ldap`, `groups` und
`tenders` es vormachen.
### (d5) Was dieser Durchlauf bewusst nicht anfasst
- **Die beiden mehrschrittigen Stellen bleiben unatomar (Befund C, TEIL
3).** Der Ersetzen-Modus des Fahrzeug-Imports (`deleteMany` gefolgt von
`createMany`) und die Zugangsdaten-Erhaltung in `saveConfig` (lesen,
entschlüsseln, neu verschlüsseln, schreiben) werden NICHT in eine
Transaktion gehoben — geprüft und bewusst gelassen, nicht übersehen. Der
im Kopf von `prisma-tenant.extension.ts` verlangte erneute Test ist mit
TEIL 3 dieser Aufgabe beantwortet: kein neuer Transaktionsfall,
`withTenantTransaction()` bleibt für diesen Bereich ungenutzt.
- **Die Ablagestruktur der Ausfuhrdateien bleibt unverändert (Befund F).**
Geprüft und bewusst gelassen — eine Lösung (Unterverzeichnisse je
Mandant, Umzug der Bestandsdateien) ist ein eigener Auftrag, siehe (d4).
## Verweis
Die Bestandsaufnahme, welche Fundstelle den hier beschriebenen Übergang