Files
schalli ff23c82220
Tessera CI/CD / Lint & Type Check (push) Successful in 51s
Tessera CI/CD / Tests (push) Successful in 54s
Tessera CI/CD / Build & Publish Images (push) Successful in 27s
docs(quick-260910-krx): Etappe 2 Bereich dashboard abgeschlossen, Luecke behoben
2026-09-11 09:16:40 +02:00

18 KiB

phase, verified, status, score, covered_files, covered_digest, behavior_unverified, overrides_applied, gaps
phase verified status score covered_files covered_digest behavior_unverified overrides_applied gaps
quick-260910-krx 2026-09-11T09:15:00Z gaps_found 10/11 must-haves verified
.planning/WINDOWS.md
.planning/quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/260910-krx-PLAN.md
.planning/quick/260910-krx-mandantentrennung-etappe-2-bereich-dashb/260910-krx-SUMMARY.md
apps/api/scripts/rls-scratch-check.mjs
apps/api/src/dashboard/dashboard.controller.ts
apps/api/src/dashboard/dashboard.service.spec.ts
apps/api/src/dashboard/dashboard.service.ts
docs/mandantentrennung-etappe2-fehlerrichtung.md
docs/mandantentrennung-zugriffsklassifikation.md
v1:sha256:76946e6dc0468eaade2fe79aaa847c342e94364af5040814f495ba1e2c460d09 0 0
truth status reason artifacts missing
Die gemessene Konfliktbehandlung in saveLayout (PrismaClientUnknownRequestError statt P2002) ist im committeten Werkzeug reproduzierbar belegt und durch einen Testfall abgesichert. partial Die im SUMMARY und in docs/mandantentrennung-etappe2-fehlerrichtung.md (Zeilen 1812-1831) als "zentrale Abweichung vom tenders-Muster" dargestellte Kernaussage — dass `tenantPrisma.dashboardLayout.upsert()` am ECHTEN, generierten Prisma Client tatsaechlich einen `PrismaClientUnknownRequestError` wirft, nicht `PrismaClientKnownRequestError` mit `.code === 'P2002'` — ist NICHT im committeten `rls-scratch-check.mjs` abgebildet. Die dortige Pruefung Nr. 5 (`dashboardlayout-gebundener-konfliktschreibvorgang-auf-unsichtbare-zeile-scheitert-laut`, Zeile 2586-2599 des Skripts) ruft ausschliesslich `tx.$executeRaw` mit handgeschriebenem `INSERT ... ON CONFLICT ("userId") DO UPDATE` auf — an keiner Stelle des Skripts wird `.upsert()` ueber den generierten Prisma Client aufgerufen (grep nach `dashboardLayout.upsert` im ganzen Skript: 0 Treffer). Die staerkere, spezifischere Behauptung (generierter Client, andere Prisma-Fehlerklasse als P2002) stammt laut Dokument aus einem separaten, nicht committeten Testaufbau ("eigens dafuer angelegte Wegwerf-Datenbank... eigene Wegwerf-Rolle ohne BYPASSRLS") und ist damit nicht unabhaengig nachvollziehbar. Zusaetzlich: kein Testfall in `dashboard.service.spec.ts` exercised den `catch`-Zweig von `saveLayout` (`grep -n "ConflictException|PrismaClientUnknownRequestError" dashboard.service.spec.ts` — 0 Treffer) — ein Refactoring, das den `catch`-Block entfernt oder auf `.code === 'P2002'` umstellt, wuerde von keinem Test erkannt.
path issue
apps/api/scripts/rls-scratch-check.mjs Pruefung 5 misst nur rohes SQL ($executeRaw), nicht den generierten Prisma-Client-Aufruf .upsert(), der in dashboard.service.ts tatsaechlich verwendet wird.
path issue
apps/api/src/dashboard/dashboard.service.spec.ts Kein Testfall ruft saveLayout mit einem Mock auf, der PrismaClientUnknownRequestError wirft, und prueft die Uebersetzung in ConflictException.
Entweder: Erweiterung von rls-scratch-check.mjs um eine Pruefung, die tatsaechlich prisma.dashboardLayout.upsert() (generierter Client) gegen die Konfliktzeile aufruft und die beobachtete Fehlerklasse (err.constructor.name bzw. instanceof-Pruefung) protokolliert.
Oder: ein Unit-Test in dashboard.service.spec.ts, der tenantPrisma.dashboardLayout.upsert im Fake mit einem Prisma.PrismaClientUnknownRequestError-Wurf versieht und erwartet, dass saveLayout eine ConflictException wirft — analog zum bestehenden Wachhund-Muster dieser Datei.
Reproduzierbarer Beleg (Skript oder Test), der im Repository landet, statt einer nur im Dokumentationstext behaupteten, einmaligen Ad-hoc-Messung.

