docs(quick-260914-eym): Etappe 3c abgeschlossen und verifiziert 9/9 — Zusammenfassung, Verifikation, Aktenstand
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

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 12:06:13 +02:00
parent 939c8121a1
commit 5f80582a37
3 changed files with 485 additions and 8 deletions
@@ -0,0 +1,199 @@
---
phase: quick-260914-eym
verified: 2026-09-14T12:40:00Z
status: passed
score: 9/9 must-haves verified
covered_files:
- .planning/WINDOWS.md
- .planning/quick/260914-eym-mandantentrennung-etappe-3c-systemkontex/260914-eym-PLAN.md
- .planning/quick/260914-eym-mandantentrennung-etappe-3c-systemkontex/260914-eym-SUMMARY.md
- apps/api/prisma/migrations/20260914120000_rls_system_context_read/migration.sql
- apps/api/scripts/rls-scratch-check.mjs
- apps/api/src/auth/auth.service.spec.ts
- apps/api/src/auth/auth.service.ts
- apps/api/src/dkv/dkv-scheduler.service.spec.ts
- apps/api/src/dkv/dkv-scheduler.service.ts
- apps/api/src/dkv/dkv.controller.ts
- apps/api/src/dkv/dkv.service.spec.ts
- apps/api/src/dkv/dkv.service.ts
- apps/api/src/groups/migration-sql.spec.ts
- apps/api/src/ldap/ldap-config.service.spec.ts
- apps/api/src/ldap/ldap-config.service.ts
- apps/api/src/mail/mail.module.ts
- apps/api/src/mail/mail.service.spec.ts
- apps/api/src/mail/mail.service.ts
- apps/api/src/prisma/prisma-tenant.extension.spec.ts
- apps/api/src/prisma/prisma-tenant.extension.ts
- apps/api/src/prisma/rls-access-inventory.spec.ts
- apps/api/src/settings/settings.service.spec.ts
- apps/api/src/settings/settings.service.ts
- apps/api/src/tenders/tender-digest.scheduler.spec.ts
- apps/api/src/tenders/tender-digest.scheduler.ts
- apps/api/src/tenders/tender-matching.service.spec.ts
- apps/api/src/tenders/tender-matching.service.ts
- apps/api/src/tenders/tender-notifications.integration.spec.ts
- docs/mandantentrennung-datenbankrolle.md
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- docs/mandantentrennung-etappe3-auftrag.md
- docs/mandantentrennung-zugriffsklassifikation.md
covered_digest: "v1:sha256:d81ca7fd6f1c5dd2e90f4b70f943467888d52417316824523b993980e9607e0e"
behavior_unverified: 0
overrides_applied: 0
---
# Quick 260914-eym: Mandantentrennung Etappe 3c — Systemkontext fuer die Hintergrunddienste — Verifikation
**Ziel:** Benannter Systemkontext `forSystem()` fuer die Hintergrunddienste: dritte Sitzungsvariable `app.system_context`, Funktion `is_system_context()`, neue Migration `20260914120000_rls_system_context_read` mit fuenf permissiven `system_read_policy ... FOR SELECT` (bestehende Migrationen unveraendert), Detektor mit fuenfter Erkennungsform und exakter Erlaubnisliste, Werkzeugabschnitt `runSystemContextChecks`, die sechs Faelle behandelt, Dokumente nachgezogen, Ledger #21/#30 fixed, #37 neu, gepusht. Der Schalter bleibt AUS.
**Verifiziert:** 2026-09-14, ca. 12:10-12:40 (HEAD 939c812, Arbeitsbaum nur mit den Orchestrator-Aenderungen `.planning/STATE.md` und der neuen SUMMARY)
**Status:** passed
**Erneute Verifikation:** Nein — Erstverifikation
Grundhaltung: Die SUMMARY wurde nicht als Beleg genommen. Jede Zahl unten ist in dieser Sitzung selbst gemessen (Kommando und beobachtetes Ergebnis stehen dabei). Wo ich etwas absichtlich kaputtgemacht habe, um den Wachhund zu pruefen, ist die Restauration mit `git status --porcelain -- apps/` (leer) belegt.
## 1. Git-Historie, Umfang und Schalter-Gates
| Pruefung | Kommando | Beobachtet | Status |
|---|---|---|---|
| Drei Commits seit Planstand | `git log --oneline 02016e1..HEAD` / `git rev-list --count 02016e1..HEAD` | `939c812`, `6e2a641`, `3d64567`; count=3 | VERIFIZIERT |
| 29 Dateien ausserhalb `.planning` | `git diff --stat 5e0e408 -- . ':!.planning'` | `29 files changed, 2499 insertions(+), 491 deletions(-)`; 29 Zeilen mit `\|` | VERIFIZIERT |
| Schalter-Gate leer | `git diff --name-only 5e0e408 -- apps/api/prisma/schema.prisma docker-compose.yml docker-compose.prod.yml docker-compose.dev.yml package.json apps/api/package.json pnpm-lock.yaml apps/api/scripts/rls-preflight.mjs apps/api/src/user/admin-seed.service.ts` | keine Ausgabe | VERIFIZIERT |
| Keine Umgebungs-/Compose-Datei im Diff | `git diff --name-only 5e0e408 \| awk 'index($0,"env") \|\| index($0,"compose")'` | keine Ausgabe | VERIFIZIERT |
| Keine bestehende Migration geaendert | `git diff --name-only 5e0e408 -- apps/api/prisma/migrations \| grep -v 20260914120000` | keine Ausgabe | VERIFIZIERT |
| Gepusht | `git fetch -q && git status -sb \| head -1`; `git rev-parse HEAD` / `origin/main` | `## main...origin/main` (kein `[ahead`); beide `939c8121a182fb8ad93b3b7f5b9fdcecc17a3ffd`; Push-URL zeigt auf `localhost:3002` | VERIFIZIERT |
| Arbeitsbaum | `git status --porcelain` | nur ` M .planning/STATE.md` und `?? .../260914-eym-SUMMARY.md` (Orchestrator-Dateien, unangetastet) | VERIFIZIERT |
## 2. Baseline-Messungen (frisch, nicht aus der SUMMARY)
| Messpunkt | Kommando | Beobachtet | Erwartung (Plan) | Status |
|---|---|---|---|---|
| API-Testsuite | `cd apps/api && npx vitest run` | `Test Files 64 passed (64)`, `Tests 1054 passed (1054)`, Exit 0 | >= 1044 | VERIFIZIERT |
| Typpruefung | `cd apps/api && npx tsc --noEmit` | Exit 0, keine Ausgabe | Exit 0 | VERIFIZIERT |
| Werkzeug gegen lebende DB (Lauf 1) | `TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@172.19.0.2:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs` | `Alle 253 Pruefungen bestanden.`, Exit 0, `FEHLGESCHLAGEN`-Zeilen: 0 | N >= 250 | VERIFIZIERT |
| Werkzeug (Lauf 2, nach allen Rueckbauten und Restaurationen) | dito | `Alle 253 Pruefungen bestanden.`, Exit 0 | 253 | VERIFIZIERT |
| Vier Funktionsfaelle | `grep -E "^is-system-context-" <log>` | `ungesetzt-false` (Rohwert null), `leer-false`, `true-true`, `fremdwert-false` — alle `bestanden` | vier gruen | VERIFIZIERT |
| Neun Kennungen je Tabelle (5 x 9 = 45) | Schleife ueber dkvmoduleconfig/ldapconfig/ldapfieldmapping/tendermatch/tendersavedsearch x wegwerftabelle-deckt-alle-spalten / ungebunden-null-zeilen / sieht-beide-mandanten / insert-abgewiesen-42501 / updatemany-count-0 / deletemany-count-0 / fortenant-a-nach-systemkontext-nur-a / is-system-context-unter-fortenant-false / pg-policies-genau-eine-system-read-policy-select | keine fehlende, keine rote Kennung | 45 gruen | VERIFIZIERT |
| Relations-Kennung | `grep '^ldapconfig-systemkontext-include-fieldmappings-beider-mandanten'` | `bestanden — ... liefert 2 Zeile(n): ["TENANT-A:1","TENANT-B:1"]` | gruen | VERIFIZIERT |
## 3. Lebende Datenbank (Container `tessera-ctl-db-1`, IP 172.19.0.2, Rolle `tessera`)
| Pruefung | Kommando | Beobachtet | Status |
|---|---|---|---|
| Container | `docker ps --filter name=tessera-ctl-db-1` | `Up About an hour (healthy)` | — |
| Migrationsstand | `DATABASE_URL=... ./node_modules/.bin/prisma migrate status` | `36 migrations found`, `Database schema is up to date!` | VERIFIZIERT |
| Funktion vorhanden | `SELECT count(*) FROM pg_proc WHERE proname='is_system_context'` | 1; `provolatile='s'` (STABLE), `prosecdef=false` (kein SECURITY DEFINER) | VERIFIZIERT |
| Systemleseregeln | `SELECT tablename, cmd, permissive, qual, with_check FROM pg_policies WHERE policyname='system_read_policy'` | genau 5 Zeilen: DkvModuleConfig, LdapConfig, LdapFieldMapping, TenderMatch, TenderSavedSearch — je `SELECT` / `PERMISSIVE` / `is_system_context()` / with_check `null` | VERIFIZIERT |
| Gesamtzahl Regeln | `SELECT count(*) FROM pg_policies WHERE schemaname='public'` | 34 | VERIFIZIERT |
| SmtpConfig ohne Systemregel | Regeln der sechs Tabellen gelistet | SmtpConfig nur `tenant_isolation_policy` (ALL); die fuenf anderen je zwei Regeln | VERIFIZIERT |
| Schalter AUS | `SELECT rolname, rolsuper, rolbypassrls FROM pg_roles` | `tessera` super+bypassrls; `tessera_app` weder noch — Anwendung verbindet unveraendert als `tessera` | VERIFIZIERT |
| Nach Rueckbau (a) | `pg_policies` erneut, plus `SELECT datname FROM pg_database WHERE datname LIKE '%scratch%'` | 34 Regeln; TenderMatch: `system_read_policy` SELECT + `tenant_isolation_policy` ALL; keine Wegwerf-DB uebrig | VERIFIZIERT |
## 4. Beobachtbare Wahrheiten (must_haves.truths)
| # | Wahrheit | Status | Beleg |
|---|---|---|---|
| 1 | `forSystem(prisma)` in Array-Form-`$transaction`, EINE getaggte Anweisung setzt `app.system_context='true'`, `app.current_tenant=''`, `app.current_user=''`; `forTenant()`/`withTenantTransaction()` setzen `app.system_context=''`; Kein-Erben gemessen und Reset per Rueckbau falsifiziert | VERIFIZIERT | Datei gelesen: `forSystem` baut `$executeRaw\`SELECT set_config('app.system_context', 'true', true), set_config('app.current_tenant', '', true), set_config('app.current_user', '', true)\`` und `$transaction([setContext, query(args)])`; `grep -cF "set_config('app.system_context', '', true)"` = 2 (forTenant + withTenantTransaction). Werkzeug: `<slug>-fortenant-a-nach-systemkontext-nur-a` und `<slug>-is-system-context-unter-fortenant-false` fuer alle fuenf Tabellen gruen. Rueckbau (c) nicht selbst wiederholt (siehe Angenommene Risiken); Helfer-Spec 15 Tests gruen |
| 2 | Migration mit `is_system_context()` (STABLE, COALESCE) und genau fuenf PERMISSIVE `system_read_policy ... FOR SELECT` auf den fuenf Tabellen, SmtpConfig nicht dabei, bestehende Migrationen unveraendert, Schalter AUS | VERIFIZIERT | Datei gelesen; Zaehlung ohne Kommentarzeilen: `CREATE POLICY system_read_policy`=5, `FOR SELECT`=5, `DROP POLICY`=0, `SmtpConfig`=0. Lebende DB s. Abschnitt 3. `git diff --name-only 5e0e408 -- apps/api/prisma/migrations \| grep -v 20260914120000` leer |
| 3 | Regel erweitert NUR das Lesen: INSERT 42501, updateMany/deleteMany count 0, je Tabelle die neun Kennungen ueber den generierten Client | VERIFIZIERT | 45 Kennungen gruen (Abschnitt 2). Eigener Rueckbau (a): `FOR SELECT` bei TenderMatch entfernt -> `tendermatch-systemkontext-insert-abgewiesen-42501: FEHLGESCHLAGEN — ... ist NICHT fehlgeschlagen — angelegt: "tm-system-schreibversuch"`, dazu updatemany count=3, deletemany count=3, `nur-a` 0 Zeilen, `pg-policies` zeigt `cmd: ALL`; `5 von 253 Pruefungen fehlgeschlagen.` Danach `git checkout -- <Migration>`, `git status --porcelain -- apps/` leer, Werkzeug wieder 253 |
| 4 | Sechs Faelle behandelt: DKV Auftrag je Mandant; Mail Transport je Versand, Startpfad geloescht; ldap beide Leser ueber forSystem, Schreibzeile forTenant; digest Kandidaten forSystem; matching Suchprofile forSystem, Katalog ungebunden; admin-seed unveraendert | VERIFIZIERT | dkv: `loadActiveConfigsForScheduler()` = `forSystem(this.prisma).dkvModuleConfig.findMany({ where: { isActive: true }, select: CONFIG_SAFE_SELECT, orderBy: { tenantId: 'asc' } })`; Scheduler `jobNameFor` = `dkv-inbox-poll:<tenantId>`, `setInterval(intervalMin, tenantId)`/`stopJob(tenantId)` nur dieser Name, Controller `setInterval(dto.pollIntervalMin, tenantId)` / `stopJob(tenantId)`; `activeTenantId` im Code 0, `loadAnyActiveConfigForScheduler` 0. mail: `mail.module.ts` nur `SettingsModule` + `MailService`; `MailerModule/MailerService` im Code 0; `resolveTransport(tenantId)` -> `getDecryptedSmtpConfig(tenantId)` (gebunden, `findUnique({ where: { tenantId } })`) sonst Env-Kette; `transport?.close()` im finally; `loadAnySmtpConfigForStartupTransport` in vier Dateien 0; `this.prisma.smtpConfig` in settings.service 0; `auth.service.ts:248` `sendPasswordResetEmail(email, token, user.tenantId)`. ldap: Zeile 71 forSystem (Bootstrap, `select id/tenantId/encryptedBindPassword`), Zeile 87/88 `forTenant(this.prisma, config.tenantId)` + `ldapConfig.update` je Altzeile; Zeile 322 forSystem `getAllActiveConfigs` mit `include: { tenant, fieldMappings }`; `this.prisma.ldapConfig` im Code 0. digest: Zeile 124 forSystem `tenderMatch.findMany({ where: { notifiedAt: null }, select: { userId, tenantId }, distinct: ['userId'] })`, Schleife `forTenant(this.prisma, tenantId)`; matching: Zeile 75 forSystem `tenderSavedSearch.findMany()`, Zeile 90 `this.prisma.tender.findMany` (D-03), Zeile 98 `forTenant(this.prisma, search.tenantId)`. admin-seed: `git diff --quiet 5e0e408 -- apps/api/src/user/admin-seed.service.ts` unveraendert |
| 5 | Mit EINEM Mandanten unter BYPASSRLS je Pfad identisch — als Test festgenagelt | VERIFIZIERT (verhaltensabhaengig, durch benannte Tests belegt) | `npx vitest run` der zehn betroffenen Specs: 10 Dateien / 172 Tests gruen. dkv-scheduler.service.spec.ts (7 Tests, ECHTES `cron`): Test 1 `cronTime.source === '*/15 * * * *'`, `isActive` true, genau ein Auftrag `dkv-inbox-poll:t1`; Test 2 `0 */2 * * *`; Test 3 `fireOnTick()` -> `processInbox('t1')`; Test 4 inaktiv/keine -> 0 Auftraege + `no active config found`; Test 5 zwei Mandanten, `setInterval(30,'t2')` laesst t1-Objekt identisch (`toBe(t1JobBefore)`), `stopJob('t1')` nur t1; Test 6 werfender Startpfad; Test 7 stopJob No-Op. mail.service.spec.ts (4 Tests): Test 1 `createTransport({host:'smtp-a.example.invalid',port:465,secure:true,requireTLS:false,auth:{user:'user-a',pass:'geheim-a'}})`, `from` = fromAddress, `close()` einmal, `MAIL_HOST` darf nicht greifen; Test 2 MAIL_* vor TESSERA_SMTP_* vor localhost:1025, `from` aus TESSERA_SMTP_FROM bzw. Vorgabe, TESSERA_SMTP_SECURE; Test 3 zwei Mandanten, kein Kennwort des anderen; Test 4 Throw verschluckt, Protokoll ohne Kennwort, close() trotzdem. Env-Kette feldweise gegen `git show 5e0e408:apps/api/src/mail/mail.module.ts` verglichen: host/port/user/pass/from/secure identisch; SmtpConfig-Zweig bildet `secure = encryption==='ssl-tls'`, `requireTLS = encryption==='starttls'` exakt wie die geloeschte `loadAnySmtpConfigForStartupTransport` und wie `dkv-mail.service.ts`. ldap-spec: `getAllActiveConfigs` forSystem 1 / forTenant nie; Bootstrap leer forSystem 1 / forTenant nie / kein Update; Bootstrap mit Altzeile `forTenant(prisma,'t1')`, update `aa11:bb22:<hex>`. digest/matching: `__systemCallLog` genau `[tenderMatch.findMany]` bzw. `[tenderSavedSearch.findMany]`, `forTenant` weiter genau einmal, `tender.findMany` auf rohem Client. auth-spec Zeile 392: `sendPasswordResetEmail('bob@example.com', expect.any(String), 't1')` |
| 6 | Detektor: fuenfte Erkennungsform, `system-gebunden`, Vorrangregel, `FORSYSTEM_ALLOWED_CALL_SITES` mit exakten Zahlen (dkv 1, ldap 2, digest 1, matching 1), Fremddatei/Abweichung/veralteter Eintrag -> rot | VERIFIZIERT | Spec: Regex `/const\s+(\w+)\s*=\s*forSystem\(/g` (Zeile 439), `STAND_TOKENS = ['gebunden','ungebunden','gemischt','system-gebunden']`, Map exakt wie im Plan. `npx vitest run src/prisma/rls-access-inventory.spec.ts` -> 30/30 gruen. Quelltextzaehlung: `grep -rl 'forSystem(' apps/api/src` ohne spec/Helfer = genau die 4 Dateien; `const systemPrisma = forSystem(this.prisma)` = 5. EIGENE Falsifikation 1: `const x = forSystem(this.prisma);` in `apps/api/src/groups/groups.service.ts` (nicht in der Liste) -> `1 failed \| 29 passed`, Meldung `apps/api/src/groups/groups.service.ts: 1 forSystem(-Aufruf(e), Datei steht NICHT in FORSYSTEM_ALLOWED_CALL_SITES — ein Anfrageweg darf den Systemkontext nie rufen`. EIGENE Falsifikation 2: zweite Zuweisung in `dkv.service.ts` (erlaubte Datei) -> `2 failed \| 28 passed`, Meldungen `gemessen 2 forSystem(-Aufruf(e), erlaubt sind genau 1` und `Erlaubnisliste nennt 1, gemessen 2 — der Eintrag ist ueberholt`. Beide per `git checkout --` restauriert; `git status --porcelain -- apps/` leer; `git diff --quiet HEAD -- <Datei>` sauber |
| 7 | Frage aus Etappe 2 je Pfad beantwortet (Leere ist nie Abwesenheit), Abschnitt (y1)-(y5) in der Kritikschrift | VERIFIZIERT | `## Systemkontext (Etappe 3c, 260914-eym)` Zeile 3250 VOR `## Etappe 2 — Abschluss` 3465; `### (y1)` 3270, `(y2)` 3333, `(y3)` 3399, `(y4)` 3431, `(y5)` 3451. (y1): 22 `bestanden`-Zeilen, enthaelt `is-system-context-ungesetzt-false: bestanden`, `tendermatch-systemkontext-insert-abgewiesen-42501: bestanden` und fuenf `#system_read_policy#SELECT#is_system_context()#`-Zeilen. (y3) nennt dkv-scheduler.service.ts, ldap-sync.scheduler.ts, ldap.service.ts, ldap-config.service.ts, tender-digest.scheduler.ts, tender-matching.service.ts, admin-seed.service.ts je einmal. Nachtrag (260914-eym) in (d4)/(s4)/(b4) je 1, Abschluss 2 Treffer |
| 8 | Aktenstand kohaerent: sechs Zeilen `system-gebunden`, settings/smtpConfig `gebunden`, 72 Paare / 35/21/14/2, Uebersichtstabelle mit Spalte System und ABGELEITETER Summenzeile, Hintergrunddienst-Regelschluesse, Auftrag 3c erledigt, Datenbankrolle mit dritter Variable + preflight-Aussage, Ledger #21/#30 fixed, #37 neu, Zaehler 15/1/21/37 | VERIFIZIERT | Gate-Schleife aus Aufgabe 3 selbst ausgefuehrt: PAARE=72; Klassen gezaehlt 35/21/14/2 = dokumentiert; `system-gebunden`-Zeilen = 6 (dkv/dkvModuleConfig, ldap-config/ldapConfig, /ldapFieldMapping, /tenant, digest/tenderMatch, matching/tenderSavedSearch), settings/smtpConfig `gebunden`; Ableitung `ABGELEITET 61/179/5` (dkv 0/22/1, ldap 1/27/2, tenders 33/27/2) = Summenzeile `**61** \| **179** \| **5**`; Kopf `\| Bereich \| Ungebunden \| Gebunden \| System \|`; `Regelschluss (260914-eym)` im Hintergrunddienst-Abschnitt 7x (>= 6); `**Stand 260914-eym` vorhanden. Auftrag: `Erledigt (260914-eym, 3d64567/6e2a641 ...` Zeile 145. Datenbankrolle: `app.system_context` (Z. 90-103), `is-system-context-ungesetzt-false` (Z. 166), preflight-Aussage (Z. 163); im Diff gegen 5e0e408 kein `SECURITY DEFINER` (0). Ledger: `gsd-tools windows status` -> #21 `fixed` resolved_at 2026-09-14T09:51:23Z, #30 `fixed` resolved_at 2026-09-14T09:51:24Z, #37 `open` (quick-260914-eym, deviation, dkv.service.ts, Single-Flight-Riegel); Frontmatter open 15 / waived 1 / fixed 21 / total 37 = aus Zeilen gezaehlt 15/21/1/37 |
| 9 | Schalter AUS, Gates gegen 5e0e408 leer, Tests >= 1044, tsc 0, Werkzeug >= 250, sauber, gepusht | VERIFIZIERT | Abschnitte 1-3 |
**Score:** 9/9 Wahrheiten verifiziert (0 present-behavior-unverified)
### Verhaltensabhaengige Wahrheiten — Belegform
Wahrheiten 1, 3, 5 und 6 behaupten Laufzeitverhalten (Kontext-Reset, Schreibverbot, Identitaet je Pfad, Wachhund). Keine davon ist auf Symbolpraesenz allein als VERIFIZIERT gesetzt: 1 und 3 sind live im Werkzeug gegen eine Rolle ohne BYPASSRLS gemessen (253 gruen, Rueckbau (a) selbst wiederholt), 5 durch die benannten Specs (172 Tests, echtes `cron`), 6 durch zwei eigene Falsifikationen.
## 5. Artefakte
| Artefakt | Erwartet | Status | Details |
|---|---|---|---|
| `apps/api/prisma/migrations/20260914120000_rls_system_context_read/migration.sql` | NEU, Funktion + fuenf Regeln + Abschnitt "bewusst NICHT" | VERIFIZIERT | 5/5/0/0-Zaehlung (s. o.); Abschnitt "Was diese Migration bewusst NICHT tut" vorhanden (SmtpConfig, Tenant/Tender, keine Schreibregel, Schalter); lokal angewendet (36 Migrationen) |
| `apps/api/src/prisma/prisma-tenant.extension.ts` | `forSystem()` mit Kopfkommentar, Reset in forTenant/withTenantTransaction, `$transaction`-Feld zwei Eintraege | VERIFIZIERT | gelesen; Abschnitt "SYSTEMKONTEXT (Etappe 3c, 260914-eym)" im Kopf; Array `[setContext, query(args)]` |
| `apps/api/src/prisma/prisma-tenant.extension.spec.ts` | >= 3 neue Tests | VERIFIZIERT | 11 -> 15 Tests, gruen |
| `apps/api/src/groups/migration-sql.spec.ts` | describe fuer neue Migration | VERIFIZIERT | `_rls_system_context_read` vorhanden; 32 Tests gruen |
| `apps/api/src/prisma/rls-access-inventory.spec.ts` | fuenfte Form, `systemModels`, `system-gebunden`, Erlaubnisliste | VERIFIZIERT | 30 Tests; zwei eigene Falsifikationen rot |
| `apps/api/scripts/rls-scratch-check.mjs` | `runSystemContextChecks`, `extractSystemReadPolicySql`, `buildInlineSystemClient` | VERIFIZIERT | grep-Treffer; 253 Pruefungen, Abschnitt laeuft im Hauptlauf |
| dkv (service, scheduler, controller, zwei Specs) | Auftrag je Mandant, `registeredTenantIds`, `stopJob(tenantId)`, neue Spec >= 6 Tests | VERIFIZIERT | 7 Scheduler-Tests, 17 Service-Tests gruen |
| mail (module, service, NEUE spec), settings (service, spec), auth (service, spec) | Startpfad weg, `resolveTransport`, Transport je Versand, `user.tenantId` durchgereicht | VERIFIZIERT | 4 Mail-Tests, 16 Settings-Tests, 29 Auth-Tests gruen |
| ldap-config, tender-digest, tender-matching (+Specs), tender-notifications.integration.spec | forSystem-Leser, Mocks ergaenzt | VERIFIZIERT | 19/16/17 Tests gruen; Integrationsspec in Vollsuite gruen |
| vier Dokumente + `.planning/WINDOWS.md` | Nachtraege, 3c erledigt, Ledger | VERIFIZIERT | Abschnitt 4, Wahrheiten 7/8 |
## 6. Schluesselverbindungen (key_links)
| Von | Nach | Ueber | Status | Details |
|---|---|---|---|---|
| `FOR SELECT` in der Migration | Schreibverbot unter Systemkontext | permissive ODER-Verknuepfung | VERBUNDEN | Rueckbau (a) selbst wiederholt: ohne `FOR SELECT` gelingt der Insert (5/253 rot); mit: 253 gruen |
| Detektor-Form `const X = forSystem(` | Bestandsaufnahme-Stand `system-gebunden` | Regex Zeile 439 + Erlaubnisliste | VERBUNDEN | 6 Zeilen `system-gebunden` in der Klassifikation; Falsifikationen rot |
| `set_config(..., true)` + Reset | Kein Erben zwischen Kontexten | Werkzeug `fortenant-a-nach-systemkontext-nur-a` | VERBUNDEN | 5x gruen; Rueckbau (c) nicht selbst wiederholt (Angenommene Risiken) |
| `auth.service.ts:248` | `MailService.sendPasswordResetEmail(..., tenantId)` | `user.tenantId` | VERBUNDEN | Quelltext + auth-spec Zeile 392 |
| `…-ungebunden-null-zeilen` + `…-sieht-beide-mandanten` | zu-wenig-statt-zu-viel-Falle | Werkzeugpaar je Tabelle | VERBUNDEN | 5 Paare gruen; (y3) beantwortet je Pfad |
## 7. Datenfluss (Level 4)
| Artefakt | Variable | Quelle | Echte Daten | Status |
|---|---|---|---|---|
| dkv-scheduler `onModuleInit` | `configs` | `forSystem(prisma).dkvModuleConfig.findMany({ where: { isActive: true } })` | ja | FLIESST |
| mail `resolveTransport` | `smtpConfig` | `settingsService.getDecryptedSmtpConfig(tenantId)` -> `forTenant(...).smtpConfig.findUnique({ where: { tenantId } })` | ja (Rueckfall Env-Kette explizit) | FLIESST |
| ldap `getAllActiveConfigs` / Bootstrap | `configs` | `forSystem(prisma).ldapConfig.findMany(...)` | ja | FLIESST |
| digest `candidates` / matching `savedSearches` | — | `forSystem(prisma).tenderMatch.findMany` / `.tenderSavedSearch.findMany()` | ja | FLIESST |
## 8. Verhaltens-Stichproben
| Verhalten | Kommando | Ergebnis | Status |
|---|---|---|---|
| Vollsuite | `npx vitest run` | 64 Dateien / 1054 Tests | PASS |
| Typpruefung | `npx tsc --noEmit` | Exit 0 | PASS |
| Zehn betroffene Specs benannt | `npx vitest run <10 Dateien>` | 10 / 172 gruen | PASS |
| Detektor | `npx vitest run src/prisma/rls-access-inventory.spec.ts` | 30/30 | PASS |
| Detektor mit Fremddatei | s. Wahrheit 6 | 1 failed / 29 passed | PASS (rot wie gefordert) |
| Detektor mit Zahlabweichung | s. Wahrheit 6 | 2 failed / 28 passed | PASS (rot wie gefordert) |
| Werkzeug gruen | `node apps/api/scripts/rls-scratch-check.mjs` (2x) | 253 / 253 | PASS |
| Werkzeug Rueckbau (a) | `FOR SELECT` bei TenderMatch entfernt | `5 von 253 Pruefungen fehlgeschlagen.` — Insert GELINGT | PASS (rot wie gefordert) |
## 9. Sonde-Ausfuehrung
Keine `scripts/*/tests/probe-*.sh` im Projekt; das Werkzeug `rls-scratch-check.mjs` ist die Sonde dieses Durchlaufs und wurde zweimal selbst ausgefuehrt (Abschnitt 2, 8).
## 10. Anforderungsabdeckung
| Anforderung | Plan | Beschreibung | Status | Beleg |
|---|---|---|---|---|
| ETAPPE-3C | 01 | Systemkontext fuer Hintergrunddienste | ERFUELLT | Wahrheiten 1-9 |
| WINDOWS-21 | 01 | DKV-Planer je Mandant | ERFUELLT | Wahrheit 4/5, Ledger #21 fixed |
| WINDOWS-30 | 01 | Mail-Startpfad | ERFUELLT (durch Entfernen, nicht Umstellen — im Plan so vorgesehen) | Wahrheit 4/5, Ledger #30 fixed |
Keine Zuordnung in `.planning/REQUIREMENTS.md` fuer Quick-Tasks — keine verwaisten Anforderungen.
## 11. Anti-Pattern-Scan
`grep -n -E "\b(TBD|FIXME|XXX|TODO|HACK|PLACEHOLDER)\b"` ueber alle 29 geaenderten Dateien: keine Treffer. `console.log` in geaenderten Nicht-Spec-Dateien: keine Treffer. Keine Stubs: `sendWelcomeEmail` ist vollstaendig implementiert und hat null Aufrufer ausserhalb `mail/` (Bestand seit vor diesem Durchlauf, in (y4) benannt).
## 12. Menschliche Pruefung erforderlich
Keine — jede Zusicherung des Plans ist entweder statisch, per Test oder live gegen die Datenbank gemessen. Was ausserhalb dieses Repos liegt, steht unter "Angenommene Risiken".
## 13. Luecken
Keine.
## Angenommene Risiken
Was ich NICHT selbst messen konnte oder bewusst nicht wiederholt habe:
1. **Rueckbau (b), (c) und (d) nicht selbst wiederholt.** Der Auftrag verlangte EINE Rueckbau-Falsifikation (empfohlen (a)); die habe ich vollstaendig wiederholt und dasselbe Ergebnis wie die SUMMARY beobachtet (5 von 253 rot, Insert gelingt). Fuer (b) (Regel aus der Migration entfernt -> Werkzeug folgt der Datei, DB bleibt 34), (c) (`local=false` + Reset entfernt -> Erben sichtbar) und (d) (Erlaubniszahl auf 0) stuetze ich mich auf die woertlichen Ausgaben der SUMMARY. (d) ist durch meine beiden eigenen Detektor-Falsifikationen in der Sache gedeckt (Fremddatei rot, Zahlabweichung rot); (c) ist durch die fuenf gruenen `…-fortenant-a-nach-systemkontext-nur-a`-Kennungen und den gelesenen Helfer-Quelltext (Reset in allen drei Formen) gedeckt, nur der Beleg "Reset traegt bei local=false" ist nicht erneut erzeugt.
2. **Alpha-Go-live morgen ist nicht beobachtet.** Dass DKV-Postfach-Abruf und Kennwort-Zuruecksetzung auf `alpha.tessera.ctl.de` mit dem echten SMTP-Server und dem echten Postfach identisch zu heute laufen, ist hier per Test festgenagelt (Cron-Expression, Tick, Transport aus der SmtpConfig des Mandanten), nicht gegen den Testserver gemessen — Deploy und Beobachtung auf dem Server macht der User selbst (Absprache). Unter BYPASSRLS sind `forSystem`/`forTenant` wirkungslos; die einzigen beobachtbaren Verhaltensaenderungen sind (i) der Registry-Name `dkv-inbox-poll:<tenantId>` statt `dkv-inbox-poll` und (ii) der Transport je Versand statt beim Start — beide durch Tests gedeckt, (ii) zusaetzlich feldweise gegen den geloeschten Startpfad verglichen.
3. **Migration auf alpha.** `20260914120000_rls_system_context_read` ist rein additiv (CREATE FUNCTION, CREATE POLICY) auf Tabellen, die RLS bereits aus frueheren Migrationen tragen; sie ist lokal per `migrate deploy` gruen. Ob `migrate deploy` auf alpha morgen ebenso sauber laeuft, ist hier nicht messbar (kein Deploy durch mich).
4. **`@nestjs-modules/mailer` bleibt installiert und unbenutzt** (`package.json`/Lockfile bewusst unveraendert, im Plan so vorgesehen). Kein Defekt, aber Aufraeumbedarf — in (y4) und in der SUMMARY benannt.
5. **WINDOWS #37 (Single-Flight-Riegel prozessweit)** ist bewusst offen; mit einem Mandanten ohne Wirkung, mit mehreren nur Verzoegerung, kein Datenverlust — im Ledger als `open` eingetragen, nicht Teil dieses Auftrags.
6. **Ledger-Zeitstempel**: `resolved_at` von #21/#30 liegt bei 09:51 UTC (11:51 lokal), passend zur SUMMARY-Sitzung 11:10-12:05; nicht weiter pruefbar.
Der Arbeitsbaum wurde exakt so hinterlassen wie vorgefunden: `git status --porcelain` zeigt nur ` M .planning/STATE.md` und die neue SUMMARY (beide unangetastet) sowie jetzt diese VERIFICATION.md. Alle drei temporaeren Aenderungen (groups.service.ts, dkv.service.ts, migration.sql) sind per `git checkout --` restauriert und mit `git status --porcelain -- apps/` (leer) belegt; die lebende Datenbank steht bei 34 Regeln, keine Wegwerf-Datenbank blieb zurueck.
---
_Verifiziert: 2026-09-14T12:40:00Z_
_Verifier: Claude (gsd-verifier)_