From 261e73603e24167ae844134a8af7817b36bb5f0f Mon Sep 17 00:00:00 2001 From: Schalli Date: Fri, 11 Sep 2026 16:30:49 +0200 Subject: [PATCH] docs(quick-260911-mkj): Plan fuer WINDOWS #27, Relations-Blindstelle Vierte Erkennungsform fuer rls-access-inventory.spec.ts (include/select/_count/ Relationsfilter ueber schema.prisma auf das Zielmodell aufgeloest), drei Waechter gegen stilles Unterberichten, gepinnte Proben fuer die #27-Form, zur Planungszeit gemessen: 7 neue Paare, 3 Stand-Aenderungen, 1 Ausnahmedatei. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR --- .../260911-mkj-PLAN.md | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 .planning/quick/260911-mkj-windows-27-schliessen-relations-blindste/260911-mkj-PLAN.md diff --git a/.planning/quick/260911-mkj-windows-27-schliessen-relations-blindste/260911-mkj-PLAN.md b/.planning/quick/260911-mkj-windows-27-schliessen-relations-blindste/260911-mkj-PLAN.md new file mode 100644 index 0000000..ebb55c8 --- /dev/null +++ b/.planning/quick/260911-mkj-windows-27-schliessen-relations-blindste/260911-mkj-PLAN.md @@ -0,0 +1,228 @@ +--- +phase: quick-260911-mkj +plan: 01 +type: execute +wave: 1 +depends_on: [] +autonomous: true +requirements: [WINDOWS-27, ETAPPE-4-VORAUSSETZUNG] + +files_modified: + - apps/api/src/prisma/rls-access-inventory.spec.ts + - docs/mandantentrennung-zugriffsklassifikation.md + - docs/mandantentrennung-etappe2-fehlerrichtung.md + - .planning/WINDOWS.md + +estimate: + tokens: 120000 + raw_tokens: 120000 + tasks: 2 + confidence: low + +must_haves: + truths: + - "Die Bestandsaufnahme SIEHT Relationszugriffe: `analyzeSource()` in `rls-access-inventory.spec.ts` traegt eine vierte Erkennungsform, die innerhalb des Argumentbereichs jedes erkannten Modellaufrufs (`..(`) jeden Objektschluessel gegen die Relationsfelder des Kontextmodells aus `apps/api/prisma/schema.prisma` aufloest und das ZIELMODELL als Fundstelle derselben Datei fuehrt — gebunden, wenn der Empfaenger gebunden ist (`const X = forTenant(`-Name, `withTenantTransaction`-Parameter, Parameter einer Transaktion auf gebundenem Empfaenger), sonst ungebunden. Das erfasst `include:`, `select:`, `_count: { select: ... }`, `_count: true`, Relationsfilter in `where:`, `orderBy:` ueber Relationen und verschachtelte Schreibzugriffe in `data:` — alles, was Prisma als Unterabfrage auf die zweite Tabelle unter DEREN Regel rendert." + - "Der Detektor faellt LAUT, nie still: (a) jede `include:`/`select:`/`_count:`-Angabe im Quelltext, die NICHT innerhalb eines erkannten Modellaufrufs liegt, ist ein Testfehler, es sei denn die Datei steht mit Grund in `RELATION_SPEC_EXCEPTIONS` (zur Planungszeit gemessen: genau `apps/api/src/tenders/backfill-tender-source.ts`, eigener `new PrismaClient()`); (b) jeder `include:`/`select:`-Wert, der weder Objektliteral noch `true` noch eine in DERSELBEN Datei definierte Objektliteral-Konstante ist, ist ein Testfehler (zur Planungszeit gemessen: vier solche Konstanten, alle in derselben Datei definiert, null Verstoesse); (c) die Ausnahmeliste veraltet nicht unbemerkt (derselbe Waechter wie fuer die beiden bestehenden Listen)." + - "Die Relations-zu-Modell-Zuordnung ist GEMESSEN, nicht angenommen: `parseSchemaRelations()` liest `schema.prisma` zur Testzeit; ein Test pinnt `Tenant.users -> User` und `Group.memberships -> GroupMembership`, prueft, dass jedes Relationsziel ein Modellname ist und dass alle `model`-Bloecke des Schemas erfasst sind." + - "Die Blindstelle aus WINDOWS #27 ist gegen eine KONTROLLIERTE Probe bewiesen und im Spec gepinnt: `include: { _count: { select: { users: true } } }` auf `this.prisma.tenant` liefert `user` als UNGEBUNDENE Fundstelle; dieselbe Form auf einem `const tenantPrisma = forTenant(`-Klienten liefert `user` als GEBUNDENE Fundstelle. Dazu gepinnt: die reale `ldap`-Form (`include: { tenant: true, fieldMappings: true }`), die verschachtelte `where`-Kette (`group: { memberships: { some } }`), ein skalarer `select` (KEINE Relation), `_count: true` (ALLE Relationen des Kontextmodells), ein unbekannter Empfaenger (faellt in Waechter (a)), eine Konstante als `select`-Wert (aufgeloest) und eine nicht aufloesbare Kennung (faellt in Waechter (b))." + - "Jedes neu sichtbare (Datei, Modell)-Paar hat eine Zeile mit Klasse, Stand und Begruendung in der Bestandsaufnahme; jeder geaenderte Stand ist fortgeschrieben, nicht geloescht; die Klasse eines Paares ist konsistent mit den bestehenden Zeilen desselben Modells (plattformglobale Tabellen `Tenant`/`Module`/`Tender`/`TenderSource` bleiben `keine-mandantengebundene-tabelle`, auch wenn die Einbindung ueber einen gebundenen Klienten laeuft). Zur Planungszeit gemessen (Prototyp, wortgleiche Erkennungslogik): SIEBEN neue Paare, DREI Stand-Aenderungen, davon EINE mit Klassenwechsel auf `beides` (`ldap-config.service.ts`/`ldapFieldMapping`: der bewusst uebergreifende Planer-Lesepfad `getAllActiveConfigs()` bindet `fieldMappings` ein). Die Ausfuehrung MISST erneut; die Spec-Ausgabe ist autoritativ, die Planungszahl ist die Untergrenze." + - "Die fuenf handgepflegten Abschnitte des Klassifikationsdokuments sind ABGELEITET, nicht abgeschrieben: Uebersichtstabelle und Summenzeile bleiben bei ihren Rohtreffer-Greps (Relationszugriffe zaehlen dort ausdruecklich NICHT, ein datierter Absatz sagt das), Klassen-Verteilung und ihre Ueberschrift nennen die aus der Tabelle GEZAEHLTE Paarzahl und je Klasse die gezaehlte Anzahl, der Hintergrunddienst-Abschnitt traegt einen Nachtrag zum ersten Fall (`getAllActiveConfigs()` reicht ueber `include` in `LdapFieldMapping` hinein — gleiches Schicksal wie die Elternzeile, kein neuer Fall, Ueberschrift bleibt `sechs Faelle`)." + - "Kein Dienstcode angefasst: die Erlaubnisliste gegen `cc26197` ist die Spec-Datei, die zwei Dokumente und `.planning/`; Schema und Migrationen unveraendert; keine Compose- oder Umgebungsdatei; Schalter AUS. Wenn der Detektor eine echte ungebundene Einbindung in eine GESCHUETZTE Tabelle auf ungeschuetztem Elternpfad findet, die NICHT bereits als Hintergrunddienst-Fall benannt ist: ANHALTEN und im SUMMARY als Befund melden, nicht beheben." + - "WINDOWS #27 steht auf `fixed` mit dem Nachweis (Spec-Tests, Zahlen) im SUMMARY; die beim Messen der Empfaengerformen gefundene, von ALLEN vier Formen unerreichbare Restmenge (`tenders.seed.ts`: Funktionsparameter `prisma: PrismaService`; `backfill-tender-source.ts`: eigener `new PrismaClient()`) ist als EIGENER Ledger-Eintrag festgehalten, nicht stillschweigend in #27 mitgeschlossen." + - "Baseline am Ende JEDER Aufgabe: mehr als 994 Tests gruen in mindestens 62 Dateien, Typpruefung sauber, Werkzeug `rls-scratch-check.mjs` mindestens 137 Pruefungen bestanden (`ENOTFOUND mailhog` ist lokal umgebungsbedingt)." + artifacts: + - "apps/api/src/prisma/rls-access-inventory.spec.ts — `analyzeSource(source, relPath)` (aus `analyzeFile` herausgeloest, damit Probestrings testbar sind), `parseSchemaRelations()`, `SCHEMA_RELATIONS`, vierte Erkennungsform, `RELATION_SPEC_EXCEPTIONS`, drei Waechter-Tests, zwei Schema-Tests, ein `describe`-Block `vierte Erkennung: Relationszugriffe (WINDOWS #27)` mit mindestens acht gepinnten Probestrings" + - "docs/mandantentrennung-zugriffsklassifikation.md — sieben neue Bestandsaufnahme-Zeilen, drei fortgeschriebene Zeilen, umgeschriebener Erkennungsluecken-Absatz, datierter Absatz in der Uebersicht, `Stand 260911-mkj`-Absatz und neue Tabelle in der Klassen-Verteilung, Nachtrag im Hintergrunddienst-Abschnitt" + - "docs/mandantentrennung-etappe2-fehlerrichtung.md — `Nachtrag (260911-mkj)` unter `### (n4)` im `## Bereich tenant` und ein Vermerk im `## Etappe 2 — Abschluss`, dass #27 geschlossen ist" + - ".planning/WINDOWS.md — #27 `fixed`, ein neuer Eintrag `quick-260911-mkj` fuer die Empfaenger ausserhalb der vier Formen" + key_links: + - "`analyzeSource()` -> `SCHEMA_RELATIONS` (Relationsfeld -> Zielmodell) -> `boundModels`/`unboundModels` derselben Datei -> `computeStandByKey()`/`findAccessSites()` unveraendert -> Vergleich gegen die Bestandsaufnahme-Tabelle (die bestehenden Tests `jede Fundstelle ist eingetragen` / `Stand stimmt` werten die neuen Paare automatisch mit)" + - "Rohzahl `include|select|_count` ueber den kommentarfreien Quelltext MINUS die innerhalb erkannter Aufrufe gezaehlten Angaben -> Waechter (a) -> `RELATION_SPEC_EXCEPTIONS` — die Grenze der Erkennung bleibt sichtbar, nicht still" + - "Prisma-Klientenname = Modellname mit kleinem Anfangsbuchstaben (`User` -> `user`, `LdapFieldMapping` -> `ldapFieldMapping`) — derselbe Schluessel wie in der Spalte `Modell` der Bestandsaufnahme; die vierte Form schreibt in DIESER Form, sonst entstehen Phantom-Paare" +--- + + +WINDOWS #27 schliessen: die maschinelle Bestandsaufnahme (`rls-access-inventory.spec.ts`) erkennt bisher nur direkte Modellzugriffe (drei Formen). Ein Zugriff, der ueber `include:`/`select:`/`_count:` in eine ZWEITE Tabelle hineinreicht, ist fuer sie unsichtbar, obwohl Prisma daraus eine Unterabfrage auf die zweite Tabelle unter DEREN Regel macht (nachgewiesen in 260911-e2s: drei `_count`-Zaehler in `User` haetten nach dem Scharfschalten fuer jeden Mandanten 0 Benutzer gezeigt). Dieser Plan fuegt eine VIERTE Erkennungsform hinzu, die Relationsfelder ueber `schema.prisma` auf ihr Zielmodell aufloest und das Zielmodell als eigene Fundstelle fuehrt; er beweist die Form gegen kontrollierte Proben (die `tenant`-Form ist im Dienstcode inzwischen aufgeloest, deshalb Probestrings im Spec), traegt die dadurch sichtbaren Paare ins Klassifikationsdokument ein, leitet die handgepflegten Abschnitte neu ab und schliesst den Ledger-Eintrag mit Nachweis. + +Purpose: Etappe 4 (Scharfschalten) stuetzt sich auf diese Bestandsaufnahme. Ein Werkzeug, das eine ganze Zugriffsform nicht sieht, macht die Vorabpruefung zur Behauptung. Der User geht am Dienstag 2026-09-15 live; der Schalter bleibt aus, aber dieser Plan ist die Voraussetzung, ihn spaeter guten Gewissens zu werfen. + +Output: erweiterter Spec (vierte Form + Waechter + gepinnte Proben), fortgeschriebenes Klassifikationsdokument (72 Paare erwartet, gemessen), Nachtrag in der Kritikschrift, WINDOWS #27 `fixed` plus ein neuer Eintrag fuer die unerreichbare Restmenge. + + + +@~/.claude/gsd-core/workflows/execute-plan.md +@~/.claude/gsd-core/templates/summary.md + + + +@.planning/STATE.md +@.planning/WINDOWS.md +@apps/api/src/prisma/rls-access-inventory.spec.ts +@apps/api/prisma/schema.prisma +@docs/mandantentrennung-zugriffsklassifikation.md +@docs/mandantentrennung-etappe2-fehlerrichtung.md +@apps/api/src/ldap/ldap-config.service.ts +@apps/api/src/module-registry/module-access.service.ts +@apps/api/src/tenders/tender-digest.scheduler.ts +@apps/api/src/tenders/tenders.controller.ts + + +Zur Planungszeit gemessen mit einem Prototyp der unten beschriebenen Erkennungslogik (Scratch, nicht eingecheckt — der Nachweis wandert mit Aufgabe 1 in den Spec). Diese Zahlen sind die UNTERGRENZE; die Ausfuehrung misst erneut und die Spec-Ausgabe ist autoritativ. + +Empfaenger aller Modellaufrufe `..(` in `apps/api/src` (ohne Specs): `tenantPrisma` 178, `this.prisma` 62, `tx` 11, `prisma` 8 (davon 4 in `tenders/backfill-tender-source.ts` mit eigenem `new PrismaClient()` und 4 in `tenders/tenders.seed.ts` ueber den Funktionsparameter `prisma: PrismaService`), `this.` 13 (Dienstmethoden, KEINE Prisma-Aufrufe — `this.userService.create(` u. ae.; der Anker darf deshalb nur bekannte Empfaenger nehmen). + +Operationen im Quelltext: findMany 60, findUnique 57, update 37, create 25, findFirst 21, upsert 18, count 12, delete 11, deleteMany 6, createMany 6, updateMany 5, groupBy 1. + +Rohangaben: 19 `include:`-Zeilen, 53 `select:`-Zeilen, 1 `_count: true` (in `tenders.controller.ts:405` — TOP-LEVEL in `groupBy`, also Aggregat, KEIN Relationszaehler), 8 `select: ` ueber vier Konstanten (`EMAIL_CONFIG_SAFE_SELECT`, `CONFIG_SAFE_SELECT`, `SMTP_SAFE_SELECT`, `SOURCE_SAFE_SELECT`), alle in derselben Datei als Objektliteral definiert; null Kurzschreibweisen (`include: { x }`) — `grep -rnE "include:\s*\{[^:}]*\}" apps/api/src --include=*.ts` leer. + +Relationsfundstellen (Datei, Empfaenger.Modell.Operation, Schluesselpfad -> Ziel): +- groups/groups.service.ts tenantPrisma.group.findMany select>memberships -> GroupMembership [gebunden] +- groups/groups.service.ts tenantPrisma.groupMembership.findMany include>user -> User [gebunden] +- groups/module-grants.service.ts tenantPrisma.tenantModuleActivation.findMany include>module -> Module [gebunden] (2x) +- groups/module-grants.service.ts tenantPrisma.moduleGrant.findMany where>group -> Group, group>memberships -> GroupMembership, include>group -> Group [gebunden] +- groups/module-grants.service.ts tenantPrisma.groupMembership.findMany where>group -> Group, include>group -> Group [gebunden] +- ldap/ldap-config.service.ts tenantPrisma.ldapConfig.{findUnique,create,update} include>fieldMappings, data>fieldMappings -> LdapFieldMapping [gebunden] +- ldap/ldap-config.service.ts this.prisma.ldapConfig.findMany (`getAllActiveConfigs`, Zeile 308) include>tenant -> Tenant, include>fieldMappings -> LdapFieldMapping [UNGEBUNDEN] +- module-registry/module-access.service.ts tenantPrisma.moduleGrant.findMany where>group -> Group, group>memberships -> GroupMembership [gebunden] +- module-registry/module-registry.service.ts tenantPrisma.tenantModuleActivation.{findMany,upsert,update} include>module -> Module [gebunden] +- tenders/tender-digest.scheduler.ts tenantPrisma.tenderMatch.findMany include>tender -> Tender, include>savedSearch -> TenderSavedSearch, orderBy>savedSearch -> TenderSavedSearch [gebunden] +- tenders/tender-matching.service.ts tenantPrisma.tenderMatch.findMany include>tender -> Tender [gebunden] +- tenders/tenders.controller.ts this.prisma.tender.findUnique (`getTender`) include>sources -> TenderSource [UNGEBUNDEN] + +Ergebnis gegen die Bestandsaufnahme (65 Paare): +NEUE PAARE (7): module-grants.service.ts/module gebunden; ldap-config.service.ts/tenant ungebunden; module-access.service.ts/group gebunden; module-access.service.ts/groupMembership gebunden; tender-digest.scheduler.ts/tender gebunden; tender-digest.scheduler.ts/tenderSavedSearch gebunden; tenders.controller.ts/tenderSource ungebunden. +STAND-AENDERUNGEN (3): ldap-config.service.ts/ldapFieldMapping gebunden -> gemischt (UND Klasse muss-mandantengebunden -> beides, siehe Aufgabe 1); module-registry.service.ts/module ungebunden -> gemischt; tender-matching.service.ts/tender ungebunden -> gemischt. +WAECHTER (a) Verstoss (1): tenders/backfill-tender-source.ts (raw 1, matched 0) — kommt in `RELATION_SPEC_EXCEPTIONS`. +WAECHTER (b) Verstoesse: 0. +Erwartete Klassen-Verteilung danach: muss-mandantengebunden 35 (33 + 3 neue − 1 Klassenwechsel), keine-mandantengebundene-tabelle 21 (17 + 4), beides 14 (13 + 1), bewusst-uebergreifend 2, Summe 72. + +Probestrings gegen den Prototyp: A (`this.prisma.tenant.findMany({ include: { _count: { select: { users: true } } } })`) -> unbound {tenant}, relation unbound {user}; B (dieselbe Form auf `const tenantPrisma = forTenant(this.prisma, tenantId)`) -> bound {tenant, user}, unbound leer; C (skalarer select + Zeichenkette mit `{(` + `_count: true` auf tenant + unbekannter Empfaenger `client.tenant.findMany({ include: { users: true } })`) -> unbound {group, tenant, user, ldapConfig, group, moduleGrant}, raw 4 / matched 3. + + + + + + + + Task 1: Vierte Erkennungsform, Waechter und gepinnte Proben im Spec; die dadurch sichtbaren Bestandsaufnahme-Zeilen + apps/api/src/prisma/rls-access-inventory.spec.ts, docs/mandantentrennung-zugriffsklassifikation.md + + - Test 1 (Schema): `SCHEMA_RELATIONS.get('Tenant')?.get('users')` ist `'User'`; `SCHEMA_RELATIONS.get('Group')?.get('memberships')` ist `'GroupMembership'`; `SCHEMA_RELATIONS.get('LdapConfig')?.get('fieldMappings')` ist `'LdapFieldMapping'`. + - Test 2 (Schema): jedes Relationsziel in `SCHEMA_RELATIONS` ist ein Modellname des Schemas; `SCHEMA_RELATIONS.size` ist gleich der Zahl der `^model ` Zeilen in `schema.prisma` (zur Planungszeit 28). + - Test 3 (Probe A, WINDOWS #27 ungebunden): `analyzeSource()` auf einem Probestring mit `this.prisma.tenant.findMany({ include: { _count: { select: { users: true } } } })` liefert `unboundModels` ⊇ {`tenant`, `user`} und `boundModels` ohne `user`. + - Test 4 (Probe B, WINDOWS #27 gebunden): derselbe Aufruf auf `const tenantPrisma = forTenant(this.prisma, tenantId) as any;` liefert `boundModels` ⊇ {`tenant`, `user`} und `unboundModels` ohne `user` (und ohne `tenant` — `forTenant(this.prisma,` ist kein `this.prisma.`). + - Test 5 (reale ldap-Form): `this.prisma.ldapConfig.findMany({ where: { isActive: true }, include: { tenant: true, fieldMappings: true } })` liefert `unboundModels` ⊇ {`ldapConfig`, `tenant`, `ldapFieldMapping`}. + - Test 6 (verschachtelte where-Kette): `tenantPrisma.moduleGrant.findMany({ where: { tenantId, group: { memberships: { some: { userId } } } } })` auf einem `forTenant(`-Klienten liefert `boundModels` ⊇ {`moduleGrant`, `group`, `groupMembership`}. + - Test 7 (Negativprobe skalarer select + Zeichenkette mit Klammern): `this.prisma.group.findMany({ select: { id: true, name: true }, where: { name: { contains: '{(' } } })` liefert `unboundModels` GENAU {`group`} — kein Relationsmodell, und die Klammern in der Zeichenkette zerreissen den Argumentbereich nicht. + - Test 8 (`_count: true`): `this.prisma.tenant.findMany({ include: { _count: true } })` liefert `unboundModels` ⊇ {`user`, `ldapConfig`, `group`, `moduleGrant`} (ALLE Relationen von `Tenant`). + - Test 9 (unbekannter Empfaenger faellt in Waechter (a)): `client.tenant.findMany({ include: { users: true } })` mit `client` als Funktionsparameter liefert `rawRelationSpecCount` 1 und `matchedRelationSpecCount` 0 und KEIN `user` in beiden Mengen. + - Test 10 (Konstante als select-Wert): Probestring mit `const SAFE = { id: true, users: true };` und `this.prisma.tenant.findMany({ select: SAFE })` liefert `unboundModels` ⊇ {`user`}; ein Probestring mit `select: IMPORTED_SELECT` OHNE Definition in der Probe liefert `unresolvedRelationSpecValues` mit genau einem Eintrag, der `IMPORTED_SELECT` nennt. + - Test 11 (Waechter ueber alle Dateien): fuer jede Datei gilt `rawRelationSpecCount - matchedRelationSpecCount <= 0` oder die Datei steht in `RELATION_SPEC_EXCEPTIONS`; `unresolvedRelationSpecValues` ist ueberall leer; jede Datei in `RELATION_SPEC_EXCEPTIONS` existiert und hat tatsaechlich einen Ueberschuss (Stale-Waechter, wortgleich zu den beiden bestehenden Listen). + - Bestehende Tests: `jede im Quelltext gefundene (Datei, Modell)-Fundstelle ist im Dokument eingetragen` und `der eingetragene Stand stimmt` werden nach Aufgabe 1 wieder GRUEN, weil die Tabelle fortgeschrieben ist — der Zwischenzustand (Spec rot mit exakt den gemessenen 7 fehlenden Paaren und 3 Stand-Abweichungen) ist der Beweis, dass der Detektor sieht, und gehoert woertlich ins SUMMARY. + + +Arbeite auf dem Hauptcheckout (Worktree-Isolation ist aus), Basis `cc26197`. Lies die Spec-Datei EINMAL vollstaendig; die drei bestehenden Formen bleiben unveraendert, die vierte kommt DAHINTER in denselben Stil (Kopfkommentar im Ton der drei vorhandenen Absaetze, `ue`-Schreibweise, Bezug „Erweitert in 260911-mkj (WINDOWS #27)"). + +**1. Refaktor fuer Testbarkeit.** Ziehe den Rumpf von `analyzeFile(absPath, relPath)` in `function analyzeSource(source: string, relPath: string): FileAnalysis` heraus (das Argument ist der ROHE Quelltext; `stripComments` laeuft innerhalb von `analyzeSource`); `analyzeFile` liest die Datei und ruft `analyzeSource`. Erweitere `FileAnalysis` um `rawRelationSpecCount: number`, `matchedRelationSpecCount: number`, `unresolvedRelationSpecValues: string[]`. Die vierte Form schreibt ihre Treffer DIREKT in `unboundModels`/`boundModels` (Klientenname = Modellname mit kleinem Anfangsbuchstaben, dieselbe Form wie Spalte `Modell` der Tabelle), damit `findAccessSites`/`computeStandByKey` und die bestehenden Vergleichstests unveraendert bleiben. + +**2. Schema lesen (Testzeit, dieselbe Idiomatik wie das Lesen der Migrationen in `rls-coverage.spec.ts`).** `const SCHEMA_PATH = join(__dirname, '../../prisma/schema.prisma')`. `parseSchemaRelations(schemaSource)`: Modellbloecke per `/^model\s+(\w+)\s*\{([\s\S]*?)^\}/gm`; je Zeile im Block `/^\s*(\w+)\s+(\w+)(\[\]|\?)?(?=\s|$)/` — ACHTUNG, das Lookahead `(?=\s|$)` ist noetig, weil `users User[]` am ZEILENENDE steht (ohne Lookahead fehlten zur Planungszeit alle Listenrelationen, `Tenant` hatte scheinbar keine Relation — das war der erste Prototyp-Fehler, nicht wiederholen); Feld ist Relation, wenn der Typ ein Modellname ist. Ergebnis `Map>`, einmal beim Laden als `SCHEMA_RELATIONS`. Dazu `CLIENT_NAME_TO_MODEL` (kleiner Anfangsbuchstabe -> Modellname) fuer den Anker. + +**3. Vierte Form in `analyzeSource`, in dieser Reihenfolge:** +(a) `blank` = der kommentarfreie Quelltext, in dem der INHALT von Zeichenkettenliteralen (einfach, doppelt, Backtick) durch nichts ersetzt ist (Anfuehrungszeichen bleiben) — NUR fuer die vierte Form; die Formen 1–3 arbeiten weiter auf `source`, damit eine Interpolation wie `${tx.user.count()}` in einer Vorlage fuer sie sichtbar bleibt. +(b) Empfaengermenge: `this\.prisma`, jeder Name aus `boundNames`, jeder Transaktionsparameter aus Form 3 (mit seiner dort ermittelten Bindung) und jeder `withTenantTransaction`-Parameter (immer gebunden). Sammle dafuer in Form 3 die Parameternamen mit Bindung in zwei Mengen (`txBoundParams`, `txUnboundParams`) — bislang werden sie nur lokal verbraucht. Anker-Regex auf `blank`: `\b()\.([a-zA-Z]+)\.(findMany|findFirst|findUnique|findFirstOrThrow|findUniqueOrThrow|create|createMany|createManyAndReturn|update|updateMany|updateManyAndReturn|upsert|delete|deleteMany|count|aggregate|groupBy)\(`. Empfaenger nur aus der bekannten Menge, NIE `[\w.]+` — sonst ankern `this.userService.create(` und die dreizehn anderen Dienstmethoden (gemessen). +(c) Argumentbereich: ab der oeffnenden Klammer des Ankers per Klammertiefe bis zur passenden schliessenden Klammer auf `blank` (Zeichenketteninhalte sind schon weg, deshalb reisst `contains: '{('` nichts auf). +(d) `matchedRelationSpecCount` += Anzahl `\b(include|select|_count)\s*:` im Bereich. `rawRelationSpecCount` = dieselbe Zaehlung ueber den GANZEN kommentarfreien `source` (nicht `blank` — eine Angabe in einer Vorlagen-Interpolation soll raw zaehlen und damit laut werden, nicht still verschwinden). +(e) Wertform: ersetze im Bereich jedes `\b(include|select)\s*:\s*([A-Za-z_]\w*)\b(?!\s*[.(])`, dessen Kennung nicht `true`/`false` ist, durch das Objektliteral der gleichnamigen Konstante aus DERSELBEN Datei (`\bconst\s+\b[^=]*=\s*\{` auf `blank`, Klammertiefe bis zur passenden `}`); gibt es keine, haenge `: : ` an `unresolvedRelationSpecValues`. +(f) Lauf ueber den Bereich mit Kontextstapel (Start: Modell des Ankers). Schluessel per `\b(\w+)\s*:`. Ist der Schluessel ein Relationsfeld des Kontextmodells: Zielmodell als Fundstelle eintragen (gebunden/ungebunden nach Empfaenger) und als naechsten Kontext vormerken. Ist der Schluessel `_count`, der Elternschluessel `include` oder `select` und der Wert `true`: ALLE Relationen des Kontextmodells eintragen. Sonst Kontext unveraendert vormerken (so laufen `include`, `select`, `where`, `orderBy`, `data`, `some`, `every`, `none`, `is`, `isNot`, `connect`, `create`, Operatoren wie `contains` und Skalare durch, ohne etwas zu erzeugen). `{` schiebt den vorgemerkten (sonst den aktuellen) Kontext, `}` nimmt ihn zurueck, `,` loescht die Vormerkung. Kein Tiefenlimit: die Kette `group > memberships` (module-access) ist zwei Ebenen tief und muss beide Ziele liefern. + +**4. Waechter.** `const RELATION_SPEC_EXCEPTIONS = new Set(['apps/api/src/tenders/backfill-tender-source.ts'])` mit Kopfkommentar im Stil der beiden bestehenden Listen: eigenstaendiges Skript mit eigenem `new PrismaClient()`, ein `select:` auf der plattformglobalen Tabelle `Tender` (Migration 20260909140000, Gruppe b, kein Zeilenschutz); der Empfaenger `prisma` ist fuer KEINE der vier Formen erreichbar; zusammen mit `tenders.seed.ts` (Funktionsparameter `prisma: PrismaService`, keine Relationsangabe, deshalb hier nicht gelistet) als eigener Ledger-Eintrag gefuehrt (Nummer traegt Aufgabe 2 nach — im Kommentar vorerst `WINDOWS #TBD-MKJ`, Aufgabe 2 ersetzt es). Drei neue `it(`: Ueberschuss raw−matched (Text: `jede include:/select:/_count:-Angabe liegt innerhalb eines erkannten Modellaufrufs oder die Datei steht in der begruendeten Ausnahmeliste (260911-mkj, WINDOWS #27)`), Stale-Waechter fuer `RELATION_SPEC_EXCEPTIONS` (wortgleiche Form wie der bestehende fuer `FORTENANT_ASSIGNMENT_EXCEPTIONS`), und `unresolvedRelationSpecValues` ueberall leer (Text nennt die Bedingung: Objektliteral, `true` oder Konstante derselben Datei). + +**5. Gepinnte Proben.** Neuer `describe('vierte Erkennung: Relationszugriffe (WINDOWS #27, 260911-mkj)')` unter dem bestehenden `describe`, mit den Tests 1–10 aus `` — jeder Probestring als Klassenrumpf mit `constructor(private readonly prisma: any)` und einer Methode, `analyzeSource(probe, 'apps/api/src/probe/probe.service.ts')`. Die Proben A und B sind der Nachweis fuer #27: die drei realen Stellen in `tenant.controller.ts` fanen seit 260911-e2s aus und tragen die Form nicht mehr, deshalb muss der Nachweis kontrolliert sein. + +**6. Zwischenmessung, woertlich ins SUMMARY.** Fuehre `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` aus, BEVOR du das Dokument anfasst. Erwartet rot mit genau: 7 fehlende Eintraege (`groups/module-grants.service.ts::module`, `ldap/ldap-config.service.ts::tenant`, `module-registry/module-access.service.ts::group`, `module-registry/module-access.service.ts::groupMembership`, `tenders/tender-digest.scheduler.ts::tender`, `tenders/tender-digest.scheduler.ts::tenderSavedSearch`, `tenders/tenders.controller.ts::tenderSource`) und 3 Stand-Abweichungen (`ldap-config.service.ts::ldapFieldMapping` gebunden→gemischt, `module-registry.service.ts::module` ungebunden→gemischt, `tender-matching.service.ts::tender` ungebunden→gemischt). MEHR Fundstellen als erwartet: pruefe jede einzeln am Quelltext — Ueberberichten ist zulaessig und wird eingetragen; ein Zusatzfund auf UNGESCHUETZTEM Elternpfad in eine GESCHUETZTE Tabelle (Klasse `muss-mandantengebunden`, Stand ungebunden), der nicht als Hintergrunddienst-Fall im Dokument benannt ist: ANHALTEN, SUMMARY-Befund, nichts beheben. WENIGER als 7/3: der Detektor unterberichtet — nicht ins Dokument, sondern die Erkennung berichtigen (die Planungszahl ist die Untergrenze). + +**7. Bestandsaufnahme fortschreiben** (Tabelle `| Datei | Modell | Klasse | Stand | Begründung |`, alphabetisch nach Datei/Modell einsortieren, Begruendung in ganzen Saetzen im Stil der Nachbarzeilen, jede nennt die Relationsangabe und die Methode/Zeile): +- NEU `groups/module-grants.service.ts | module | keine-mandantengebundene-tabelle | gebunden` — nur ueber `include: { module: true }` auf `tenantPrisma.tenantModuleActivation.findMany` erreicht (Zeilen 196, 253); `Module` plattformweit ohne Zeilenschutz, die Bindung des Elternaufrufs ist fuer die Unterabfrage wirkungslos, nicht schaedlich (gleiche Einordnung wie `module-registry.service.ts`/`module`). +- NEU `ldap/ldap-config.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden` — `getAllActiveConfigs()` (Zeile 308) `include: { tenant: true }` auf `this.prisma.ldapConfig.findMany`; `Tenant` traegt keinen Zeilenschutz (Aufgabe 1 in 260911-e2s, `tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar`). +- NEU `module-registry/module-access.service.ts | group | muss-mandantengebunden | gebunden` und `| groupMembership | muss-mandantengebunden | gebunden` — Relationsfilter `where: { tenantId, group: { memberships: { some: { userId } } } }` auf `tenantPrisma.moduleGrant.findMany` (Zeile 73): Prisma rendert das als Unterabfragen auf `Group` und `GroupMembership` unter DEREN Regeln; der Klient ist gebunden, beide Tabellen gehoeren demselben Mandanten. +- NEU `tenders/tender-digest.scheduler.ts | tender | keine-mandantengebundene-tabelle | gebunden` und `| tenderSavedSearch | muss-mandantengebunden | gebunden` — `include: { tender: true, savedSearch: true }` plus `orderBy` ueber `savedSearch` auf `tenantPrisma.tenderMatch.findMany` (Zeile 149) innerhalb der Mandantenschleife des Planers; `Tender` ist der plattformglobale Katalog, `TenderSavedSearch` mandantengebunden und ueber denselben gebundenen Klienten erreicht. +- NEU `tenders/tenders.controller.ts | tenderSource | keine-mandantengebundene-tabelle | ungebunden` — `getTender` (Zeile 612) `include: { sources: { select: ... } }` auf `this.prisma.tender.findUnique`; `TenderSource` plattformweit ohne Zeilenschutz (gleiche Einordnung wie `tender-dedup.service.ts`/`tenderSource`). +- GEAENDERT `ldap/ldap-config.service.ts | ldapFieldMapping`: Klasse `muss-mandantengebunden` -> `beides`, Stand `gebunden` -> `gemischt`. Begruendung ERGAENZEN, nicht ersetzen: die vierte Erkennung (260911-mkj) macht sichtbar, dass der bewusst uebergreifende Planer-Lesepfad `getAllActiveConfigs()` ueber `include: { fieldMappings: true }` in `LdapFieldMapping` hineinreicht — dieselbe Unterabfrage-Form wie die drei `tenant`-Zaehler aus WINDOWS #27, hier aber KEIN neuer Befund: der Elternpfad ist als erster Fall der Hintergrunddienst-Falle bereits an Etappe 3 uebergeben, die Feldzuordnungen teilen nach dem Scharfschalten sein Schicksal (Regel auf `LdapFieldMapping` ueber Join auf `LdapConfig`, Migration 20260618112133). Die Klasse folgt der Elternzeile `ldapConfig` (`beides`). +- GEAENDERT `module-registry/module-registry.service.ts | module`: Stand `ungebunden` -> `gemischt`, Klasse bleibt; ergaenzen: die gebundene Haelfte stammt ausschliesslich aus `include: { module: true }` auf `tenantPrisma.tenantModuleActivation` (Zeilen 56, 97, 148), fuer die schutzlose Katalogtabelle wirkungslos. +- GEAENDERT `tenders/tender-matching.service.ts | tender`: Stand `ungebunden` -> `gemischt`, Klasse bleibt; ergaenzen: gebundene Haelfte aus `include: { tender: true }` auf `tenantPrisma.tenderMatch.findMany` (Zeile 139) im Sofortmeldungs-Dispatch. +Dazu den Absatz am Kopf von `## Bestandsaufnahme`, der die Erkennungsluecke als in 260911-e2s vermessen beschreibt, ERSETZEN durch einen Absatz **Erkennungslücke GESCHLOSSEN (260911-mkj, WINDOWS #27):** was die vierte Form sieht (Aufzaehlung aus must_haves Wahrheit 1), was sie bewusst NICHT sieht und wie das begrenzt ist (Empfaenger ausserhalb der vier Formen -> Waechter raw/matched + `RELATION_SPEC_EXCEPTIONS`; `include:`/`select:`-Werte aus fremden Dateien -> Wertform-Waechter; Kurzschreibweise `include: { x }` -> gemessen null Vorkommen, kein Prisma-Idiom fuer diese Schluessel; Vorlagen-Interpolationen -> zaehlen raw, fallen laut), die gemessenen Zahlen (7 neue Paare, 3 fortgeschriebene Staende, 1 Ausnahmedatei) und den Satz, dass die Spalte `Modell` seither auch RELATIONSZIELE fuehrt, die in der Datei nie als `.` stehen. + +Verboten in diesem Plan: irgendeine Datei unter `apps/api/src` ausser der Spec, `apps/api/prisma/**`, Compose-/Umgebungsdateien. Nach GRUENEM Spec-Lauf Baseline pruefen und committen: `test(quick-260911-mkj): vierte Erkennungsform fuer Relationszugriffe, WINDOWS #27` — das Dokument gehoert mit in diesen Commit (Spec und Tabelle sind nur zusammen gruen). + + + cd /home/vicolab/projects/tessera-ctl && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && S=apps/api/src/prisma/rls-access-inventory.spec.ts && grep -q 'function analyzeSource(' "$S" && grep -q 'function parseSchemaRelations(' "$S" && grep -q 'const SCHEMA_RELATIONS' "$S" && grep -q 'const RELATION_SPEC_EXCEPTIONS' "$S" && grep -q "'apps/api/src/tenders/backfill-tender-source.ts'" "$S" && grep -q 'rawRelationSpecCount' "$S" && grep -q 'matchedRelationSpecCount' "$S" && grep -q 'unresolvedRelationSpecValues' "$S" && grep -q 'createManyAndReturn' "$S" && grep -qF '(?=\s|$)' "$S" && grep -q "vierte Erkennung: Relationszugriffe" "$S" && grep -q "_count: { select: { users: true } }" "$S" && grep -q "include: { tenant: true, fieldMappings: true }" "$S" && grep -q "memberships: { some: { userId } }" "$S" && grep -q "_count: true" "$S" && grep -q "IMPORTED_SELECT" "$S" && N=$(grep -c "^ it(\|^ it(" "$S") && { test "$N" -ge 24 || { echo "TESTZAHL IM SPEC: $N it(, erwartet mindestens 24 (13 bisherige + 3 Waechter + 2 Schema + mindestens 8 Proben)"; exit 1; }; } && K=docs/mandantentrennung-zugriffsklassifikation.md && for R in 'apps/api/src/groups/module-grants\.service\.ts \| module \| keine-mandantengebundene-tabelle \| gebunden' 'apps/api/src/ldap/ldap-config\.service\.ts \| tenant \| keine-mandantengebundene-tabelle \| ungebunden' 'apps/api/src/module-registry/module-access\.service\.ts \| group \| muss-mandantengebunden \| gebunden' 'apps/api/src/module-registry/module-access\.service\.ts \| groupMembership \| muss-mandantengebunden \| gebunden' 'apps/api/src/tenders/tender-digest\.scheduler\.ts \| tender \| keine-mandantengebundene-tabelle \| gebunden' 'apps/api/src/tenders/tender-digest\.scheduler\.ts \| tenderSavedSearch \| muss-mandantengebunden \| gebunden' 'apps/api/src/tenders/tenders\.controller\.ts \| tenderSource \| keine-mandantengebundene-tabelle \| ungebunden' 'apps/api/src/ldap/ldap-config\.service\.ts \| ldapFieldMapping \| beides \| gemischt' 'apps/api/src/module-registry/module-registry\.service\.ts \| module \| keine-mandantengebundene-tabelle \| gemischt' 'apps/api/src/tenders/tender-matching\.service\.ts \| tender \| keine-mandantengebundene-tabelle \| gemischt'; do grep -qE "^\| $R \|" "$K" || { echo "BESTANDSAUFNAHME: Zeile fehlt oder falsch: $R"; exit 1; }; done && P=$(grep -cE '^\| apps/api/src/[^|]+ \| [a-zA-Z]+ \| (muss-mandantengebunden|bewusst-uebergreifend|keine-mandantengebundene-tabelle|beides) \| (gebunden|ungebunden|gemischt) \|' "$K") && { test "$P" -ge 72 || { echo "PAARE: $P, erwartet mindestens 72 (65 + 7 gemessene neue)"; exit 1; }; } && test 0 -eq "$(grep -c 'seit 260911-e2s vermessen' "$K")" && awk '/^## Bestandsaufnahme$/{f=1; next} /^\| Datei \| Modell/{f=0} f && /260911-mkj/{m=1} f && /RELATION_SPEC_EXCEPTIONS/{e=1} f && /[Gg][Ee][Ss][Cc][Hh][Ll][Oo][Ss][Ss][Ee][Nn]/{g=1} END{ if(!m||!e||!g){print "BESTANDSAUFNAHME-KOPF: der Absatz zur geschlossenen Erkennungsluecke (260911-mkj, RELATION_SPEC_EXCEPTIONS) fehlt"; exit 1} }' "$K" && grep -qE '^\| apps/api/src/ldap/ldap-config\.service\.ts \| ldapFieldMapping \| beides \| gemischt \| .*getAllActiveConfigs' "$K" && TOUT=$(npm --prefix apps/api run test 2>&1) && echo "$TOUT" | tail -6 && T=$(echo "$TOUT" | sed -nE 's/^ *Tests +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$T" && test "$T" -gt 994 || { echo "TESTZAHL: ${T:-unbekannt}, erwartet mehr als 994"; exit 1; }; } && TF=$(echo "$TOUT" | sed -nE 's/^ *Test Files +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$TF" && test "$TF" -ge 62 || { echo "TESTDATEIEN: ${TF:-unbekannt}, erwartet mindestens 62"; exit 1; }; } && npm --prefix apps/api run type-check && git rev-parse --verify cc26197 >/dev/null && PRISMA_CHANGED=$(git diff --name-only cc26197 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only cc26197) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/src/prisma/rls-access-inventory\.spec\.ts|docs/mandantentrennung-zugriffsklassifikation\.md|docs/mandantentrennung-etappe2-fehlerrichtung\.md|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DER ERLAUBNISLISTE:\n%s\n' "$UNEXPECTED"; exit 1; }; } && test -z "$(git diff --name-only cc26197 -- docker-compose.yml docker-compose.*.yml .env .env.* apps/api/.env apps/api/.env.* 2>/dev/null)" + + Spec gruen mit vierter Form, drei Waechtern, zwei Schema-Tests und mindestens acht gepinnten Proben (A/B beweisen die #27-Form ungebunden/gebunden); die Zwischenmessung (rot mit 7 fehlenden Paaren und 3 Stand-Abweichungen) steht woertlich im SUMMARY; die Bestandsaufnahme fuehrt die sieben neuen und drei fortgeschriebenen Zeilen mit Begruendung, der Kopfabsatz beschreibt Reichweite und Grenzen der vierten Form; mehr als 994 Tests in mindestens 62 Dateien gruen, Typpruefung sauber; nichts ausserhalb der Erlaubnisliste geaendert. + + + + Task 2: Die handgepflegten Abschnitte neu ableiten, Kritikschrift nachtragen, WINDOWS #27 mit Nachweis schliessen und die Restmenge als eigenen Eintrag fuehren + docs/mandantentrennung-zugriffsklassifikation.md, docs/mandantentrennung-etappe2-fehlerrichtung.md, .planning/WINDOWS.md, apps/api/src/prisma/rls-access-inventory.spec.ts + +**1. Klassifikationsdokument, vier Abschnitte ableiten (die Bestandsaufnahme selbst ist seit Aufgabe 1 fertig):** +- `## Übersicht je Bereich`: die beiden Rohtreffer-Greps (`this\.prisma\.[a-zA-Z]*` bzw. `tenantPrisma\.[a-zA-Z]*\.`) sehen Relationsziele strukturell nicht; Summenzeile bleibt bei den abgeleiteten Werten (zur Planungszeit 68/178 — mit der Schleife aus dem Gate NACHRECHNEN, nicht abschreiben). Fuege unmittelbar nach dem Absatz „Methodische Lücke, seit 260909-jts sichtbar" einen datierten Absatz **Zweite methodische Lücke, seit 260911-mkj geschlossen — aber nur in der Bestandsaufnahme:** ein: die Rohtrefferzaehlung dieser Tabelle zaehlt Relationsziele (`include:`/`select:`/`_count:`/Relationsfilter) NICHT, die Spalten behalten ihre Bedeutung; autoritativ fuer Relationszugriffe ist allein die Bestandsaufnahme, deren vierte Erkennungsform die Ziele als eigene Paare fuehrt (sieben davon tauchen in KEINER Rohtrefferzahl auf, z. B. `module-access.service.ts`/`group`). +- `## Klassen-Verteilung (nach (Datei, Modell)-Fundstellen, N Paare)`: N = aus der Tabelle GEZAEHLT (Gate). Neuer Absatz **Stand 260911-mkj (Aufgabe 1/2):** N Paare — 65 aus dem Endstand der Etappe 2 plus die neuen Paare der vierten Erkennungsform (alle sieben mit Klasse und Stand aufzaehlen); drei Paare aendern ihren Stand, eines davon auch die Klasse (`ldap-config.service.ts`/`ldapFieldMapping` von `muss-mandantengebunden` auf `beides` — Grund in einem Satz); der Satz, dass die Zahl der Ausgabe von `rls-access-inventory.spec.ts` entnommen ist. Tabelle `| Klasse | Anzahl Paare |` mit den GEZAEHLTEN Werten je Klasse und Summe (Erwartung 35/21/14/2/72 — nur als Plausibilitaet, das Gate zaehlt). +- `## Der Hintergrunddienst als Falle — sechs Fälle`: Ueberschrift bleibt (kein neuer Dienst). Im ersten Fall (`ldap`, `getAllActiveConfigs`) einen Absatz **Nachtrag (260911-mkj):** ergaenzen: derselbe Aufruf reicht ueber `include: { tenant: true, fieldMappings: true }` in `Tenant` (schutzlos, harmlos) und `LdapFieldMapping` (geschuetzt ueber Join auf `LdapConfig`) hinein; nach dem Scharfschalten verstummt die Feldzuordnung mit der Elternzeile, nicht getrennt von ihr — der Etappe-3-Systemkontext muss BEIDE Tabellen sehen; die Bestandsaufnahme fuehrt das Paar seither als `beides`/`gemischt`. +- `## Was diese Etappe NICHT entscheidet`: pruefen, ob dort ein Punkt auf die Relations-Blindstelle verweist; wenn ja, mit „(geschlossen, 260911-mkj)" markieren, wenn nein, nichts anlegen. + +**2. Kritikschrift `docs/mandantentrennung-etappe2-fehlerrichtung.md`:** unter `### (n4) Was dieser Durchlauf bewusst nicht löst` (im `## Bereich tenant`, dort ist #27 entstanden) einen Absatz **Nachtrag (260911-mkj):** WINDOWS #27 ist geschlossen — vierte Erkennungsform, Nachweis gegen kontrollierte Proben im Spec (die drei realen Stellen tragen die Form seit (n3)/Aufgabe 3 nicht mehr), gemessene Zahlen (7 neue Paare, 3 fortgeschriebene Staende, davon `ldap-config`/`ldapFieldMapping` als einziger Klassenwechsel), und die verbleibende, LAUT gehaltene Grenze (Empfaenger ausserhalb der vier Formen: `tenders.seed.ts`, `backfill-tender-source.ts`, eigener Ledger-Eintrag). Im `## Etappe 2 — Abschluss` hinter der Nennung von #27 in Klammern „(seit 260911-mkj geschlossen)" einfuegen, sonst nichts an dem Abschnitt aendern. + +**3. Ledger.** Erst den neuen Eintrag anlegen, dann #27 schliessen, dann den Platzhalter im Spec ersetzen: +- `node ~/.claude/gsd-core/bin/gsd-tools.cjs windows append --kind unmet-truth --phase quick-260911-mkj --file apps/api/src/tenders/tenders.seed.ts --description ""` — Inhalt: Modellaufrufe auf Empfaengern, die weder `this.prisma` noch eine `const X = forTenant(`-Zuweisung noch ein Transaktionsparameter sind, sind fuer ALLE vier Erkennungsformen der Bestandsaufnahme unsichtbar. Gemessen 260911-mkj: `tenders/tenders.seed.ts` (Funktionsparameter `prisma: PrismaService`, `tenderRssFeedSource.findFirst`/`create`, kein Eintrag in der Bestandsaufnahme) und `tenders/backfill-tender-source.ts` (eigenstaendiges Skript mit `new PrismaClient()`, `tender.findMany`/`update`, durch `RELATION_SPEC_EXCEPTIONS` laut gehalten). Beide beruehren nur den plattformglobalen Katalog bzw. die plattformweite RSS-Verwaltung (WINDOWS #24), heute ungefaehrlich; OFFEN ist der Mechanismus (ein kuenftiger Dienst mit Parameter-Empfaenger auf einer geschuetzten Tabelle bliebe unsichtbar). Zu schliessen vor Etappe 4 durch eine Zaehlung ALLER `..(`-Anker gegen die bekannte Empfaengermenge, Ueberschuss laut. +- Die vergebene Nummer aus der Ausgabe lesen (oder `grep -E '^\| [0-9]+ \| quick-260911-mkj \|' .planning/WINDOWS.md`), im Spec-Kommentar `WINDOWS #TBD-MKJ` durch `WINDOWS #` ersetzen. +- `node ~/.claude/gsd-core/bin/gsd-tools.cjs windows fixed 27`. Der Nachweis (Testnamen der Proben A/B, die Zwischenmessung aus Aufgabe 1, die Zahlen 7/3/1) steht im SUMMARY unter einer Ueberschrift „Nachweis WINDOWS #27". + +**4. STATE.md** nicht manuell anfassen (der Quick-Workflow schreibt den Eintrag); im SUMMARY die Zahlen fuer den naechsten Halt liefern: Paare (gezaehlt), Tests (gezaehlt), Ledger offen/gesamt (aus dem Kopf der WINDOWS.md). + +Baseline pruefen, committen: `docs(quick-260911-mkj): Abschnitte abgeleitet, WINDOWS #27 geschlossen, Restmenge als eigener Eintrag`. + + + cd /home/vicolab/projects/tessera-ctl && K=docs/mandantentrennung-zugriffsklassifikation.md && P=$(grep -cE '^\| apps/api/src/[^|]+ \| [a-zA-Z]+ \| (muss-mandantengebunden|bewusst-uebergreifend|keine-mandantengebundene-tabelle|beides) \| (gebunden|ungebunden|gemischt) \|' "$K") && { test "$P" -ge 72 || { echo "PAARE: $P, erwartet mindestens 72"; exit 1; }; } && { grep -qE "^## Klassen-Verteilung \(nach \(Datei, Modell\)-Fundstellen, ${P} Paare\)" "$K" || { echo "KLASSEN-VERTEILUNG: Ueberschrift nennt nicht die gezaehlten ${P} Paare"; exit 1; }; } && { grep -qE "^\| \*\*Summe\*\* \| \*\*${P}\*\* \|$" "$K" || { echo "KLASSEN-VERTEILUNG: Summe nennt nicht ${P}"; exit 1; }; } && for C in muss-mandantengebunden keine-mandantengebundene-tabelle beides bewusst-uebergreifend; do N=$(grep -cE "^\| apps/api/src/[^|]+ \| [a-zA-Z]+ \| ${C} \|" "$K"); grep -qE "^\| ${C} \| ${N} \|$" "$K" || { echo "KLASSEN-VERTEILUNG: ${C} nennt nicht die gezaehlten ${N}"; exit 1; }; done && grep -q '^\*\*Stand 260911-mkj' "$K" && awk '/^\*\*Stand 260911-mkj/{f=1} /^\| Klasse \| Anzahl Paare \|/{f=0} f && /module-access\.service\.ts/{a=1} f && /tenderSource/{b=1} f && /ldapFieldMapping/{c=1} f && /beides/{d=1} END{ if(!a||!b||!c||!d){print "STAND-ABSATZ 260911-mkj: neue Paare oder der Klassenwechsel sind nicht benannt"; exit 1} }' "$K" && TU=0 && TB=0 && for d in apps/api/src/*/; do u=$(grep -ro "this\.prisma\.[a-zA-Z]*" "$d" 2>/dev/null | grep -v spec | wc -l | tr -d ' '); b=$(grep -ro "tenantPrisma\.[a-zA-Z]*\." "$d" 2>/dev/null | grep -v spec | wc -l | tr -d ' '); TU=$((TU+u)); TB=$((TB+b)); done && { grep -qE "^\| \*\*Summe\*\* \| \*\*${TU}\*\* \| \*\*${TB}\*\* \|" "$K" || { echo "SUMMENZEILE nennt nicht die abgeleiteten Werte ${TU}/${TB}"; exit 1; }; } && awk '/^## Übersicht je Bereich/{f=1; next} /^\| Bereich \| Ungebunden/{f=0} f && /260911-mkj/{m=1} f && /module-access\.service\.ts/{x=1} END{ if(!m||!x){print "UEBERSICHT: der datierte Absatz zur zweiten methodischen Luecke (260911-mkj, Beispiel module-access) fehlt"; exit 1} }' "$K" && grep -q '^## Der Hintergrunddienst als Falle — sechs Fälle$' "$K" && test 0 -eq "$(grep -c 'sieben Fälle' "$K")" && awk '/^## Der Hintergrunddienst als Falle/{f=1; next} /^## /{f=0} f && /Nachtrag \(260911-mkj\)/{n=1} f && /fieldMappings/{m=1} f && /LdapFieldMapping/{l=1} END{ if(!n||!m||!l){print "HINTERGRUNDDIENST: Nachtrag (260911-mkj) zu getAllActiveConfigs/fieldMappings/LdapFieldMapping fehlt"; exit 1} }' "$K" && D=docs/mandantentrennung-etappe2-fehlerrichtung.md && awk '/^### \(n4\)/{f=1} /^### \(n5\)/{f=0} f && /Nachtrag \(260911-mkj\)/{n=1} f && /tenders\.seed\.ts/{s=1} END{ if(!n||!s){print "(n4): Nachtrag (260911-mkj) mit der Restmenge (tenders.seed.ts) fehlt"; exit 1} }' "$D" && awk '/^## Etappe 2 — Abschluss$/{f=1; next} /^## /{f=0} f && /260911-mkj/{m=1} END{ if(!m){print "ABSCHLUSS: der Vermerk zu #27 (260911-mkj) fehlt"; exit 1} }' "$D" && W=.planning/WINDOWS.md && { grep -E '^\| 27 \| ' "$W" | grep -q '| fixed |' || { echo "WINDOWS #27 steht nicht auf fixed"; exit 1; }; } && NEWID=$(grep -E '^\| [0-9]+ \| quick-260911-mkj \|' "$W" | head -1 | sed -E 's/^\| ([0-9]+) \|.*/\1/') && { test -n "$NEWID" || { echo "WINDOWS: kein neuer Eintrag quick-260911-mkj"; exit 1; }; } && grep -E "^\| ${NEWID} \| " "$W" | grep -q 'tenders\.seed\.ts' && grep -E "^\| ${NEWID} \| " "$W" | grep -q 'backfill-tender-source\.ts' && OC=$(sed -nE 's/^open_count: ([0-9]+)$/\1/p' "$W") && TC=$(sed -nE 's/^total_count: ([0-9]+)$/\1/p' "$W") && ROWS=$(grep -cE '^\| [0-9]+ \| ' "$W") && { test "$ROWS" -eq "$TC" || { echo "WINDOWS: total_count=$TC, Tabellenzeilen=$ROWS"; exit 1; }; } && OPENROWS=$(grep -E '^\| [0-9]+ \| ' "$W" | grep -c '| open |') && { test "$OPENROWS" -eq "$OC" || { echo "WINDOWS: open_count=$OC, offene Zeilen=$OPENROWS"; exit 1; }; } && S=apps/api/src/prisma/rls-access-inventory.spec.ts && test 0 -eq "$(grep -c 'TBD-MKJ' "$S")" && grep -q "WINDOWS #${NEWID}" "$S" && DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && SOUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$SOUT" | tail -1 && N=$(echo "$SOUT" | sed -nE 's/^Alle ([0-9]+) Pruefungen bestanden\.$/\1/p') && { test -n "$N" && test "$N" -ge 137 || { echo "WERKZEUG: ${N:-nicht alle} Pruefungen bestanden, erwartet mindestens 137"; exit 1; }; } && TOUT=$(npm --prefix apps/api run test 2>&1) && echo "$TOUT" | tail -6 && T=$(echo "$TOUT" | sed -nE 's/^ *Tests +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$T" && test "$T" -gt 994 || { echo "TESTZAHL: ${T:-unbekannt}, erwartet mehr als 994"; exit 1; }; } && TF=$(echo "$TOUT" | sed -nE 's/^ *Test Files +([0-9]+) passed.*/\1/p' | head -1) && { test -n "$TF" && test "$TF" -ge 62 || { echo "TESTDATEIEN: ${TF:-unbekannt}, erwartet mindestens 62"; exit 1; }; } && npm --prefix apps/api run type-check && git rev-parse --verify cc26197 >/dev/null && PRISMA_CHANGED=$(git diff --name-only cc26197 -- apps/api/prisma) && { test -z "$PRISMA_CHANGED" || { printf 'SCHEMA/MIGRATION GEAENDERT — verboten:\n%s\n' "$PRISMA_CHANGED"; exit 1; }; } && CHANGED=$(git diff --name-only cc26197) && UNEXPECTED=$(printf '%s\n' "$CHANGED" | grep -v '^$' | grep -vE '^(apps/api/src/prisma/rls-access-inventory\.spec\.ts|docs/mandantentrennung-zugriffsklassifikation\.md|docs/mandantentrennung-etappe2-fehlerrichtung\.md|\.planning/.*)$' || true) && { test -z "$UNEXPECTED" || { printf 'UNERWARTETE AENDERUNGEN AUSSERHALB DER ERLAUBNISLISTE:\n%s\n' "$UNEXPECTED"; exit 1; }; } && test -z "$(git diff --name-only cc26197 -- docker-compose.yml docker-compose.*.yml .env .env.* apps/api/.env apps/api/.env.* 2>/dev/null)" + + Klassen-Verteilung, ihre Ueberschrift, Summenzeile und Uebersichtsabsatz sind aus Tabelle und Greps ABGELEITET und stimmen mit den Gates ueberein; der Hintergrunddienst-Abschnitt traegt den Nachtrag zum ersten Fall; die Kritikschrift traegt Nachtrag (n4) und Abschluss-Vermerk; WINDOWS #27 ist `fixed`, der neue Eintrag fuer die Empfaenger ausserhalb der vier Formen existiert und ist im Spec-Kommentar referenziert; Zaehler im Ledger-Kopf konsistent; Werkzeug mindestens 137, mehr als 994 Tests in mindestens 62 Dateien, Typpruefung sauber; Erlaubnisliste gegen `cc26197` eingehalten; SUMMARY traegt „Nachweis WINDOWS #27" mit der Zwischenmessung und den Zahlen. + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Quelltext -> Detektor | Der Detektor ist eine MESSUNG; ein Fehler dort ist kein Sicherheitsvorfall, aber er entscheidet, ob die Etappe-4-Vorabpruefung auf einer vollstaendigen Bestandsaufnahme steht. Fehlerrichtung: Ueberberichten faellt laut (Dokument rot), Unterberichten faellt still (die eigentliche #27-Falle). | +| schema.prisma -> Relationszuordnung | Das Schema wird gelesen, nie geschrieben. Eine falsche Zuordnung Feld -> Modell erzeugt Phantom-Paare (laut) oder verschluckt Ziele (still). | +| Bestandsaufnahme -> Etappe 4 | Neue Zeilen muessen mit der Klassifikation gleicher Modelle konsistent sein, sonst traegt die Vorabpruefung widerspruechliche Annahmen. | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-MKJ-01 | Information Disclosure (stilles Unterberichten) | `analyzeSource` vierte Form — Regex verfehlt eine Formatierungsvariante (Wert als Konstante, Empfaenger ausserhalb der vier Formen, Angabe in Vorlagen-Interpolation, neue Prisma-Operation) | high | mitigate | Rohzahl `include|select|_count` ueber den ganzen kommentarfreien Quelltext gegen die innerhalb erkannter Aufrufe gezaehlte Zahl (Waechter (a), Ausnahmen nur mit Grund in `RELATION_SPEC_EXCEPTIONS`, Stale-Waechter); Wertform-Waechter (b) fuer Kennungen ohne Objektliteral in derselben Datei; Probe 9/10 pinnen beide Waechter. Verbleibende, benannte Grenze: Kurzschreibweise `include: { x }` (gemessen null, kein Prisma-Idiom) und Empfaenger ausserhalb der vier Formen ohne Relationsangabe (`tenders.seed.ts`) — als eigener Ledger-Eintrag gefuehrt, nicht still. | +| T-MKJ-02 | Tampering (falsche Zuordnung) | `parseSchemaRelations` — Feldtyp am Zeilenende (`User[]` ohne Folgezeichen) oder ein Skalar, der zufaellig wie ein Modellname heisst | medium | mitigate | Lookahead `(?=\s\|$)` (der erste Prototyp verlor damit ALLE Listenrelationen — im Plan als Fehler benannt); Test pinnt `Tenant.users -> User`, `Group.memberships -> GroupMembership`, `LdapConfig.fieldMappings -> LdapFieldMapping`; jedes Ziel muss Modellname sein; Modellanzahl gegen `^model `-Zeilen. Klientenname per kleinem Anfangsbuchstaben, dieselbe Form wie Spalte `Modell`. | +| T-MKJ-03 | Repudiation (inkonsistente Klassifikation) | Neue Bestandsaufnahme-Zeilen widersprechen bestehenden Zeilen desselben Modells | medium | mitigate | Klassen sind aus den Nachbarzeilen desselben Modells abgeleitet (`Tender`/`Module`/`Tenant`/`TenderSource` bleiben `keine-mandantengebundene-tabelle` auch bei gebundener Einbindung; `ldapFieldMapping` folgt der Elternzeile `ldapConfig` auf `beides`); Klassen-Verteilung, Ueberschrift und Summen werden per Gate aus der Tabelle GEZAEHLT; der Spec prueft Stand je Paar gegen den Quelltext. | +| T-MKJ-04 | Elevation of Privilege (Ueberberichten als falsche Sicherheit) | Ein Schluessel, der zufaellig ein Relationsfeld des Kontextmodells ist (z. B. `where: { tenant: ... }` als Relationsfilter) wird als Fundstelle gefuehrt | low | accept | Das IST ein Zugriff auf die zweite Tabelle (Prisma rendert Relationsfilter, Relations-Sortierung und verschachtelte Schreibzugriffe als Unterabfrage/Join unter der Regel der Zieltabelle); ein moegliches Ueberberichten (Ternaer-Operator `a ? b : c` mit `b` = Relationsname) faellt laut ins Dokument und wird dort begruendet — die vorgesehene Fehlerrichtung. | +| T-MKJ-05 | Denial of Service (Dienstcode „nebenbei" konvertiert) | Ein Zusatzfund verleitet zur Behebung im Dienstcode | medium | mitigate | Erlaubnisliste gegen `cc26197` als Gate in beiden Aufgaben (nur Spec, zwei Dokumente, `.planning/`); Zusatzfund auf ungeschuetztem Elternpfad in geschuetzte Tabelle -> ANHALTEN, SUMMARY-Befund. | +| T-MKJ-SC | Tampering | npm/pip/cargo installs | low | accept | Keine Paketinstallation in diesem Plan (nur `node:fs`/`node:path`/vitest, bereits vorhanden); Lockfile darf sich nicht aendern (Erlaubnisliste). | + + + +- Aufgabe 1: `npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts` gruen; die zehn geforderten Tabellenzeilen vorhanden; Kopfabsatz ersetzt; Gesamtsuite mehr als 994 Tests in mindestens 62 Dateien; Typpruefung; Erlaubnisliste. +- Aufgabe 2: Klassen-Verteilung/Ueberschrift/Summen aus der Tabelle gezaehlt; Uebersichtsabsatz und Hintergrunddienst-Nachtrag vorhanden; Kritikschrift (n4) und Abschluss; WINDOWS #27 `fixed`, neuer Eintrag mit beiden Dateien, Zaehler konsistent; Werkzeug mindestens 137; Gesamtsuite; Typpruefung; Erlaubnisliste. +- Ausdruecklich NICHT gemessen (Grenze, nicht Luecke): der Detektor ersetzt keine Live-Messung an der Datenbank — dass Prisma `_count` als LEFT-JOIN-Unterabfrage unter der Regel der Zieltabelle rendert, ist in 260911-e2s gegen die Wegwerf-Datenbank bewiesen und wird hier nicht wiederholt. + + + +- Die vierte Erkennungsform existiert, ist gegen acht kontrollierte Proben gepinnt (darunter die #27-Form ungebunden UND gebunden) und faellt bei jeder erkannten Grenze laut (drei Waechter). +- Die Bestandsaufnahme fuehrt mindestens 72 Paare, alle aus der Spec-Ausgabe entnommen; die vier handgepflegten Abschnitte sind per Gate aus Tabelle und Greps abgeleitet. +- WINDOWS #27 `fixed` mit Nachweis im SUMMARY; die von allen vier Formen unerreichbare Restmenge ist eigener Ledger-Eintrag. +- Baseline gehalten (mehr als 994 Tests, 62+ Dateien, Typpruefung, Werkzeug 137+), Schalter aus, kein Dienstcode, kein Schema, keine Umgebungsdatei. + + + +Create `.planning/quick/260911-mkj-windows-27-schliessen-relations-blindste/260911-mkj-SUMMARY.md` when done — mit dem Abschnitt „Nachweis WINDOWS #27" (Zwischenmessung woertlich: fehlende Paare, Stand-Abweichungen; die Zahlen 7/3/1 oder die tatsaechlich gemessenen, wenn hoeher, mit Einzelpruefung jedes Zusatzfundes) und, falls der Detektor eine ungebundene Einbindung in eine geschuetzte Tabelle auf ungeschuetztem Elternpfad zeigt, die nicht als Hintergrunddienst-Fall benannt ist, einem Abschnitt „BEFUND — nicht behoben". +