Quick Task 260910-krx: Mandantentrennung Etappe 2, Bereich dashboard — Verification Report

Task Goal: Bind the tenant-bound access sites in apps/api/src/dashboard/dashboard.service.ts, leave the platform-wide module catalogue unbound with the measured reason, create the missing spec coverage, record the evidence-destroying loop, and leave the classification document's five hand-maintained sections in sync.

Verified: 2026-09-11T09:15:00Z Status: gaps_found (one partial gap — see below; all other checked items pass) Re-verification: No — initial verification

Goal Achievement

Observable Truths

# Truth Status Evidence
1 13 Datenbankzugriffe vollstaendig entschieden: 12 gebunden, 1 begruendet ungebunden, keiner unentschieden ✓ VERIFIED git show c7d93f2:.../dashboard.service.ts | grep -oE "this\.prisma\.[a-zA-Z]+" | wc -l = 13 (2 dashboardLayout, 1 module, 4 searchProvider, 6 widgetInstance) at base; current file: 12 tenantPrisma.* call sites over 9 forTenant() invocations + 1 this.prisma.module.findMany (unbound, catalogue). Zero occurrences of tenantPrisma.module. (negative gate confirmed by direct grep of the committed file).
2 Lesen/Schreiben derselben Tabelle nie auf gebunden/ungebunden aufgeteilt; getLayout/saveLayout gemeinsam gebunden; drei Besitzpruefungen laufen ueber denselben Klienten ✓ VERIFIED Read dashboard.service.ts: every method creates exactly one const tenantPrisma = forTenant(this.prisma, tenantId) and both queries of updateWidgetConfig, removeWidget, removeSearchProvider use that same tenantPrisma. Test "Anordnung lesen und speichern sind GEMEINSAM gebunden" and "keine Methode ... erzeugt mehr als EINEN gebundenen Klienten" pin this down; both pass (27/27).
3 Umgekehrte Fehlerrichtung an den echten Regeln nach Migration 20260910120000 gemessen, mit Signal je Pfad ✓ VERIFIED Re-ran rls-scratch-check.mjs live against tessera-ctl-db-1 (172.19.0.2): "Alle 87 Pruefungen bestanden." (74 baseline + 13 new, matching the SUMMARY's claim exactly). docs/mandantentrennung-etappe2-fehlerrichtung.md ## Bereich dashboard (w1)/(w2) present with per-path signal table.
4 Beweisvernichtende Schleife ausdruecklich benannt: in Kritikschrift UND offener WINDOWS-Eintrag mit Etappe-4-Vorabpruefung ✓ VERIFIED (w3) in fehlerrichtung.md describes the loop (empty dashboard → rebuild → auto-writeback → overwritten row → duplicate widgets) at the two actual web files. .planning/WINDOWS.md entry #25 present in both the markdown table and the JSON block, status: open, names the concrete Etappe-4 preflight check and cross-references #22. Frontend files (apps/web/**) confirmed untouched by git diff --name-only c7d93f2..HEAD.
5 Widerlegte SearchProvider-Praemisse eigenstaendig nachgeprueft, mit benannter Reichweite ✓ VERIFIED (w1) names the exact search instruction and scope limits (no dynamic model-name write path, no manual DB edit covered). Database-side defense reproduced live: searchprovider-gebundenes-einfuegen-ohne-mandant-abgelehnt: bestanden with SQLSTATE 42501.
6 Geerbte Entlastung (Modul-Zugriffsaufloesung bereits gebunden seit 260910-exd) nachgeprueft, nicht doppelt repariert ✓ VERIFIED git diff --name-only c7d93f2..HEAD -- apps/api/src/module-registry/ returns nothing — no file under that path touched. dashboard.service.ts calls this.moduleAccessService.getAccessibleModuleIds(...) unchanged, with an explicit comment that it "already binds internally".
7 Modulkatalog bleibt ungebunden, MESSUNG und BEDINGUNG getrennt, beruft sich auf bestehende Werkzeugpruefung ✓ VERIFIED End-of-file comment block in dashboard.service.ts (lines 340-354) separates MESSUNG (pg_class.relrowsecurity false, cites module-tabelle-traegt-keinen-zeilenschutz) from BEDINGUNG (becomes catastrophic once Etappe 3 adds a rule). module-tabelle-traegt-keinen-zeilenschutz: bestanden reproduced live.
8 Testlage deckt vorher ungetestete Pfade ab; vergessener Bindungsaufruf wird rot ✓ VERIFIED dashboard.service.spec.ts: 27 test cases (8 pre-existing, unchanged; 19 new, counted). Reverted tenantPrisma.widgetInstance.delete → this.prisma.widgetInstance.delete in removeWidget: exactly the claimed test failed with the exact claimed message; restored, 27/27 green again (independently reproduced, see Behavioral Spot-Checks).
9 Regeln kennen keine Benutzerdimension; Besitzpruefungen ueber Benutzerkennung bleiben einziger Schutz, unveraendert ✓ VERIFIED widget.userId !== userId present in updateWidgetConfig and removeWidget; provider.userId !== userId present in removeSearchProvider — all three intact, all comparing against the session-sourced userId parameter, not touched by the binding. Three *-fremder-nutzer-desselben-mandanten-gebunden-sichtbar checks pass (gelingen IS the expected/passing result), confirming the DB layer alone has no user dimension.
10 Baseline gehalten am Ende jeder Aufgabe (≥839 Tests, ≥56 Dateien, saubere Typpruefung, Werkzeug 0) ✓ VERIFIED Per orchestrator's independent measurement: 858/858 tests green across 56 files, type-check exit 0. Independently reproduced for the two most relevant suites (dashboard.service.spec.ts 27/27, rls-access-inventory.spec.ts 10/10) and for rls-scratch-check.mjs (87/87, live run).
11 Schalter bleibt AUS; kein Schema-, Migrations-, Compose- oder Umgebungs-Datei angefasst ✓ VERIFIED git diff --name-only c7d93f2..HEAD touches exactly 9 files (confirmed independently), none under apps/api/prisma, no compose/env file. groups.service.ts and apps/api/src/module-registry/** confirmed untouched.
12 Die gemessene Konfliktbehandlung in saveLayout ist im committeten Werkzeug reproduzierbar belegt und durch einen Test abgesichert ✗ FAILED (partial) See gaps in frontmatter. The committed rls-scratch-check.mjs only measures a raw-SQL $executeRaw ... ON CONFLICT path (reproduced live: SQLSTATE 42501), never the actual generated prisma.dashboardLayout.upsert() call that saveLayout uses. The specific, stronger claim in the SUMMARY/critique document — that the generated client throws PrismaClientUnknownRequestError rather than the P2002-shaped PrismaClientKnownRequestError — is attributed to a separate, non-committed ad-hoc measurement and is not independently reproducible from repo state. No unit test exercises saveLayout's catch branch (0 hits for ConflictException/PrismaClientUnknownRequestError in dashboard.service.spec.ts).

Score: 10/11 truths fully verified, 1 partial gap (0 present-but-behavior-unverified truths; the one gap is a documented, provable absence, not an uncertainty).

Deferred Items

None identified — no later-phase success criteria in the current milestone roadmap were found to cover this gap; this quick task is not tied to a numbered ROADMAP phase, so no deferral cross-check applies.

Required Artifacts

Artifact Expected Status Details
apps/api/scripts/rls-scratch-check.mjs 9th section runDashboardAreaChecks, 13 named checks ✓ VERIFIED Present at line 2425; live re-run: "Alle 87 Pruefungen bestanden."
docs/mandantentrennung-etappe2-fehlerrichtung.md ## Bereich dashboard with (w1)-(w5) + Nachtrag in ## Bereich module-registry ✓ VERIFIED Section at line 1745; (w4)/(w5) at 1930/1966; Nachtrag at 1614.
apps/api/src/dashboard/dashboard.service.ts 12 bound, 1 unbound-with-reason ✓ VERIFIED Confirmed by direct read and grep counts above.
apps/api/src/dashboard/dashboard.controller.ts 5 handlers pass tenant through ✓ VERIFIED All 8 handlers call extractContext(req) and pass tenantId positionally after userId, matching service signatures.
apps/api/src/dashboard/dashboard.service.spec.ts Two-client TDD proof, 8→27 cases, all 8 original green ✓ VERIFIED 27/27 passing; describe block 1 (getWidgets — Modulfilter) unchanged with 8 cases.
docs/mandantentrennung-zugriffsklassifikation.md 4 Bestandsaufnahme rows, overview row, sum row, class distribution, background-service section, "Was NICHT entscheidet" ✓ VERIFIED All five confirmed by direct read; rls-access-inventory.spec.ts 10/10 (machine-gated consistency).
.planning/WINDOWS.md Open entry for evidence-destroying loop, table + JSON ✓ VERIFIED Entry #25 present in both forms, status: open.
From To Via Status Details
getLayout/saveLayout Same DashboardLayout row Shared userId uniqueness key, no tenant component ✓ WIRED Both bound over the same tenantPrisma per call; co-binding test present and passing.
dashboard.controller.ts dashboard.service.ts extractContext(req) → positional tenantId argument ✓ WIRED All 8 handlers pass tenantId from session-derived context, no new trust source introduced.
dashboard.service.ts getWidgets module-access.service.ts getAccessibleModuleIds Direct call, already bound since 260910-exd ✓ WIRED Confirmed unchanged; module-registry files untouched by this diff.
dashboard.service.ts saveLayout catch branch ConflictException instanceof Prisma.PrismaClientUnknownRequestError ⚠️ WIRED BUT UNTESTED Code path exists and reads correctly, but no test exercises it — see gap above.

Behavioral Spot-Checks

Behavior Command Result Status
rls-scratch-check.mjs reproducibly passes against live DB TESSERA_SCRATCH_ADMIN_URL=... node apps/api/scripts/rls-scratch-check.mjs "Alle 87 Pruefungen bestanden." ✓ PASS
rls-access-inventory.spec.ts reproducibly passes npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts 10/10 ✓ PASS
Falsification proof 1 (removeWidget binding reverted) Edited tenantPrisma.widgetInstance.delete → this.prisma.widgetInstance.delete, ran suite, reverted Exact claimed test failed with exact claimed message; restored clean, 27/27 green ✓ PASS
Falsification proof 2 (module catalogue bound) Edited this.prisma.module.findMany → tenantPrisma.module.findMany, ran suite, reverted 8 tests failed incl. the named watchdog, TypeError: Cannot read properties of undefined (reading 'findMany') at dashboard.service.ts:175 — matches SUMMARY verbatim; restored clean, 27/27 green ✓ PASS
Falsification proof 3 (classification Stand set wrong) Edited row 320 gebunden→ungebunden, ran inventory spec, reverted Failed with exact claimed mismatch message; restored clean, 10/10 green ✓ PASS
saveLayout ConflictException translation Searched for a test exercising it 0 hits for ConflictException/PrismaClientUnknownRequestError in dashboard.service.spec.ts ✗ FAIL (gap, see above)

Anti-Patterns Found

None (TBD/FIXME/XXX/TODO/HACK/placeholder scan of all 9 changed files: no hits). No hardcoded-empty-return stubs found; getLayout's default-arrangement return and getWidgets'/getSearchProviders' empty-list behavior are explicitly documented as intentional, pre-existing "emptiness as absence" semantics, not stubs introduced by this task.

Requirements Coverage

No .planning/REQUIREMENTS.md entries exist for WINDOWS-18 or ETAPPE-2-DASHBOARD (this is a quick task, not tracked against the formal requirements ledger) — not orphaned, simply out of scope for that document's tracking.

Human Verification Required

None — the one gap found (saveLayout conflict-translation test/measurement coverage) is a provable absence, not an item requiring human judgment to establish the facts. It is reported as a gaps_found item so a human can decide whether to accept it via an override or request a follow-up fix.

Gaps Summary

Twelve of thirteen checked truths verify cleanly against the codebase, and three separate, independently-reproduced falsification proofs (all three claimed in the SUMMARY) matched verbatim — this is a well-evidenced piece of work overall, with the classification document, the WINDOWS ledger entry, the critique document sections, the ownership checks, and the "module catalogue stays unbound" negative gate all holding up under direct adversarial re-verification.

The one real gap: the SUMMARY's most load-bearing new technical claim — that a bound conflicting saveLayout write throws PrismaClientUnknownRequestError (not the P2002-shaped error the tenders area handles) — is asserted with unusual specificity ("am echten, generierten Prisma Client gemessen, nicht nur an rohem SQL") but that specific measurement was not committed anywhere reproducible: rls-scratch-check.mjs's check 5 only exercises raw SQL via $executeRaw, and no unit test exercises saveLayout's catch (error) branch. The production code itself (instanceof Prisma.PrismaClientUnknownRequestError) is plausible and well-reasoned, and the raw-SQL measurement it's grounded in did reproduce live with SQLSTATE 42501 — but the exact claim made in the document goes beyond what's in the repository, and nothing would catch a regression if the catch clause were later changed or removed.

This looks like a real, if narrow, evidence gap rather than intentional scope creep — the fix is small (either extend the scratch tool to call the real .upsert() and log the observed error class, or add one unit test asserting saveLayout throws ConflictException when the bound client's upsert rejects with a Prisma.PrismaClientUnknownRequestError). To accept the current state as-is instead of requiring that follow-up, add to this file's frontmatter:

overrides:
  - must_have: "Die gemessene Konfliktbehandlung in saveLayout ist im committeten Werkzeug reproduzierbar belegt und durch einen Testfall abgesichert."
    reason: "Die Codepfad-Logik ist inhaltlich plausibel und durch eine verwandte (rohe SQL) Messung gestuetzt; die fehlende .upsert()-spezifische Messung/der fehlende Unit-Test werden als Nachtrag statt als Blocker akzeptiert."
    accepted_by: "<name>"
    accepted_at: "<ISO timestamp>"

Verified: 2026-09-11T09:15:00Z Verifier: Claude (gsd-verifier)