20 KiB
phase, verified, status, score, covered_files, covered_digest, overrides_applied, behavior_unverified
| phase | verified | status | score | covered_files | covered_digest | overrides_applied | behavior_unverified | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260910-exd | 2026-09-10T00:00:00Z | passed | 9/9 must-haves verified |
|
v1:sha256:47e75c553a401d96420242d168f37b0fbc0f89e54dfac382bd3ee80ba1d04a0c | 0 | 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.tsmocksforTenantto identity (vi.fn((p) => p)) — this is true, and by itself would be the exact defect this effort documented inldap.- 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, 16it()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. ItsactivateForTenantdescribe block (lines 182–207) assertsexpectBoundCall(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 indocs/mandantentrennung-etappe2-fehlerrichtung.md(see below) with a concrete red message, not just a commit-log claim. - Checked whether "the exact
ldap.service.spec.tsconvention" claim holds up:ldap.service.spec.tsdoes 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 theforTenantspy per-test and assertsexpect(forTenant).toHaveBeenCalledWith(...)forlistGroups,searchUsers, etc.ldap-config.service.spec.tsfollows the identical two-part pattern (identity mock at top, line 9–10; dedicated "Bindung an forTenant()" describe block at line 197 assertingtoHaveBeenCalledWith). So the convention is real, not a misreading.tender-scheduler.service.spec.tsonly needed the "identity mock" half of that convention, because — unlikeldap.service.spec.tstesting itself — it doesn't testModuleRegistryService's binding at all; it testsTenderSchedulerService's poll-once-fan-out-many behavior using the realModuleRegistryServiceas an unmocked collaborator. The binding proof forModuleRegistryService.activateForTenantcorrectly lives inmodule-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
- Critique text, unvarnished:
docs/mandantentrennung-etappe2-fehlerrichtung.md(m3), "Die Antwort lautet keines — schlicht, ohne Beschönigung," with the three identical-looking spots named (sameForbiddenExceptionmessage, same empty 200 list, no log line) and the added observation that the affected user has a ready, false explanation. - Test case:
module.guard.spec.tslines 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. - 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-registrypresent with the correctStand(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
awkpass over everyapps/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 statesmodule-registryadds **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.tstest "USER-Zweig bindet BEIDE..." went red withexpected 1 to be 2; reverted, 23/23 green again. - Task 3:
deactivateForTenantwrite rollback →module-registry.service.spec.tstest "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*.exampleappear in the diff file list. assertTargetBelongsToTenantstill present and called twice inapps/api/src/groups/module-grants.service.ts(lines 46, 103, 242) — unchanged.- No
dashboardfile 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.tsremains the only protection until Etappe 4. - Full diff file list (10 files) matches exactly what the plan's
files_modifiedfrontmatter 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)