Files
tessera-ctl/.planning/quick/260911-mkj-windows-27-schliessen-relations-blindste/260911-mkj-PLAN.md
T
schalli 261e73603e 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-11 16:30:49 +02:00

53 KiB
Raw Blame History

phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
phase plan type wave depends_on autonomous requirements files_modified estimate must_haves
quick-260911-mkj 01 execute 1
true
WINDOWS-27
ETAPPE-4-VORAUSSETZUNG
apps/api/src/prisma/rls-access-inventory.spec.ts
docs/mandantentrennung-zugriffsklassifikation.md
docs/mandantentrennung-etappe2-fehlerrichtung.md
.planning/WINDOWS.md
tokens raw_tokens tasks confidence
120000 120000 2 low
truths artifacts key_links
Die Bestandsaufnahme SIEHT Relationszugriffe: `analyzeSource()` in `rls-access-inventory.spec.ts` traegt eine vierte Erkennungsform, die innerhalb des Argumentbereichs jedes erkannten Modellaufrufs (`<Empfaenger>.<Modell>.<Operation>(`) 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).
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
`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.

<execution_context> @/.claude/gsd-core/workflows/execute-plan.md @/.claude/gsd-core/templates/summary.md </execution_context>

@.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

<planning_measurements> 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 <x>.<modell>.<op>( 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.<dienst> 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: <KONSTANTE> 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. </planning_measurements>

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<Modell, Map<Feld, Zielmodell>>, 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(<Empfaenger, alternativ>)\.([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+<Name>\b[^=]*=\s*\{ auf blank, Klammertiefe bis zur passenden }); gibt es keine, haenge <relPath>: <schluessel>: <Kennung> 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<string>(['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 <behavior> — 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 <Klient>.<Modell> 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 "<ein Absatz>" — 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 <Kennung>.<Modell>.<Operation>(-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 #<Nummer> 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.

<threat_model>

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
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).
</threat_model>
- 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.

<success_criteria>

  • 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. </success_criteria>
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".