Files
schalli cc26197fa1
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 52s
Tessera CI/CD / Build & Publish Images (push) Successful in 27s
docs: Etappe 2 der Mandantentrennung abgeschlossen — alle zwoelf Bereiche gebunden und verifiziert
2026-09-11 14:18:11 +02:00

122 lines
13 KiB
Markdown

---
phase: quick-260911-gwh
verified: 2026-09-11T14:20:00Z
status: passed
score: 9/9 must-haves verified
covered_files:
- .planning/WINDOWS.md
- .planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-PLAN.md
- .planning/quick/260911-gwh-mandantentrennung-etappe-2-bereiche-favo/260911-gwh-SUMMARY.md
- apps/api/scripts/rls-scratch-check.mjs
- apps/api/src/favorites/favorites.controller.ts
- apps/api/src/favorites/favorites.service.spec.ts
- apps/api/src/favorites/favorites.service.ts
- apps/api/src/mail/mail.module.ts
- apps/api/src/settings/settings.service.spec.ts
- apps/api/src/settings/settings.service.ts
- docs/anleitung-entwicklung.md
- docs/mandantentrennung-etappe2-fehlerrichtung.md
- docs/mandantentrennung-zugriffsklassifikation.md
covered_digest: "v1:sha256:2a5afae1c3039a871e737d6548a419ce5db94b270a0023fb53167db516d4b32f"
behavior_unverified: 0
overrides_applied: 0
---
# Quick 260911-gwh: Etappe 2 der Mandantentrennung, Bereiche favorites/settings — Verification Report
**Task Goal:** Mandantentrennung Etappe 2, Bereiche `favorites` und `settings` — 7 `favoriteLink`- und 3 `smtpConfig`-Anfragewege binden, den umbenannten Startpfad bewusst ungebunden lassen (sechster Hintergrunddienst-Fall), den gemessenen Widget-Besitzriegel einbauen, beide fehlenden Spec-Dateien anlegen, Befund K schließen, das Klassifikationsdokument auf den Etappe-2-Endstand bringen.
**Verified:** 2026-09-11
**Status:** passed
**Re-verification:** No — initial verification
This report independently re-measures every claim in the SUMMARY against the live codebase and a live database container. No claim was accepted on the SUMMARY's word alone.
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | All 7 `favoriteLink` sites bound (5 methods), 3 `smtpConfig` request-path sites bound, exactly 1 `smtpConfig` site (startup path) deliberately unbound | ✓ VERIFIED | `grep -n "favoriteLink\."` → 7 hits, all on `tenantPrisma`. `grep -n "smtpConfig\."` → 3 on `tenantPrisma` (`getSmtpConfig`, `saveSmtpConfig`, `getDecryptedSmtpConfig`), 1 on `this.prisma` (`loadAnySmtpConfigForStartupTransport`, line 244) |
| 2 | `mail.module.ts` calls the new name; old name `getStartupSmtpConfig` no longer exists anywhere in `apps/api/src` | ✓ VERIFIED | `mail.module.ts` calls `settingsService.loadAnySmtpConfigForStartupTransport()`. `grep -rn getStartupSmtpConfig apps/api/src` → 0 hits. Old name only appears in historical phase artifacts (`.planning/phases/07-*`, `.planning/phases/12-*`, untouched history) and as an explicit "(vormals `getStartupSmtpConfig()`)" annotation in the ledger/critique docs — never as a live call |
| 3 | Befund K closed and recorded as closed at (t4), (d4), and in the classification doc | ✓ VERIFIED | `getDecryptedSmtpConfig(tenantId)` runs over `forTenant()` (1 client). (t4) carries `**Nachtrag (260911-gwh):**` confirming the ordering condition is fulfilled (line ~625). (d4) carries the matching `**Nachtrag (260911-gwh):**` (line ~935), plus a correction that `dkv.seed.ts`/`module-registry` is NOT bound (measured, not copied from the plan's suggestion). Classification doc's background-service section states "Befund K ist mit dieser Bindung ERFÜLLT" |
| 4 | Widget-ownership guard in `favorites.create()`, measured necessary via Prüfung 7 | ✓ VERIFIED | Guard exists in `favorites.service.ts` (`tenantPrisma.widgetInstance.findUnique` → `NotFoundException('Widget not found')` on null/foreign owner). Prüfung 7 (`favoritelink-fremdschluessel-prueft-am-zeilenschutz-vorbei`) is committed in `rls-scratch-check.mjs:3986-4046` and reproduced independently against the live `tessera-ctl-db-1` container: a bound `create` with a foreign tenant's `widgetId` **succeeds** (FK bypasses RLS) while a nonexistent `widgetId` throws P2003 — confirming the guard is necessary, not decorative. Reverted the guard live and re-ran the spec: exactly the 4 claimed failures reproduced (3 "Widget not found" cases + 1 watchdog case), then restored (clean `git diff`) |
| 5 | Startup path names both states (today: arbitrary tenant's SMTP; post-cutover: null/silent) and is named so it can't be mistaken for a request-path method | ✓ VERIFIED | `loadAnySmtpConfigForStartupTransport()` doc-comment explicitly states both states, the fallback-chain double-concealment, the asymmetry to `ldap`/`dkv`, and the "own ledger entry, not attached to #21" decision with reason. `mail.module.ts` header comment mirrors this |
| 6 | Three ledger entries #30/#31/#32 exist, open, and match plan rationale | ✓ VERIFIED | `.planning/WINDOWS.md` rows 47-49 confirmed: #30 (mail.module.ts startup path, own entry not attached to #21), #31 (favorites-widget.tsx silent-empty), #32 (smtp-settings-form.tsx silent-empty). All `status: open`. Header counters cross-checked: `open_count=14`/`total_count=32` vs. 32 table rows / 14 open rows — match |
| 7 | Two new spec files use the two-client harness, `nodemailer` is `vi.mock`'d, no real send attempted | ✓ VERIFIED | `favorites.service.spec.ts`: bound/unbound client separation via `__makeBoundClient`, `IconDiscoveryService` fully mocked (`vi.fn`), no network calls. `settings.service.spec.ts`: unbound client offers ONLY `findFirst`, bound client offers ONLY `findUnique`/`upsert`; `nodemailer` is `vi.mock('nodemailer', ...)` with `createTransport` returning stub `verify`/`sendMail`. Reproduced falsification (a): reverting the `create()` guard reproduced the exact claimed 4 test failures |
| 8 | Generated-client measurements committed — 137 total checks, named `favoritelink-*`/`smtpconfig-*` checks, throwaway tables column-checked (10 scalar fields each) | ✓ VERIFIED | Re-ran `rls-scratch-check.mjs` fresh against the live `tessera-ctl-db-1` container (resolved IP freshly: `172.19.0.2`). Output: "Alle 137 Pruefungen bestanden." 8 `favoritelink-*` named checks + 9 `smtpconfig-*` named checks observed, including the two column-coverage checks confirming 10 scalar fields each match `schema.prisma` exactly, and the `SmtpConfig_tenantId_key` unique index presence |
| 9 | Final stage-2 state of the classification document: recomputed sums, six-case heading, closing section numbers match derived measurements | ✓ VERIFIED | Recomputed independently: Übersicht column sums 68 (ungebunden) / 178 (gebunden) — matches Summenzeile exactly. Klassen-Verteilung 33+17+13+2 = 65 — matches. `## Der Hintergrunddienst als Falle — sechs Fälle` heading present; sixth case (`mail.module.ts`/`loadAnySmtpConfigForStartupTransport`) documented with Befund-K-erfüllt statement. `## Etappe 2 — Abschluss` closing section cites 68/178, 65 Paare, 12 runs, matching the same derived numbers. `rls-access-inventory.spec.ts` (11/11 tests) independently re-run and green, confirming the doc-vs-source consistency gate holds |
**Score:** 9/9 truths verified (0 present, behavior-unverified)
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/api/scripts/rls-scratch-check.mjs` | `runFavoritesAreaChecks` (≥7 named checks), `runSettingsAreaChecks`, positioned after `runAuthAreaChecks` | ✓ VERIFIED | 8 + 9 = 17 new named checks confirmed by live re-run; 137/137 total |
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | `## Bereich favorites`, `## Bereich settings`, `## Etappe 2 — Abschluss`, Nachträge at (t4)/(d4) | ✓ VERIFIED | All sections present and content-checked above |
| `apps/api/src/favorites/favorites.service.ts` | 5 methods, 1 client each, widget-ownership guard in `create` | ✓ VERIFIED | Confirmed by direct read; 7 bound `favoriteLink` + 1 bound `widgetInstance` accesses |
| `apps/api/src/favorites/favorites.service.spec.ts` | NEW, two-client harness, icon service mocked, watchdog, edge cases | ✓ VERIFIED | 23 cases, all green in full suite run |
| `apps/api/src/favorites/favorites.controller.ts` | passes `tenantId` from `extractContext` to all 5 service methods | ✓ VERIFIED | Direct read confirms all 5 call sites pass `tenantId` |
| `apps/api/src/settings/settings.service.ts` | 3 methods bound, startup path renamed with header comment | ✓ VERIFIED | Direct read confirms |
| `apps/api/src/settings/settings.service.spec.ts` | NEW, two-client harness with boundary (unbound only `findFirst`, bound only `findUnique`/`upsert`), nodemailer/CryptoService mocked, null-client proof for startup path | ✓ VERIFIED | 20 cases, all green; `forTenant` call-count assertions confirm boundary |
| `apps/api/src/mail/mail.module.ts` | calls renamed startup path, comment names both states | ✓ VERIFIED | Direct read confirms |
| `docs/mandantentrennung-zugriffsklassifikation.md` | overview rows, Summenzeile, Bestandsaufnahme, Klassen-Verteilung, six-case section, "was diese Etappe nicht entscheidet" | ✓ VERIFIED | All recomputed and matched independently |
| `docs/anleitung-entwicklung.md` | paragraph updated to 23 RLS tables / 3 migrations, `FavoriteLink` no longer named as rule-less | ✓ VERIFIED | Confirmed: 4+3+16=23 tables independently recounted from the three migration files |
| `.planning/WINDOWS.md` | 3 new open entries via `gsd-tools windows append` | ✓ VERIFIED | #30/#31/#32 present, open, header counters consistent |
### Key Link Verification
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `tender-mail.service.ts` / `dkv-mail.service.ts` | `settingsService.getDecryptedSmtpConfig(tenantId)` | direct call | ✓ WIRED | Method now runs over `forTenant()`; Befund K closed |
| `mail.module.ts` `useFactory` | `settingsService.loadAnySmtpConfigForStartupTransport()` | direct call, startup only | ✓ WIRED | Confirmed call site and naming; fallback chain (env vars → localhost:1025) confirmed unchanged |
| `FavoriteLink.widgetId` → `WidgetInstance.id` | app-level ownership check | `tenantPrisma.widgetInstance.findUnique` in `create()` | ✓ WIRED | Live-measured: FK bypasses RLS (Prüfung 7); guard closes the existence-oracle gap |
| `favorites.controller.ts` `extractContext` | `dashboard.controller.ts` (same tenant source) | textual identity of extraction logic | ✓ WIRED | Confirmed identical `req.tenantId ?? req.user?.tenantId` pattern |
| `settings.controller.ts` | `req.tenantId` (unchanged) | direct read | ✓ WIRED | Controller correctly left unchanged per D-10 rationale |
### Behavioral Spot-Checks / Probe Execution
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| Full test suite | `npm --prefix apps/api run test` | 994/994 passed, 62 files | ✓ PASS |
| Type-check | `npm --prefix apps/api run type-check` | exit 0 | ✓ PASS |
| `rls-access-inventory.spec.ts` (doc-vs-source consistency) | `npx vitest run src/prisma/rls-access-inventory.spec.ts` | 11/11 passed | ✓ PASS |
| Generated-client tool, live re-run against DB container | `TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs` | "Alle 137 Pruefungen bestanden." | ✓ PASS |
| Falsification reproduction: widget-ownership guard removed | manual revert + `npx vitest run src/favorites/favorites.service.spec.ts` | 4 failures (matches claimed deviation note), restored cleanly | ✓ PASS |
| `this.prisma.<model>` raw count across `apps/api/src` | `grep -rn "this\.prisma\.[a-zA-Z]*" apps/api/src --include="*.ts" \| grep -v spec \| wc -l` | 68 | ✓ PASS (matches Summenzeile) |
| Class-distribution sums | recomputed from table rows | 33+17+13+2 = 65; 68+178=246 raw hits | ✓ PASS |
### Anti-Patterns Found
None. No `TBD`, `FIXME`, `XXX`, `TODO`, `HACK`, or `PLACEHOLDER` markers found in any modified file. No empty stub implementations. No hardcoded empty data flowing to render paths.
### Constraints Held
- Allow-list scope against `46f0e78`: `git diff --name-only 46f0e78` lists exactly the 12 files declared in `files_modified` (plus the PLAN.md itself, committed separately, and WINDOWS.md) — no unexpected files.
- No schema/migration/compose/environment file appears in the diff.
- Switch remains OFF (`DATABASE_URL` role `tessera`/`BYPASSRLS` unchanged — no env file touched).
- No Active Directory / LDAP code changed (only a comment reference in a doc-string).
- No multi-tenant mail transport built — startup path remains deliberately unbound, only renamed and documented.
### Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| WINDOWS-18 | 260911-gwh-PLAN.md | Etappe 2 fully documented at endstate | ✓ SATISFIED | Classification doc, critique doc, anleitung, ledger all recomputed and matched |
| ETAPPE-2-FAVORITES | 260911-gwh-PLAN.md | favorites.service.ts fully bound with ownership guard | ✓ SATISFIED | Verified directly |
| ETAPPE-2-SETTINGS | 260911-gwh-PLAN.md | settings.service.ts bound, startup path renamed, Befund K closed | ✓ SATISFIED | Verified directly |
### Human Verification Required
None. All must-haves were verifiable programmatically and against a live database container.
### Gaps Summary
No gaps found. Every must-have in the plan's frontmatter was independently re-measured against the current codebase and/or a live database container — not accepted from the SUMMARY's narrative. The one place the SUMMARY itself documents a deviation from its own prediction (4 vs. 3 falsification failures) was independently reproduced and confirmed accurate.
---
_Verified: 2026-09-11_
_Verifier: Claude (gsd-verifier)_