Files
tessera-ctl/.planning/quick/260914-eym-mandantentrennung-etappe-3c-systemkontex/260914-eym-SUMMARY.md
T
schalli 5f80582a37
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 58s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
docs(quick-260914-eym): Etappe 3c abgeschlossen und verifiziert 9/9 — Zusammenfassung, Verifikation, Aktenstand
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
2026-09-14 12:06:13 +02:00

23 KiB

phase, plan, subsystem, tags, status, requires, provides, affects, tech-stack, key-files, decisions, metrics, actuals, plan_head_before
phase plan subsystem tags status requires provides affects tech-stack key-files decisions metrics actuals plan_head_before
quick-260914-eym 01 mandantentrennung
rls
systemkontext
forSystem
dkv
mail
ldap
tenders
windows-21
windows-30
complete
quick-260911-nke
forSystem
is_system_context
system_read_policy
dkv-auftrag-je-mandant
mail-transport-je-versand
etappe-4
added patterns
Systemkontext-Schwesterhelfer forSystem(prisma)
FOR-SELECT-Systemleseregel
Erlaubnisliste mit exakter Zahl je Datei
Transport je Versand nach Mandant
created modified
apps/api/prisma/migrations/20260914120000_rls_system_context_read/migration.sql
apps/api/src/dkv/dkv-scheduler.service.spec.ts
apps/api/src/mail/mail.service.spec.ts
apps/api/src/prisma/prisma-tenant.extension.ts
apps/api/src/prisma/prisma-tenant.extension.spec.ts
apps/api/src/prisma/rls-access-inventory.spec.ts
apps/api/src/groups/migration-sql.spec.ts
apps/api/scripts/rls-scratch-check.mjs
apps/api/src/dkv/dkv.service.ts
apps/api/src/dkv/dkv.service.spec.ts
apps/api/src/dkv/dkv-scheduler.service.ts
apps/api/src/dkv/dkv.controller.ts
apps/api/src/mail/mail.module.ts
apps/api/src/mail/mail.service.ts
apps/api/src/settings/settings.service.ts
apps/api/src/settings/settings.service.spec.ts
apps/api/src/auth/auth.service.ts
apps/api/src/auth/auth.service.spec.ts
apps/api/src/ldap/ldap-config.service.ts
apps/api/src/ldap/ldap-config.service.spec.ts
apps/api/src/tenders/tender-digest.scheduler.ts
apps/api/src/tenders/tender-digest.scheduler.spec.ts
apps/api/src/tenders/tender-matching.service.ts
apps/api/src/tenders/tender-matching.service.spec.ts
apps/api/src/tenders/tender-notifications.integration.spec.ts
docs/mandantentrennung-zugriffsklassifikation.md
docs/mandantentrennung-etappe2-fehlerrichtung.md
docs/mandantentrennung-etappe3-auftrag.md
docs/mandantentrennung-datenbankrolle.md
.planning/WINDOWS.md
forSystem(prisma) als Schwesterhelfer statt viertem Parameter — eigene Zugriffsklasse, eigene Erkennungsform im Detektor (Umkehrung der 3b-Begruendung)
system_read_policy FOR SELECT auf genau fuenf Tabellen; SmtpConfig bekommt keine, weil der Mail-Startpfad entfernt statt umgestellt wurde
DKV-Planer: Auftrag je Mandant (promote, nicht add-alongside) — Einzahl-Feld activeTenantId ersatzlos entfernt
Mail: Transport je Versand nach Mandant des Empfaengers; Umgebungs-Kette nur Rueckfall fuer Mandanten ohne SmtpConfig
admin-seed nur dokumentiert (liest ausserhalb der Schleife nur Tenant, keine Regel) — Datei unveraendert
Single-Flight-Riegel processInbox bleibt prozessweit — WINDOWS #37 statt Umbau (Auftrag: Tick unangetastet)
duration completed
1 Sitzung (2026-09-14, ca. 11:10-12:05) 2026-09-14
tokens tasks commits
58868 3 3
02016e19eb

Quick 260914-eym: Mandantentrennung Etappe 3c — Systemkontext fuer die Hintergrunddienste — Summary

