16 KiB
phase, verified, status, score, covered_files, covered_digest, behavior_unverified, overrides_applied
| phase | verified | status | score | covered_files | covered_digest | behavior_unverified | overrides_applied | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| quick-260910-jab | 2026-09-10T14:55:00Z | passed | 11/11 must-haves verified |
|
v1:sha256:4ba76478ddf3d04462ca28c23051e03f469ffd731ae9db235c5cd3e25c803e6f | 0 | 0 |
Quick 260910-jab: Die drei zu kurz greifenden Datenbankregeln — Verification Report
Task Goal: Close T-JTS-02 (GroupMembership checked only the group side),
T-JTS-03 (ModuleGrant checked its own tenant but not the group/user it
points at) and WINDOWS #19 (nullable tenantId would hide platform-wide rows
from every tenant after cutover) — while leaving the written record coherent.
Verified: 2026-09-10, independently, against the live database container
tessera-ctl-db-1 (address 172.19.0.2, resolved fresh via docker inspect)
and the working tree at commits f4f3115, 6b23735, 03fb3bf (base 93444aa).
Status: passed
Summary
Every priority item in the verification brief was independently re-checked, including three destructive falsification experiments (temporarily reverting each of the three fixed policies in the migration file and re-running the scratch tool against a throwaway database) to prove the inverted checks would actually fail if the fix regressed. All three did. Full test suite (839/839), type-check (clean), and the scratch tool (74/74) were re-run independently and match the SUMMARY's claims exactly. No discrepancy found.
Goal Achievement
Observable Truths
| # | Truth | Status | Evidence |
|---|---|---|---|
| 1 | GroupMembership policy checks both sides (group AND user) |
✓ VERIFIED | Live pg_policies: ("groupId" IN (...Group...)) AND ("userId" IN (...User...)). Falsification: reverted to group-only text in migration file, reran scratch tool → groupmembership-schreiben-fremder-benutzer-abgelehnt FAILED as expected, then restored (file byte-identical after restore) |
| 2 | ModuleGrant policy checks both possible targets (group OR user) with null-allowance, both D-04 branches measured |
✓ VERIFIED | Live pg_policies shows tenantId = ... AND (groupId IS NULL OR ...) AND (userId IS NULL OR ...). Falsification: reverted to tenant-only text → both modulegrant-fremde-gruppe-abgelehnt and modulegrant-fremder-benutzer-abgelehnt FAILED as expected, then restored |
| 3 | assertTargetBelongsToTenant in module-grants.service.ts unchanged, held by a test that goes red if removed |
✓ VERIFIED | 3 call sites present (grep -n); two dedicated spec cases in module-grants.service.spec.ts:290-312 with an explicit comment (lines 281-289) stating why they'd go red if the app-level guard were dropped |
| 4 | TenderRssFeedSource read/write split: bound read returns own + platform rows; write (insert/update/delete) still requires a tenant; both error directions (too strict / too loose) separately measured |
✓ VERIFIED | Live pg_policies: 4 command-separated policies; only tenant_platform_read_policy (SELECT) contains IS NULL, none of insert/update/delete do. Scratch tool: tenderrssfeed-plattformzeile-gebunden-sichtbar + tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar (too-strict direction) and tenderrssfeed-gebundenes-aendern-...-abgelehnt + ...-loeschen-...-abgelehnt (too-loose direction), all passed independently |
| 5 | The three hole-claiming scratch-tool checks are INVERTED, not deleted/loosened, named for the new truth, referencing the old finding | ✓ VERIFIED | Old check names (...-nicht-verhindert, ...-trotz-eigener-mandantenkennung-erlaubt, ...-unter-jedem-mandanten-unsichtbar) appear only as backward-references inside the new checks' message text, not as separate passing assertions (grep over the whole tool file). New names assert rejection/visibility, matching the fix |
| 6 | grant-foreign-group row (dependency for two module-registry checks) still gets created, now via the maintenance role, and neither dependent check passes for the wrong reason |
✓ VERIFIED | runGroupsAreaChecks creates it via withAdminPrisma/BYPASSRLS (line ~913-917) before runModuleRegistryAreaChecks runs (both share the same scratch DB in main()); gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus message text now correctly says the row was excluded because the maintenance-role insert bypassed a rule that would otherwise reject it — not because the rule "lets it through" |
| 7 | The claim "no application code needs to change" was measured, not assumed; the one path that does (listForUser) is bound |
✓ VERIFIED | listForUser uses forTenant(this.prisma, tenantId); controller passes tenantId from session context (tenders.controller.ts:267-268). Falsification: reverted the binding, ran tender-rss-feed.service.spec.ts → exactly 1 test failed (listForUser() bindet...), 30 passed, matching SUMMARY's claim precisely; restored |
| 8 | Every record describing one of the three old rules now tells the truth, naming the new migration | ✓ VERIFIED | All 4 in-source header comments (module-access.service.ts, groups.service.ts, module-grants.service.ts, tender-rss-feed.service.ts x3) and rls-coverage.spec.ts description line updated and name 20260910120000_rls_widen_membership_grant_and_platform_read; classification doc tenders row (35/27) and sum row (107/135) independently re-derived from source via the same grep the doc cites and matched exactly; rls-access-inventory.spec.ts (10/10) passes |
| 9 | WINDOWS #19 closed with evidence naming the migration and both error-direction checks; #18/#20/#21/#23 remain open; what's NOT solved recorded as its own open entry that doesn't vanish with #19 | ✓ VERIFIED | #19 status: fixed, resolved_at set, description names the migration and 5 named checks, explicitly frames SearchProvider half as REFUTED PREMISE not solved problem. #18/#20/#21/#23 byte-identical to base commit. New entry #24 (open, deviation) names both blocked paths (createPlatform/remove) and states explicitly this predates 260910-jab. Header counts (6 open/17 fixed/1 waived/24 total) match a fresh count of the JSON-equivalent table rows |
| 10 | Switch stays OFF; DATABASE_URL still points at role tessera |
✓ VERIFIED | docker-compose.yml:33 and .env.example:2 both show role tessera (no _app suffix), unchanged from base |
| 11 | Baseline held: test count doesn't drop, type-check clean, scratch tool all-passed, at every task boundary | ✓ VERIFIED | Independently re-ran: 839/839 tests (56 files), tsc --noEmit clean, rls-scratch-check.mjs 74/74 passed — all match SUMMARY's claimed end-state exactly |
Score: 11/11 truths verified (0 present-but-behavior-unverified)
Falsification Experiments (independently performed by this verifier)
Three destructive experiments were run directly against the scratch tool / migration file, each reverted afterward and confirmed byte-identical to the original:
- Reverted
GroupMembershippolicy text to group-only →groupmembership-schreiben-fremder-benutzer-abgelehntFAILED (1/74). Restored. - Reverted
ModuleGrantpolicy text to tenant-only →modulegrant-fremde-gruppe-abgelehntandmodulegrant-fremder-benutzer-abgelehntboth FAILED (2/74). Restored. - Reverted
TenderRssFeedSourceto the old single-policy form → the tenders-area extraction guard correctly aborted withtenders-policies-aus-migration-gefunden: FEHLGESCHLAGEN(fail-closed, not a silent wrong measurement) rather than measuring the old text as if it were current. Restored. - Reverted
TenderRssFeedSourceService.listForUser'sforTenant()binding → exactly 1 test failed (listForUser() bindet — forTenant() wird mit der uebergebenen Mandantenkennung aufgerufen), 30 passed. Restored.
All four confirm the guarding assertions (tests and scratch checks) genuinely detect the regression they claim to detect — not passing by construction.
Required Artifacts
| Artifact | Expected | Status | Details |
|---|---|---|---|
apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql |
New migration, applied locally | ✓ VERIFIED | Exists, prisma migrate status reports up to date, live pg_policies text matches file text exactly (diffed by hand for GroupMembership/ModuleGrant/TenderRssFeedSource/SearchProvider) |
apps/api/scripts/rls-scratch-check.mjs |
3 inverted checks + gegenmessungen + 4 command directions + extraction redirect | ✓ VERIFIED | readRlsWidenMigrationSql()/extractAllPolicySql() wired into runGroupsAreaChecks/runTendersAreaChecks; old extraction (readGroupsRlsPoliciesMigrationSql) no longer used for GroupMembership/ModuleGrant |
apps/api/src/groups/migration-sql.spec.ts |
New describe block, text-only | ✓ VERIFIED | rls_widen_membership_grant_and_platform_read migration.sql block present, 6 tests, all pure regex/string assertions, no DB |
apps/api/src/tenders/tender-rss-feed.service.ts |
listForUser bound, 3 header comments corrected |
✓ VERIFIED | Confirmed by reading; createPlatform/remove deliberately still unbound with corrected reasoning pointing at WINDOWS #24 |
apps/api/src/tenders/tenders.controller.ts + both spec files |
tenant pass-through, two-client proof | ✓ VERIFIED | extractTriageContext(req) supplies tenantId; two-client proof in spec uses __makeBoundClient/boundCallLog, not an identity mock (unbound fake doesn't log, bound one does) |
apps/api/src/groups/groups.service.ts, module-grants.service.ts, module-access.service.ts, rls-coverage.spec.ts |
4 in-source records corrected | ✓ VERIFIED | Read all 4 — each names the new migration and describes the new rule while explicitly retaining the app-level guard as "second net" |
docs/mandantentrennung-zugriffsklassifikation.md |
#19 block answered, 4 inventory rows updated, overview/sum rows re-derived | ✓ VERIFIED | tenders row 35/27 matches independently re-run grep exactly; sum row 107/135 matches; rls-access-inventory.spec.ts passes (machine-checked binding to this doc) |
docs/mandantentrennung-etappe2-fehlerrichtung.md |
New ## Regelschluss... section, 5 subsections (r1-r5), annotations at superseded spots |
✓ VERIFIED | All 5 subsections present; r1 quotes an actual 74-check run (verified to match reality); r2 signal table covers both error directions per rule; old measurement blocks preserved verbatim with adjacent NACHTRAG (260910-jab) annotations (6+ locations found, not rewritten) |
docs/mandantentrennung-datenbankrolle.md |
The one #19-as-open spot updated | ✓ VERIFIED | Line 91-94 now says GESCHLOSSEN with migration name |
.planning/WINDOWS.md |
#19 fixed, #24 new, counters derived | ✓ VERIFIED | Header counts (6/1/17/24) match row-by-row count; #18/#20/#21/#23 byte-identical to base |
Key Link Verification
| From | To | Via | Status | Details |
|---|---|---|---|---|
rls-scratch-check.mjs extraction |
20260910120000_... migration file |
readRlsWidenMigrationSql() + extractAllPolicySql() |
✓ WIRED | Confirmed by falsification #3 above: reverting the migration file's TenderRssFeedSource text made the tool's own extraction guard fire, proving it reads the live file, not a cached/hardcoded value |
Groups-area grant-foreign-group provisioning |
module-registry-area dependent checks |
shared scratch DB across main()'s sequential area-check calls |
✓ WIRED | Confirmed both dependent checks (gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus, gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit) pass in the live run and read code confirming ordering |
TendersController.listRssFeeds |
TenderRssFeedSourceService.listForUser |
extractTriageContext(req).tenantId passthrough |
✓ WIRED | Read at tenders.controller.ts:266-268 |
TenderRssFeedSource command-separated policies |
live database | migration applied via prisma migrate deploy, not migrate dev/reset |
✓ WIRED | prisma migrate status → up to date; pg_policies text matches file text |
Behavioral Spot-Checks / Probe Execution
| Behavior | Command | Result | Status |
|---|---|---|---|
| Scratch tool passes fully | node apps/api/scripts/rls-scratch-check.mjs against live container |
Alle 74 Pruefungen bestanden. |
✓ PASS |
| Full test suite | npm --prefix apps/api run test |
839/839, 56 files | ✓ PASS |
| Type check | npm --prefix apps/api run type-check |
clean, no output | ✓ PASS |
rls-access-inventory.spec.ts (machine clamp doc↔code) |
npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts |
10/10 | ✓ PASS |
| Falsification: GroupMembership reverted | scratch tool against reverted migration text | 1/74 failed (correct check) | ✓ PASS (as regression detector) |
| Falsification: ModuleGrant reverted | scratch tool against reverted migration text | 2/74 failed (correct checks) | ✓ PASS (as regression detector) |
| Falsification: TenderRssFeedSource reverted | scratch tool against reverted migration text | extraction guard fired, 1/62 failed | ✓ PASS (fail-closed, not silently wrong) |
| Falsification: listForUser binding reverted | npm --prefix apps/api run test -- src/tenders/tender-rss-feed.service.spec.ts |
1/31 failed (correct test) | ✓ PASS (as regression detector) |
Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|---|---|---|---|---|
| WINDOWS-19 | 260910-jab-PLAN.md | Platform-wide rows visible to every tenant, not just their own, after cutover | ✓ SATISFIED | 4-policy split live, both error directions measured and independently falsified |
| T-JTS-02 | 260910-jab-PLAN.md | GroupMembership policy checks user side too |
✓ SATISFIED | Live policy text confirmed, falsified |
| T-JTS-03 | 260910-jab-PLAN.md | ModuleGrant policy checks referenced group/user, not just own tenant |
✓ SATISFIED | Live policy text confirmed, falsified, both D-04 branches measured |
Anti-Patterns Found
None. Searched all 16 files in files_modified for TBD|FIXME|XXX|TODO|HACK|PLACEHOLDER — zero hits. No stray stub/empty-return patterns found in the reviewed code.
Scope Discipline
apps/api/prisma/schema.prisma— untouched (git diff --statempty against base)docker-compose*.yml,.env.example,.env.prod.example— untouchedDATABASE_URLstill roletessera(no_appsuffix) — switch remains OFFassertTargetBelongsToTenantpresent with 3 call/definition sites, unchanged behavior- WINDOWS #21, #23 — byte-identical to base commit (not touched by this task)
- Full diff scope (16 files changed) matches the plan's declared
files_modifiedlist exactly, plusmodule-grants.service.spec.ts(listed in SUMMARY key-files, consistent with Aufgabe 2's test-first requirement)
Human Verification Required
None. All must-haves are verifiable via the live database, the scratch tool, and static analysis, and were independently re-derived rather than accepted from the SUMMARY.
Gaps Summary
No gaps found. This is an unusually well-evidenced quick task: every claim in the SUMMARY that could be independently re-measured was re-measured (not re-read), including four destructive falsification experiments this verifier ran fresh (beyond the two the executor already ran), and every number matched exactly (74/74, 839/839, 35/27, 107/135, 6/17/1/24). The framing of the SearchProvider half of WINDOWS #19 as a refuted premise (rather than a solved problem) is accurate and consistent across the migration header, the ledger entry, and the classification document.
Verified: 2026-09-10T14:55:00Z Verifier: Claude (gsd-verifier)