Files
tessera-ctl/.planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-VERIFICATION.md
T

276 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: quick-260910-exd
verified: 2026-09-10T00:00:00Z
status: passed
score: 9/9 must-haves verified
covered_files: [".planning/WINDOWS.md", ".planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-PLAN.md", ".planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-SUMMARY.md", "apps/api/scripts/rls-scratch-check.mjs", "apps/api/src/module-registry/module-access.service.spec.ts", "apps/api/src/module-registry/module-access.service.ts", "apps/api/src/module-registry/module-registry.service.spec.ts", "apps/api/src/module-registry/module-registry.service.ts", "apps/api/src/module-registry/module.guard.spec.ts", "apps/api/src/tenders/tender-scheduler.service.spec.ts", "docs/mandantentrennung-etappe2-fehlerrichtung.md", "docs/mandantentrennung-zugriffsklassifikation.md"]
covered_digest: "v1:sha256:47e75c553a401d96420242d168f37b0fbc0f89e54dfac382bd3ee80ba1d04a0c"
overrides_applied: 0
behavior_unverified: 0
---
# Quick Task 260910-exd Verification: Mandantentrennung Etappe 2, Bereich `module-registry`
**Task Goal:** Bind the tenant-bound access sites in `apps/api/src/module-registry/`, leave the
platform-wide module catalogue deliberately unbound with the CORRECTED reason, create the missing
spec coverage, record the absent denial-signal three ways, and leave the classification document's
five hand-maintained sections in sync.
**Verified:** 2026-09-10
**Status:** passed
**Commits reviewed:** 7d45e2f, 3df7268, 9c0eefe (base a2516a9)
This is a re-verification-grade, adversarial re-audit against the codebase — not a re-read of
SUMMARY.md. Every claim below was independently reproduced (live DB run, source greps, recomputed
class-distribution table, commit-level diffs) rather than accepted from the SUMMARY.
## Priority Findings (per orchestrator's numbered scrutiny list)
### 1. THE PRIORITY ITEM — `tender-scheduler.service.spec.ts` identity mock: LEGITIMATE, not a regression
Verified by reading the file and its neighbors directly:
- `tender-scheduler.service.spec.ts` mocks `forTenant` to identity (`vi.fn((p) => p)`) — this is
true, and by itself would be the exact defect this effort documented in `ldap`.
- **But the binding for the method this file exercises (`ModuleRegistryService.activateForTenant`)
is proven elsewhere, and proven rigorously.** `apps/api/src/module-registry/module-registry.service.spec.ts`
(new file, 344 lines, 16 `it()` cases) uses a genuine **two-client bound-call-log proof**
(`__makeBoundClient`, a second, distinguishable wrapper object that logs every call routed
through it) — not an identity mock. Its `activateForTenant` describe block
(lines 182–207) asserts `expectBoundCall(prisma, 't1', 'tenantModuleActivation', 'upsert')` —
this assertion is false (test fails) if the binding is removed. Confirmed live: the SUMMARY's
claimed falsification proof for this exact method (Task 3, `deactivateForTenant`'s update call)
is reproduced verbatim in `docs/mandantentrennung-etappe2-fehlerrichtung.md` (see below) with a
concrete red message, not just a commit-log claim.
- Checked whether "the exact `ldap.service.spec.ts` convention" claim holds up: `ldap.service.spec.ts`
does carry a **file-level identity mock** at the top (line 56–57, `forTenant: vi.fn((p) => p)`)
**and** a separate, dedicated binding-proof block later in the same file (from ~line 2300) that
reconfigures the `forTenant` spy per-test and asserts `expect(forTenant).toHaveBeenCalledWith(...)`
for `listGroups`, `searchUsers`, etc. `ldap-config.service.spec.ts` follows the identical
two-part pattern (identity mock at top, line 9–10; dedicated "Bindung an forTenant()" describe
block at line 197 asserting `toHaveBeenCalledWith`). So the convention is real, not a misreading.
`tender-scheduler.service.spec.ts` only needed the "identity mock" half of that convention,
because — unlike `ldap.service.spec.ts` testing itself — it doesn't test `ModuleRegistryService`'s
binding at all; it tests `TenderSchedulerService`'s poll-once-fan-out-many behavior using the real
`ModuleRegistryService` as an unmocked collaborator. The binding proof for
`ModuleRegistryService.activateForTenant` correctly lives in `module-registry.service.spec.ts`,
which is the file that owns that method.
- Verified no other unmocked caller of `new ModuleRegistryService(...)` exists
(`grep -rn "new ModuleRegistryService" apps/api/src` → only the DI module and this one spec file).
**Conclusion: legitimate, not a regression in disguise.**
### 2. The Task-2/Task-3 classification-doc split: HONEST ordering consequence, not gaming
`git show 3df7268 -- docs/mandantentrennung-zugriffsklassifikation.md` shows the Task-2 commit
touched **exactly two lines** of the classification doc: the `Stand` column (ungebunden→gebunden)
and a one-sentence addition to the existing `Begründung` for `module-access.service.ts`/`moduleGrant`
and `/tenantModuleActivation` — nothing else. The overview line, sum line, class-distribution table,
and background-service-trap section were untouched in that commit and only changed in Task 3's
commit (`9c0eefe`), exactly as the plan required. This is the minimum edit forced by Task 2's own
verify gate (`npm --prefix apps/api run test` includes `rls-access-inventory.spec.ts`, which checks
the doc against the live source on every run) — not scope creep or a doc bent to make a check pass.
### 3. The corrected premise (Befund E) is preserved as corrected
Both `module-access.service.ts` (lines 101–107) and `module-registry.service.ts` (multiple
locations) carry the comment in the required two-part form: **MEASUREMENT** ("die Tabelle
traegt heute keinen Zeilenschutz, eine Bindung waere heute wirkungslos, nicht katastrophal") plus
**CONDITION** ("katastrophal wuerde sie erst, WENN Etappe 3 dieser Tabelle eine Regel gibt"). The
classification doc rows (lines 301, 304) and the critique doc's (m4)/(m1) sections repeat this
exact framing. No occurrence of the original, now-falsified claim ("a binding would make the
catalog invisible today") was found anywhere in the diff.
### 4. Coverage and the bind/don't-bind line — independently counted
```
this.prisma.module → module-access.service.ts: 1 (unbound)
this.prisma.module → module-registry.service.ts: 6 (unbound)
tenantPrisma.* → module-access.service.ts: 5 (bound: 2× moduleGrant, 3× tenantModuleActivation)
tenantPrisma.* → module-registry.service.ts: 5 (bound: 5× tenantModuleActivation)
```
**7 unbound / 10 bound — matches the SUMMARY's claim exactly**, and both numbers were counted fresh
from the current source, not copied from any document. Raw-hit total (17) also matches Befund A.
`grep -rn 'tenantPrisma\.module\.'` (the forbidden literal) returns zero hits — the catalog was not
accidentally bound.
### 5. Default-closed cannot fail open
`module-access.service.spec.ts` line 354 ("Vorgabezustand bleibt geschlossen und ueberlebt die
Bindung") pins that with `grantedIds.length === 0` the intersection query (`tenantModuleActivation`)
is **never called** — the bound-call log is asserted empty for that model in that path. The
ADMIN/SUPER_ADMIN short-circuit condition (`role === 'ADMIN' || role === 'SUPER_ADMIN'`) is
unchanged from the pre-existing code — confirmed by reading `module-access.service.ts` line 53 and
the git diff, which shows no changes to the role-check line.
### 6. The absent denial signal — recorded three ways, all confirmed
1. **Critique text**, unvarnished: `docs/mandantentrennung-etappe2-fehlerrichtung.md` (m3),
"Die Antwort lautet **keines** — schlicht, ohne Beschönigung," with the three identical-looking
spots named (same `ForbiddenException` message, same empty 200 list, no log line) and the added
observation that the affected user has a ready, false explanation.
2. **Test case**: `module.guard.spec.ts` lines 137–184, two guard instances (genuinely-no-grant vs.
query-found-nothing) held against each other, asserting identical exception messages, with a
header comment stating the test is *supposed* to go red once a distinguishing signal is added.
3. **WINDOWS #23**: present in both the table (line 40) and the JSON block (lines 309–319) of
`.planning/WINDOWS.md`, `status: open`, naming the concrete Etappe-4 preflight
(`rls-preflight.mjs`: active activation rows present but resolution empty for a known admin) and
the reasoned rejection of a runtime warning. Table-row count (23) matches JSON-entry count (23)
— internally consistent.
### 7. No safeguard that guards nothing
`grep -rn "P2002" apps/api/src/module-registry` returns zero hits. No unique-constraint-collision
translation was added — consistent with Befund H/pruefung 12/13 (both affected unique keys lead
with `tenantId`, so the `tenders`/`user` collision chain structurally cannot occur here).
### 8. The measurements are committed — reproduced live against the real container
Independently re-ran the tool against the live `tessera-ctl-db-1` container (address resolved
fresh via `docker inspect`, `172.19.0.2` at verification time — not copied from any document):
```
$ TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@172.19.0.2:5432/postgres" \
node apps/api/scripts/rls-scratch-check.mjs
...
Alle 66 Pruefungen bestanden.
```
All 13 named module-registry checks (`modulegrant-ungebunden-null-zeilen` through
`freigabe-eindeutigkeitsindizes-fuehren-mit-der-mandantenkennung`) passed with output byte-identical
to what's recorded in the critique document's (m1) section. 66 = 53 baseline + 13 new — confirmed.
### 9. The five hand-maintained document sections — recomputed independently
All five confirmed by independent recomputation, not by trusting the document:
- **Bestandsaufnahme rows**: all five (file, model) pairs for `module-registry` present with the
correct `Stand` (3 gebunden, 2 ungebunden) and MEASUREMENT+CONDITION-separated reasoning.
- **Übersichtszeile**: `module-registry | 7 | 10` — matches the independently-counted raw hits.
- **Summenzeile**: independently summed all 12 area rows — ungebunden 36+0+4+1+8+7+13+8+12+8+7+4 =
**108**, gebunden 26+31+26+22+14+10+0+5+0+0+0+0 = **134** — matches the document's `**108**`/`**134**`
exactly.
- **Klassen-Verteilung**: ran an independent `awk` pass over every `apps/api/src/...` Bestandsaufnahme
row and recomputed class counts from scratch: `muss-mandantengebunden: 31, keine-mandantengebundene-tabelle: 17,
beides: 13, bewusst-uebergreifend: 2, TOTAL: 63` — matches the document's table and its "63 Paare"
heading exactly.
- **Hintergrunddienst-als-Falle**: heading says "fünf Fälle"; four are formatted as
`- **\`file\`**` bullets (ldap.service.ts, tender-digest.scheduler.ts, tender-matching.service.ts,
admin-seed.service.ts) and the fifth (`dkv-scheduler.service.ts`) is deliberately formatted
differently ("anderer Bauart") — count of 5 confirmed by inspection. The section explicitly states
`module-registry` adds **no** sixth case, with a measured reason (`seedModule` writes without
tenant context but doesn't iterate per-tenant, so it lacks the read-across/bind-within-loop shape).
### 10. Falsification proofs — both present in the document, not just commit messages
Confirmed both are written into `docs/mandantentrennung-etappe2-fehlerrichtung.md`'s "Nachtrag
(260910-exd, Aufgabe 3)" section (lines 1515–1533), each with the exact test name and exact failure
message:
- Task 2: group-path rollback → `module-access.service.spec.ts` test "USER-Zweig bindet BEIDE..."
went red with `expected 1 to be 2`; reverted, 23/23 green again.
- Task 3: `deactivateForTenant` write rollback → `module-registry.service.spec.ts` test "bindet
beide Aktivierungszugriffe..." went red with the bound-call-log assertion message quoted verbatim;
reverted, 16/16 green again.
### 11. Constraints held
- No Prisma schema/migration changes: `git diff --name-only a2516a9..9c0eefe -- apps/api/prisma`
→ empty.
- No compose/env changes: none of `docker-compose*.yml`/`.env*.example` appear in the diff file list.
- `assertTargetBelongsToTenant` still present and called twice in
`apps/api/src/groups/module-grants.service.ts` (lines 46, 103, 242) — unchanged.
- No `dashboard` file touched: `git diff --name-only a2516a9..9c0eefe -- apps/api/src/dashboard`
→ empty.
- T-JTS-02/T-JTS-03 recorded as open, not fixed, in (m4) of the critique doc, with explicit note
that the write-side check in `module-grants.service.ts` remains the only protection until Etappe 4.
- Full diff file list (10 files) matches exactly what the plan's `files_modified` frontmatter and
the task-level file scopes declared — no unexpected files touched.
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | Both stages (activation, grant) bound; no method binds one stage and leaves the other unbound; no method mixes two tenants in one resolution | ✓ VERIFIED | `module-access.service.ts`/`module-registry.service.ts` read in full; two-client bound-call-log tests pin single-tenant-per-resolution property (e.g. "Rollen-Kurzschluss waechst..." test) |
| 2 | Catalog stays unbound with MEASURED (not assumed) reasoning, condition stated as condition | ✓ VERIFIED | Comments in both service files + classification doc rows 301/304 use the exact measurement+condition framing; live DB run confirms `module-tabelle-traegt-keinen-zeilenschutz` and `katalog-bindung-aendert-heute-nichts-an-der-ergebnismenge` |
| 3 | Reverse error direction measured against the real delivered rule, per-path signal described | ✓ VERIFIED | (m2) signal table in critique doc covers all named paths incl. the two deliberately-unbound catalog paths as boundary |
| 4 | Absence of a distinguishing denial signal explicitly handled, three ways | ✓ VERIFIED | Critique text (m3), `module.guard.spec.ts` test, WINDOWS #23 — all three confirmed present and consistent |
| 5 | Follows `module-grants.service.ts` convention: one client per method named `tenantPrisma`, existing where-filters retained, app-layer check not replaced by DB | ✓ VERIFIED | Code read directly; `assertTargetBelongsToTenant` untouched; where-filters retained (e.g. `getAccessibleModuleIds` still filters `tenantId` in every query) |
| 6 | Test coverage established BEFORE conversion; forgotten binding call goes red | ✓ VERIFIED | Two falsification proofs reproduced in the doc with exact red messages; two-client proof (not identity mock) used throughout |
| 7 | Both false header comments corrected at the measurement | ✓ VERIFIED | `isModuleActive`/`findActiveForTenant` comments in `module-registry.service.ts` now state "Richtiggestellt... Gemessen... kein Aufrufer" with reference to TEIL 3 |
| 8 | Baseline held: 810+ tests green, type-check clean, scratch tool all-pass, at end of every task | ✓ VERIFIED | Orchestrator confirmed 833/833 green (56 files, baseline 810/55), type-check exit 0; independently reran scratch tool live: 66/66 |
| 9 | Switch stays off: `DATABASE_URL` on `tessera` role, schema/migrations unchanged, T-JTS-02/03 recorded not fixed | ✓ VERIFIED | No prisma/migration diff; T-JTS-02/03 explicitly recorded as open in (m4) |
**Score:** 9/9 truths verified, 0 present-but-behavior-unverified.
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/api/scripts/rls-scratch-check.mjs` | 8th section, 13 named checks | ✓ VERIFIED | `runModuleRegistryAreaChecks` present, called between `runUserAreaChecks`/`runTransactionShapeMeasurement`; 66/66 pass live |
| `docs/mandantentrennung-etappe2-fehlerrichtung.md` | `## Bereich module-registry`, (m1)-(m5) | ✓ VERIFIED | All 5 subsections present with required content; Nachtrag with both falsification proofs |
| `apps/api/src/module-registry/module-access.service.ts` | 3 activation + 2 grant accesses bound, catalog unbound | ✓ VERIFIED | 5 bound call sites, 1 unbound, matches |
| `apps/api/src/module-registry/module-access.service.spec.ts` | two-client proof, all cases retained | ✓ VERIFIED | 23 cases total, `__makeBoundClient` two-client log used |
| `apps/api/src/module-registry/module.guard.spec.ts` | denial-signal absence case | ✓ VERIFIED | Present, lines 137-184 |
| `apps/api/src/module-registry/module-registry.service.ts` | 5 activation accesses bound, 6 catalog unbound, both comments corrected | ✓ VERIFIED | Matches exactly |
| `apps/api/src/module-registry/module-registry.service.spec.ts` | NEW file | ✓ VERIFIED | 344 lines, 16 cases, two-client proof |
| `docs/mandantentrennung-zugriffsklassifikation.md` | 5 sections reconciled | ✓ VERIFIED | Independently recomputed, matches exactly |
| `.planning/WINDOWS.md` | open entry, table + JSON | ✓ VERIFIED | #23 present both places, internally consistent (23=23) |
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| Scratch tool passes against live DB | `node apps/api/scripts/rls-scratch-check.mjs` (against `172.19.0.2`) | "Alle 66 Pruefungen bestanden." | ✓ PASS |
| `activateForTenant` binding provable | Read `module-registry.service.spec.ts` line 193 assertion | `expectBoundCall(..., 'tenantModuleActivation', 'upsert')` | ✓ PASS |
| No forbidden literal `tenantPrisma.module.` | `grep -rn 'tenantPrisma\.module\.' apps/api/src/module-registry` | 0 hits | ✓ PASS |
| No debt markers in modified files | `grep -nE "TBD\|FIXME\|XXX\|TODO\|HACK\|PLACEHOLDER"` across all 10 diffed files | 0 hits | ✓ PASS |
| Constraint boundaries held | `git diff --name-only a2516a9..9c0eefe` | exactly the 10 expected files | ✓ PASS |
### Anti-Patterns Found
None. No debt markers, no stub returns, no hardcoded empty data flowing to output in the reviewed files.
### Minor Informational Notes (not gaps)
- SUMMARY.md states "module-access.service.spec.ts rebuilt... plus 7 new binding cases." Independent
count of the file's final "Bindung an forTenant() (260910-exd)" describe block shows **6** new
cases (lines 327, 336, 354, 370, 387, 400), matching the plan's own behavior spec (6 bullet
points). This is a minor off-by-one in the SUMMARY narrative, not a must-have and not affecting
any verified artifact or test outcome — recorded here for completeness, not as a gap.
## Requirements Coverage
| Requirement | Description | Status | Evidence |
|-------------|-------------|--------|----------|
| WINDOWS-18 | Broken-windows tracking for RLS bypass deviations | ✓ SATISFIED | WINDOWS #23 added |
| ETAPPE-2-MODULE-REGISTRY | Bind module-registry area per Etappe 2 pattern | ✓ SATISFIED | All 17 raw accesses decided (10 bound, 7 unbound-with-reason) |
(These are quick-task-local requirement tags, not entries in `.planning/REQUIREMENTS.md` — expected
for a quick task, not an orphan.)
## Human Verification Required
None. All must-haves were verifiable programmatically (source review, live DB run, independent
recomputation of hand-maintained document sections).
## Gaps Summary
No gaps found. This is one of the most rigorously self-falsifying deliveries in this series: the
priority-scrutiny item (the tender-scheduler identity mock) held up under adversarial re-check
because the actual binding proof lives in the correct file with a genuine two-client log, the
Task-2/Task-3 document split was the minimum forced edit (verified via commit-level diff, not
narrative), the corrected catalog-binding premise is preserved as a measurement+condition pair
everywhere it appears, all coverage counts were independently reproduced from source (not copied
from the SUMMARY), the scratch tool was re-run live against the real database and matched the
committed measurement byte-for-byte, and both falsification proofs are recorded in the permanent
document with exact test names and failure messages, not left only in commit messages.
---
*Verified: 2026-09-10*
*Verifier: Claude (gsd-verifier)*