Benannter Systemkontext forSystem(prisma) mit is_system_context() und je einer nur lesenden system_read_policy FOR SELECT auf fuenf Tabellen; alle sechs Hintergrunddienst-Faelle behandelt (DKV-Planer je Mandant, Mail-Transport je Versand mit entferntem Startpfad, ldap/digest/matching ueber den Systemkontext, admin-seed dokumentiert); Detektor mit fuenfter Erkennungsform und falsifizierbarer Erlaubnisliste; Werkzeug 203 -> 253; WINDOWS #21 und #30 geschlossen, #37 neu. Der Schalter bleibt AUS.

Commits

939c812 docs(quick-260914-eym): Etappe 3c abgeschlossen — Kritikschrift, Klassifikation, Auftrag, Datenbankrolle, WINDOWS #21/#30 geschlossen, Single-Flight-Riegel als Eintrag
6e2a641 feat(quick-260914-eym): Mail-Transport je Versand nach Mandant (WINDOWS #30), ldap/digest/matching ueber Systemkontext, vier Tabellen im Werkzeug, Erlaubnisliste vollstaendig
3d64567 feat(quick-260914-eym): forSystem(), is_system_context(), Systemleseregel auf fuenf Tabellen, DKV-Planer je Mandant — ein Pfad (WINDOWS #21)

git log --oneline 02016e1..HEAD (oben, drei Commits). git rev-list --count 02016e1..HEAD = 3. Gepusht: git push -> 5e0e408..939c812 main -> main; git fetch -q && git status -sb | head -1 -> ## main...origin/main; git rev-parse HEAD == git rev-parse origin/main.

git status --porcelain vor dem Schreiben dieser SUMMARY: leer (keine Ausgabe).

Hinweis zum Branch: das Projekt committet seit jeher direkt auf main (alle Quick-Tasks, Plan-Gate HEAD == origin/main, Auftrag "plain git push") — dem Projekt-Workflow gefolgt; git.allow_default_branch_commits ist in .planning/config.json nicht gesetzt.

Gemessene Zahlen (beobachtet, nicht abgeschrieben)

Messpunkt Baseline (HEAD 5e0e408/02016e1) nach Aufgabe 1 (3d64567) nach Aufgabe 2 (6e2a641) Ende (939c812)
npm --prefix apps/api run test — Test Files 62 63 64 64
Tests 1028 1051 1054 1054
npm --prefix apps/api run type-check Exit 0 0 0 0
rls-scratch-check.mjs Schlusszeile Alle 203 Pruefungen bestanden. Alle 216 Pruefungen bestanden. Alle 253 Pruefungen bestanden. 253 (kein apps-Diff seit Aufgabe 2, Gate git diff --stat HEAD~1 -- apps leer)
prisma migrate status 35 Migrationen, up to date 36 Migrationen, up to date 36 36
pg_proc is_system_context 0 1 1 1
pg_policies public gesamt / system_read_policy 29 / 0 34 / 5 34 / 5 34 / 5
git diff --stat 5e0e408 -- . ':!.planning' Dateien — — — 29 (29 files changed, 2499 insertions(+), 491 deletions(-))
Ledger (aus Zeilen gezaehlt) open 16 / waived 1 / fixed 19 / total 36 — — open 15 / waived 1 / fixed 21 / total 37 (Frontmatter identisch)

Plan-Erwartung vs. beobachtet: Tests erwartet >= 1040 / >= 1044, beobachtet 1051 / 1054; Werkzeug erwartet >= 216 / >= 250 (abgeleitet 216 / 253), beobachtet exakt 216 / 253; Uebersichtstabelle erwartet 61/179/5 (tenders 33/27/2, ldap 1/27/2, dkv 0/22/1, settings 0/3/0), mit der Gate-Schleife nachgerechnet: identisch.

Umgebung: Container tessera-ctl-db-1 lief beim Einstieg bereits (Up 25 minutes (healthy), vom Planer gestartet); IP per docker inspect 172.19.0.2; DB-Zugang tessera:tessera_dev; Prisma-Binary apps/api/node_modules/.bin/prisma. Schalter-Gate: git diff --name-only 5e0e408 nennt keine Compose-, .env-, schema.prisma-, package.json-, Lockfile-, rls-preflight.mjs- oder admin-seed.service.ts-Datei (in jedem der drei Gates geprueft).

[BLOCKING] Migration lokal angewendet — woertliche Ausgabe

cd apps/api && DATABASE_URL="postgresql://tessera:tessera_dev@172.19.0.2:5432/tessera" ./node_modules/.bin/prisma migrate deploy:

The following migration(s) have been applied:

migrations/
  └─ 20260914120000_rls_system_context_read/
    └─ migration.sql

All migrations have been successfully applied.

migrate status: 36 migrations found in prisma/migrations / Database schema is up to date!

pg_proc / pg_policies (Tabelle#Regelname#Befehl#PERMISSIV#USING#WITH CHECK):

pg_proc is_system_context = 1
DkvModuleConfig#system_read_policy#SELECT#PERMISSIVE#is_system_context()#
DkvModuleConfig#tenant_isolation_policy#ALL#PERMISSIVE#("tenantId" = current_tenant_id())#
LdapConfig#system_read_policy#SELECT#PERMISSIVE#is_system_context()#
LdapConfig#tenant_isolation_policy#ALL#PERMISSIVE#("tenantId" = current_tenant_id())#
LdapFieldMapping#system_read_policy#SELECT#PERMISSIVE#is_system_context()#
LdapFieldMapping#tenant_isolation_policy#ALL#PERMISSIVE#("ldapConfigId" IN ( SELECT "LdapConfig".id FROM "LdapConfig" WHERE ("LdapConfig"."tenantId" = current_tenant_id())))#
SmtpConfig#tenant_isolation_policy#ALL#PERMISSIVE#("tenantId" = current_tenant_id())#
TenderMatch#system_read_policy#SELECT#PERMISSIVE#is_system_context()#
TenderMatch#tenant_isolation_policy#ALL#PERMISSIVE#("tenantId" = current_tenant_id())#
TenderSavedSearch#system_read_policy#SELECT#PERMISSIVE#is_system_context()#
TenderSavedSearch#tenant_isolation_policy#ALL#PERMISSIVE#(("tenantId" = current_tenant_id()) AND ((current_user_id() IS NULL) OR ("userId" = current_user_id())))#
system_read_policy gesamt = 5
policies public gesamt = 34

Werkzeug — die vier Funktionsfaelle (woertlich, Lauf nach Aufgabe 2)

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 (dkvmoduleconfig, ldapconfig, ldapfieldmapping, tendermatch, tendersavedsearch) neun Kennungen gruen, plus ldapconfig-systemkontext-include-fieldmappings-beider-mandanten: bestanden — ... liefert 2 Zeile(n): ["TENANT-A:1","TENANT-B:1"]. Die vollstaendigen Zeilen stehen in docs/mandantentrennung-etappe2-fehlerrichtung.md, Abschnitt (y1).

Falsifizierung durch Rueckbau — woertliche Ausgaben

Jeder Rueckbau wurde ausgefuehrt, das rote Ergebnis protokolliert, die Datei restauriert (git checkout -- <Datei> fuer committete Dateien; fuer die in Aufgabe 2 noch uncommitteten Dateien Werkzeug/Detektor per Kopie mit identischem SHA-256-Praefix a1f845ba787cac52 bzw. d9595ef09df57fce) und der gruene Zustand erneut gemessen (Werkzeug 253, Detektor 30/30, git status --short danach nur die gewollten Aufgabe-2-Dateien).

(a) FOR SELECT bei "TenderMatch" in der Migrationsdatei entfernt (Regel wird ALL):

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.

Plan erwartete: …-insert-abgewiesen-42501 rot. Beobachtet: 5 rot — der Insert gelingt, danach auch updateMany/deleteMany (count 3, Zeilen 0), deshalb liefert der Folgeschritt …-nur-a 0 Zeilen, und pg_policies zeigt ALL. Die Kernaussage (Insert GELINGT ohne FOR SELECT) ist woertlich belegt.

(b) system_read_policy fuer "TenderSavedSearch" aus der Migrationsdatei entfernt:

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.

Lebende Datenbank waehrend des Rueckbaus (per pg_policies): policies public gesamt = 34 | system_read_policy auf TenderSavedSearch = 1. Plan erwartete: …-sieht-beide-mandanten und …-pg-policies-genau-eine-… rot. Beobachtet: die innere Routine bricht fuer diese Tabelle mit einer eigenen roten Extraktions-Kennung ab (Muster runSingleRulePersonalTableCheck: nicht raten, wenn die Regel in der Migration fehlt), die neun Kennungen der Tabelle laufen nicht (253 -> 245). Die Aussage des Plans — das Werkzeug misst die geschnittene Regel, nicht die lebende Datenbank, und die Datenbank bleibt bei 34 — ist belegt; die Form des Rotwerdens ist strenger als erwartet, nicht lockerer. Beide Zahlen (erwartet 2 rote Kennungen von 253; beobachtet 1 rote von 245) stehen hier und in (y2).

(c) local=false in forSystemQuery/buildInlineSystemClient (Werkzeug): Alle 253 Pruefungen bestanden. — alle fuenf …-fortenant-a-nach-systemkontext-nur-a bleiben gruen, der Reset in buildInlineExtendedClient traegt. Zusaetzlich 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.

(…-is-system-context-unter-fortenant-false blieb gruen, weil diese Kennung ihre Transaktion mit eigenem Reset-Literal baut, nicht ueber buildInlineExtendedClient.)

(d) Detektor — Zahl fuer 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 []
Tests  2 failed | 28 passed (30)

Fremddatei admin-seed.service.ts voruebergehend 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 []
Tests  3 failed | 27 passed (30)

Danach git checkout -- apps/api/src/user/admin-seed.service.ts, git diff --quiet 5e0e408 -- apps/api/src/user/admin-seed.service.ts -> unveraendert.

Ausserdem ein nicht geplanter Beleg fuer den Veraltet-Wachhund: in Aufgabe 1 war die Erlaubnisliste bereits mit dkv.service.ts: 1 gefuellt, bevor dkv.service.ts umgestellt war — die Spec wurde rot (Erlaubnisliste nennt 1, gemessen 0 — der Eintrag ist ueberholt) und erst mit der Umstellung gruen.

Identitaet mit einem Mandanten (morgen alpha, BYPASSRLS) — je Pfad als Test

  • dkv (dkv-scheduler.service.spec.ts, 7 Tests): ein aktiver Mandant, pollIntervalMin 15 -> genau ein Auftrag dkv-inbox-poll:t1, cronTime.source === '*/15 * * * *', isActive true; 120 -> 0 */2 * * *; fireOnTick() ruft processInbox('t1') genau einmal; inaktive/keine Config -> kein Auftrag, Protokollzeile DKV scheduler: no active config found — cron job not registered; zwei Mandanten -> zwei Auftraege, setInterval(30,'t2') laesst das t1-Objekt identisch, stopJob('t1') entfernt nur t1; werfender Startpfad -> DKV scheduler init failed: db down, kein Auftrag; stopJob unbekannt -> No-Op.
  • mail (mail.service.spec.ts, 4 Tests): Mandant MIT SmtpConfig -> getDecryptedSmtpConfig('t1') genau einmal, createTransport({host:'smtp-a.example.invalid',port:465,secure:true,requireTLS:false,auth:{user:'user-a',pass:'geheim-a'}}), from = noreply@a.example.invalid, close() einmal; ohne SmtpConfig -> MAIL_* vor TESSERA_SMTP_* vor localhost:1025, from aus TESSERA_SMTP_FROM bzw. Tessera <tessera@tessera.local>; zwei Mandanten -> zwei Transporte, keiner enthaelt das Kennwort des anderen; sendMail wirft -> kein Throw, Protokoll ohne Kennwort, close() trotzdem.
  • ldap/digest/matching: bestehende Verhaltenstests unveraendert gruen (ldap 92, tenders 413 Tests in den Bereichen), plus je eine Zusicherung forSystem genau einmal und forTenant genauso oft wie bisher (ldap: getAllActiveConfigs forSystem 1 / forTenant 0; Bootstrap leer: forSystem 1 / forTenant 0; Bootstrap mit Altzeile t1: forTenant genau einmal mit t1, update traegt aa11:bb22:<hex>; digest: __systemCallLog genau [tenderMatch.findMany], forTenant weiter genau einmal; matching: __systemCallLog genau [tenderSavedSearch.findMany], tender.findMany weiter auf dem rohen Client).

Deviations from Plan

Auto-fixed Issues

1. [Rule 3 - Blocking] Gate-Zaehlung const systemPrisma = forSystem(this.prisma) traf die Proben der Detektor-Spec

  • Found during: Aufgabe 2, Gate-Lauf
  • Issue: Das Gate zaehlt grep -rh … | grep -v spec — -h laesst den Dateinamen weg, spec steht nicht im Zeilentext, deshalb zaehlten die drei Proben C/D1/D2 mit (8 statt 5). Der Plan verlangt die Probe C mit genau diesem Text UND das Gate mit genau 5 — in sich widerspruechlich.
  • Fix: Empfaengername in den drei Proben auf sysPrisma geaendert (Regex des Detektors ist const\s+(\w+)\s*=\s*forSystem\( — die Probe prueft weiterhin dieselbe Form und belegt zusaetzlich, dass der Name nicht hartkodiert ist); Gate unveraendert, Zahl unveraendert (5).
  • Files modified: apps/api/src/prisma/rls-access-inventory.spec.ts
  • Commit: 6e2a641. Als Falle in docs/mandantentrennung-etappe3-auftrag.md ("Werkzeuge und Fallen") eingetragen.

2. [Rule 3 - Blocking] Kopfkommentar dkv-scheduler.service.ts nannte forSystem() als Text

  • Found during: Aufgabe 2, Gate test 4 -eq <Dateien mit forSystem(>
  • Issue: Das Gate zaehlt Dateien mit dem Text forSystem( auch in Kommentaren; der in Aufgabe 1 geschriebene Kopfkommentar nannte den Helfer.
  • Fix: Kommentar umformuliert ("systemgebunden ueber den Systemkontext-Helfer"). Datei steht in files_modified des Plans (Aufgabe-1-Liste), Aenderung im Aufgabe-2-Commit.
  • Files modified: apps/api/src/dkv/dkv-scheduler.service.ts
  • Commit: 6e2a641

3. [Rule 3 - Blocking] Spec-Kopfkommentar nannte den geloeschten Methodennamen

  • Found during: Aufgabe 2 (eigener Assert vor dem Gate)
  • Issue: Das Gate verlangt null Treffer loadAnySmtpConfigForStartupTransport in vier Dateien, auch in Kommentaren; mein erster Entwurf des Spec-Kopfkommentars nannte ihn.
  • Fix: Umschrieben ("der ungebundene Startpfad des Mailmoduls").
  • Files modified: apps/api/src/settings/settings.service.spec.ts
  • Commit: 6e2a641

4. [Rule 1 - Bug] pg_policies-Form in (y1)

  • Found during: Aufgabe 3, Gate
  • Issue: Ich hatte die Zeilen mit sechs Spalten (inkl. PERMISSIV) geschrieben; das Gate erwartet die 3b-Form Tabelle#Regelname#Befehl#USING#WITH CHECK.
  • Fix: Zeilen auf die 3b-Form gebracht (die PERMISSIV-Eigenschaft steht im Satz davor).
  • Files modified: docs/mandantentrennung-etappe2-fehlerrichtung.md
  • Commit: 939c812

Abweichungen zum Auftrag (bewusst, im Plan so vorgesehen)

  • Fuenf statt sechs Tabellen: SmtpConfig traegt keine system_read_policy, weil der Mail-Startpfad ENTFERNT wurde (Transport je Versand nach Mandant des Empfaengers), nicht auf den Systemkontext umgestellt.
  • ldap hat ZWEI Systemkontext-Leser: getAllActiveConfigs() und die Nachverschluesselung in onApplicationBootstrap() (je eigene Zuweisung, der Detektor zaehlt 2); die Schreibzeile je Altzeile laeuft ueber forTenant(this.prisma, config.tenantId).
  • Mail-Startpfad entfernt statt umgestellt: MailerModule.forRootAsync und loadAnySmtpConfigForStartupTransport() samt vier Spec-Tests geloescht; @nestjs-modules/mailer bleibt in package.json/Lockfile installiert, ist aber unbenutzt (kein Lockfile-Eingriff in diesem Durchlauf).
  • Container: musste NICHT gestartet werden — er lief beim Einstieg bereits (vom Planer gestartet, Up 25 minutes (healthy)).
  • Rueckbau (b): rot in strengerer Form als im Plan beschrieben (Extraktions-Abbruch statt zwei rote Messkennungen), siehe oben.
  • Doppelte Kennung im Werkzeug: dkvmoduleconfig-ungebunden-null-zeilen gibt es jetzt zweimal (einmal aus runDkvAreaChecks, einmal aus dem neuen Abschnitt) — der Plan schreibt den Namen vor; beide gruen, das Gate greift per ^…: bestanden.

Was bewusst offen bleibt

  • WINDOWS #37 (neu, open): Der Single-Flight-Riegel processing in DkvService.processInbox ist EIN prozessweites Boolean. Seit je aktivem Mandanten ein eigener Cron-Auftrag laeuft, bricht bei Ueberschneidung zweier Ticks verschiedener Mandanten der zweite still ab (Warnzeile already processing) und wartet bis zum naechsten Intervall — kein Datenverlust, Verzoegerung; mit einem Mandanten unveraendert (T-EYM-09, accept mit Aufzeichnung). Loesungsweg: Riegel je Mandant (Set<tenantId>) mit Test "zwei Mandanten gleichzeitig, beide werden bedient".
  • sendWelcomeEmail hat weiterhin null Aufrufer; @nestjs-modules/mailer unbenutzt in package.json — Aufraeumen, kein Defekt.
  • Digest-Sonderfall "Nutzer mit Treffern unter zwei Mandanten" (distinct: ['userId']) bleibt wie in (t4) beschrieben.
  • rls-preflight.mjs bekommt in Etappe 4 die Pruefung mit-systemkontext-sichtbar; ohne-kontext-leer bleibt gueltig (Beleg is-system-context-ungesetzt-false, Rohwert null -> false).
  • Etappe 3a (Anmeldenamen pro Mandant) und Etappe 4 (Scharfschalten) — unveraendert offen.

Was ohne den User nicht geht

Nichts Neues. Wie im Auftrag: 3a Weg (i) vs. (ii) (wie der Mandant beim Login bestimmt wird) und Etappe 4 (Scharfschalten, DATABASE_URL auf tessera_app) bleiben Rueckfragen. Dieser Durchlauf hat den Schalter nicht angefasst: keine Compose-Datei, keine .env, nichts auf einem Server, nichts in Active Directory.

Threat Flags

Keine neue Angriffsflaeche ausserhalb des <threat_model> des Plans: kein neuer Netzwerk-Endpunkt, kein neuer Auth-Pfad, keine Schemaaenderung an Vertrauensgrenzen (nur zusaetzliche, nur lesende Regeln plus eine Funktion ohne SECURITY DEFINER). T-EYM-01 bis T-EYM-08 mitigiert wie geplant (Belege oben), T-EYM-09 accept mit Ledger-Eintrag #37, T-EYM-SC: keine Paketinstallation, package.json/Lockfile unveraendert gegen 5e0e408.

Known Stubs

Keine. sendWelcomeEmail ist kein Stub (vollstaendig implementiert, nur ohne Aufrufer — seit vor diesem Durchlauf).

Self-Check: PASSED

  • Dateien: apps/api/prisma/migrations/20260914120000_rls_system_context_read/migration.sql, apps/api/src/dkv/dkv-scheduler.service.spec.ts, apps/api/src/mail/mail.service.spec.ts — FOUND (im Commit-Baum, git diff --stat 5e0e408 nennt 29 Dateien).
  • Commits 3d64567, 6e2a641, 939c812 — FOUND (git log --oneline 02016e1..HEAD), gepusht (HEAD == origin/main).