Files
schalli 93444aa91e docs(quick-260910-jab): Plan fuer die drei zu kurz greifenden Datenbankregeln
Schliesst T-JTS-02, T-JTS-03 und WINDOWS #19 in einer handgeschriebenen
Migration. Drei zur Planungszeit gemessene Funde praegen den Zuschnitt:
eine DRITTE loch-behauptende Pruefung im Wegwerf-Werkzeug (nicht zwei), eine
Zeile, auf der zwei spaetere Pruefungen aufsetzen und die nach der Reparatur
nicht mehr entsteht, und genau ein Anwendungspfad, den die Reparatur still
falsch machen wuerde.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AMASaSxv5QMY7RncqZriRR
2026-09-10 14:11:39 +02:00

63 KiB

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-260910-jab 01 execute 1
true
WINDOWS-19
T-JTS-02
T-JTS-03
apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql
apps/api/scripts/rls-scratch-check.mjs
apps/api/src/groups/migration-sql.spec.ts
apps/api/src/tenders/tender-rss-feed.service.ts
apps/api/src/tenders/tender-rss-feed.service.spec.ts
apps/api/src/tenders/tenders.controller.ts
apps/api/src/tenders/tenders.controller.spec.ts
apps/api/src/groups/groups.service.ts
apps/api/src/groups/module-grants.service.ts
apps/api/src/module-registry/module-access.service.ts
apps/api/src/prisma/rls-coverage.spec.ts
docs/mandantentrennung-zugriffsklassifikation.md
docs/mandantentrennung-etappe2-fehlerrichtung.md
docs/mandantentrennung-datenbankrolle.md
.planning/WINDOWS.md
tokens raw_tokens tasks confidence
210000 210000 3 low
truths artifacts key_links
Die Regel auf `GroupMembership` prueft nach diesem Durchlauf BEIDE Seiten der Beziehung: die Gruppenseite wie bisher UND die Benutzerseite ueber denselben Join-Praezedenzfall, den `PasswordResetToken` seit 20260618112133 vormacht. Eine Mitgliedschaft, die eine Gruppe des einen Mandanten mit einem Benutzer eines anderen verbindet, wird von der Datenbank abgewiesen — gemessen mit einem Benutzer, den es im anderen Mandanten TATSAECHLICH gibt, nicht mit einer erfundenen Kennung.
Die Regel auf `ModuleGrant` prueft nach diesem Durchlauf nicht mehr nur die Mandantenkennung der Zeile, sondern auch, wohin die Zeile zeigt: eine Freigabe mit korrekter eigener Mandantenkennung, aber fremder Gruppen- ODER fremder Benutzerkennung, wird abgewiesen. Beide Zweige des Entweder-oder (D-04) sind einzeln gemessen.
`assertTargetBelongsToTenant` in `module-grants.service.ts` steht am Ende dieses Durchlaufs unveraendert und wird von einem Test gehalten, der rot wird, wenn jemand sie als 'macht jetzt die Datenbank' entfernt. Die Datenbankregel ist ein ZWEITES Netz, kein Ersatz.
Fuer `TenderRssFeedSource` ist der Lesezugriff vom Schreibzugriff getrennt: ein gebundener Lesezugriff liefert die eigenen UND die plattformweiten Zeilen, waehrend Einfuegen, Aendern und Loeschen weiterhin einen Mandanten verlangen. BEIDE Fehlerrichtungen sind einzeln gemessen — zu streng (plattformweite Zeile bleibt unsichtbar) und zu locker (ein Mandant koennte eine plattformweite Zeile aendern oder entfernen).
Die drei Pruefungen des Wegwerf-Werkzeugs, die die Loecher bisher als erwartetes Verhalten festhielten, sind UMGEKEHRT — nicht geloescht und nicht gelockert. Jede traegt einen Namen, der die neue Wahrheit sagt, und in ihrem Meldetext einen Verweis auf den alten Befund und den alten Pruefungsnamen, damit der Nachweis, dass das Loch existierte, nicht verloren geht.
Die Zeile `grant-foreign-group`, die bisher als Nebenwirkung einer der umgekehrten Pruefungen entstand und auf der ZWEI spaetere Pruefungen des Bereichs `module-registry` aufsetzen, wird weiterhin angelegt — jetzt ueber die Wartungsrolle. Keine spaetere Pruefung besteht dadurch aus dem falschen Grund (weil es nichts zu finden gaebe).
Die Behauptung 'kein Anwendungscode muss sich aendern' ist geprueft statt geglaubt worden, und ihr gemessenes Ergebnis steht im Bericht: sie trifft fuer GENAU EINEN Pfad nicht zu. `TenderRssFeedSourceService.listForUser` liefert nach der Reparatur ungebunden nur noch die plattformweiten Zeilen statt gar keiner — aus einer schreienden Leere wuerde eine glaubhafte Teilantwort. Dieser Pfad ist deshalb gebunden.
Jede Aufzeichnung, die eine der drei alten Regeln beschreibt, sagt am Ende die Wahrheit: die vier Kopfkommentare im Quelltext, die Beschreibungszeile im Abdeckungstest, die betroffenen Zeilen der Klassifikation samt Uebersichts- und Summenzeile, und die vier ueberholten Stellen der Kritikschrift — jede mit dem Namen der neuen Migration. Eine Aufzeichnung, die ein geschlossenes Loch beschreibt, ohne zu sagen, dass es geschlossen wurde, ist die Luege durch Auslassung, die dieser Durchlauf zu vermeiden hat.
WINDOWS #19 ist mit Beleg geschlossen (Migrationsname plus die namentlich benannten Pruefungen). #18, #20, #21 und #23 bleiben offen. Was die Reparatur NICHT loest — plattformweite Zeilen lassen sich unter der Anwendungsrolle weder anlegen noch entfernen, in der alten wie in der neuen Regel — ist als eigener offener Eintrag aufgezeichnet und verschwindet nicht mit #19.
Der Schalter bleibt AUS: `DATABASE_URL` zeigt weiterhin auf die Rolle `tessera`. Die Regeln sind heute wirkungslos — genau das macht sie jetzt sicher aenderbar und bedeutet zugleich, dass die laufende Anwendung sie nicht bestaetigen kann. Das Wegwerf-Werkzeug und die Regelliste der lebenden Datenbank sind die einzigen Zeugen.
Baseline gehalten: die Testzahl faellt nicht unter den zu Beginn JEDER Aufgabe frisch gemessenen Stand, die Typpruefung ist sauber, das Wegwerf-Werkzeug meldet alle Pruefungen bestanden — am Ende JEDER Aufgabe, nicht nur am Ende des Plans. 'Gruen' bedeutet nach dieser Aufgabe etwas anderes als vorher, und genau das ist der Zweck.
apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql — NEU, handgeschrieben nach dem Muster von 20260909140000: die beiden zu kurz greifenden Regeln ersetzt, die vier befehlsgetrennten Regeln fuer die plattformweiten Zeilen angelegt, `SearchProvider` mit gemessener Begruendung ausdruecklich NICHT angefasst
apps/api/scripts/rls-scratch-check.mjs — drei umgekehrte Pruefungen, die Wartungsrollen-Gegenmessungen dazu, die vier Befehlsrichtungen des Lese-/Schreibsplits, die Bereitstellung von `grant-foreign-group` ueber die Wartungsrolle, und die Extraktion, die die abgeloesten Regeln aus der NEUEN Migration liest
apps/api/src/groups/migration-sql.spec.ts — ein neuer Beschreibungsblock fuer die neue Migration, im Stil der beiden bestehenden Bloecke: reiner Textabgleich ohne Datenbank
apps/api/src/tenders/tender-rss-feed.service.ts — `listForUser` gebunden, alle drei Kopfkommentare an der neuen Regel richtiggestellt
apps/api/src/tenders/tenders.controller.ts + beide Testdateien des Bereichs — die Durchreichung des Mandanten und der Zwei-Klienten-Nachweis
apps/api/src/groups/groups.service.ts, apps/api/src/groups/module-grants.service.ts, apps/api/src/module-registry/module-access.service.ts, apps/api/src/prisma/rls-coverage.spec.ts — die vier Aufzeichnungen im Quelltext, die die alten Regeln beschreiben
docs/mandantentrennung-zugriffsklassifikation.md — der #19-Block beantwortet statt offen, die vier betroffenen Bestandsaufnahme-Zeilen, die Uebersichtszeile `tenders`, die Summenzeile und der Punkt in 'Was diese Etappe NICHT entscheidet'
docs/mandantentrennung-etappe2-fehlerrichtung.md — ein neuer Abschnitt `## Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19` mit den fuenf ueblichen Unterabschnitten, plus Nachtraege an den vier ueberholten Bestandsstellen
docs/mandantentrennung-datenbankrolle.md — die eine Stelle, die #19 als offen fuehrt
.planning/WINDOWS.md — #19 geschlossen mit Beleg, ein neuer offener Eintrag fuer den Schreibweg plattformweiter Zeilen, Zaehler aus dem JSON-Block abgeleitet statt getippt
Das Wegwerf-Werkzeug schneidet die Regeln WORTGLEICH aus den ausgelieferten Migrationsdateien. Sobald eine Regel abgeloest wird, misst die Extraktion die falsche Datei weiter — die Umleitung auf die neue Migration ist deshalb keine Kosmetik, sondern die Bedingung dafuer, dass die umgekehrten Pruefungen ueberhaupt das messen, was sie behaupten.
`grant-foreign-group` entstand bisher als Nebenwirkung der Pruefung, die T-JTS-03 festhielt. Zwei Pruefungen des Bereichs `module-registry` setzen darauf auf: eine schliesst die Zeile aus, die andere weist ueber die Wartungsrolle nach, dass der Ausschluss von der Bindung kommt. Faellt die Zeile weg, besteht die erste aus dem falschen Grund und die zweite scheitert.
Ein `USING`-Ausdruck allein bestimmt auch, welche Zeilen ein UPDATE oder DELETE ueberhaupt erreicht. Ein einziger permissiver Ausdruck, der die plattformweiten Zeilen fuer das Lesen einschliesst, gaebe damit jedem Mandanten das Recht, sie zu aendern und zu entfernen. Die Trennung nach Befehl ist deshalb die Sache selbst, nicht eine Stilfrage.
Nach der Reparatur kehrt sich die Fehlerrichtung fuer `listForUser` um: heute liefert der ungebundene Pfad nach dem Scharfschalten NICHTS, danach die plattformweiten Zeilen. Eine leere Liste faellt auf, eine kurze Liste nicht — die Reparatur erzeugt genau die stille Falschantwort, gegen die dieses ganze Vorhaben laeuft, wenn dieser eine Pfad nicht mitgebunden wird.
Die Praemisse von #19 stimmt nur zur Haelfte: fuer `TenderRssFeedSource` gibt es plattformweite Zeilen und sie werden benutzt (D-06, der geseedete Feed), fuer `SearchProvider` gibt es keinen Codeweg, der eine mandantenlose Zeile erzeugt — die Vorgaben sind Konstanten (05-02). Die ausgelieferte Migration und die Klassifikation sagen das bereits; der Ledger-Eintrag sagt es nicht. Die Schliessung muss diese Haelfte als widerlegte Praemisse schliessen, nicht als geloestes Problem.
Die drei Datenbankregeln schliessen, die kuerzer greifen als sie sollen: `GroupMembership` prueft nur die Gruppen-, nicht die Benutzerseite (T-JTS-02); `ModuleGrant` prueft die Zeile, aber nicht, wohin sie zeigt (T-JTS-03); und die Regel fuer die Tabellen mit nullbarer Mandantenkennung wuerde die plattformweiten Zeilen nach dem Scharfschalten fuer JEDEN Mandanten unsichtbar machen statt nur fuer fremde (WINDOWS #19).

Zweck: alle drei sind heute wirkungslos, weil die Anwendung als Rolle mit Umgehungsrecht verbindet (#18). Genau das macht sie jetzt gefahrlos aenderbar — und bedeutet zugleich, dass die laufende Anwendung die Reparatur nicht bestaetigen kann. Das Wegwerf-Werkzeug und die Regelliste der lebenden Datenbank sind die einzigen Zeugen, die es gibt.

Dieser Durchlauf weicht bewusst von der geplanten Reihenfolge ab, auf ausdrueckliche Anweisung des Nutzers. Die Folge ist einzukalkulieren, nicht zu verschweigen: die fuenf noch offenen Bereiche der Etappe 2 messen ab jetzt gegen die NEUEN Regeln, und mehrere bereits abgeschlossene Bereiche haben gegen die alten gemessen. Diese Messungen stehen aufgezeichnet. Sie muessen am Ende stimmen.

Ergebnis: eine handgeschriebene, lokal angewandte Migration; ein Messwerkzeug, dessen drei loch-behauptende Pruefungen umgekehrt statt entfernt sind; genau ein mitgebundener Anwendungspfad, weil die Reparatur ihn sonst still falsch machen wuerde; und ein Aktenstand, der nirgends mehr ein Loch beschreibt, das es nicht mehr gibt.

Der Schalter bleibt AUS. DATABASE_URL zeigt weiterhin auf die Rolle tessera. Das Scharfschalten ist Etappe 4 und findet hier NICHT statt.

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

@.planning/STATE.md @.planning/WINDOWS.md @docs/mandantentrennung-zugriffsklassifikation.md @docs/mandantentrennung-etappe2-fehlerrichtung.md @docs/mandantentrennung-datenbankrolle.md @apps/api/prisma/migrations/20260618112133_rls_policies/migration.sql @apps/api/prisma/migrations/20260804130918_groups_rls_policies/migration.sql @apps/api/prisma/migrations/20260909140000_rls_remaining_tenant_tables/migration.sql @apps/api/scripts/rls-scratch-check.mjs @apps/api/src/groups/migration-sql.spec.ts @apps/api/src/prisma/rls-coverage.spec.ts @apps/api/src/groups/module-grants.service.ts @apps/api/src/tenders/tender-rss-feed.service.ts @.planning/quick/260910-exd-mandantentrennung-etappe-2-bereich-modul/260910-exd-PLAN.md

<planning_time_findings>

Alle Zahlen und Fundstellen unten sind zur Planungszeit am 2026-09-10 gegen HEAD 4843058 gemessen, mit der jeweils genannten Anweisung. Sie leiten die Untersuchung, sie sind KEINE Bearbeitungsvollmacht — jede Datei wird vor jeder Aenderung erneut gelesen, und jede Zeilenangabe erneut aufgesucht statt abgeschrieben.

Befund A — die drei Regeln, im ausgelieferten Text nachgelesen. 20260804130918_groups_rls_policies/migration.sql fuehrt drei Regeln unter dem Namen tenant_isolation_policy: Group vergleicht direkt gegen current_tenant_id(); GroupMembership prueft ausschliesslich, ob groupId in den Gruppen des Mandanten liegt; ModuleGrant vergleicht ausschliesslich die eigene tenantId. 20260909140000_rls_remaining_tenant_tables/migration.sql legt sechzehn weitere Regeln an, darunter SearchProvider und TenderRssFeedSource, beide in der einfachen Gleichheitsform. Ihr Kopf sagt ausdruecklich, dass keine getrennte Schreibbedingung angelegt wurde, weil PostgreSQL dann denselben Ausdruck auch fuer neu geschriebene Zeilen verwendet — genau diese Vereinfachung ist es, die fuer die nullbaren Spalten nicht traegt.

Befund B — es sind DREI loch-behauptende Pruefungen im Werkzeug, nicht zwei. Die Aufgabenstellung nennt zwei und sagt ausdruecklich, dass der Fund einer Planungszeit-Suche keine Vollstaendigkeitsgarantie ist. Die Suche ist gemacht worden. Die dritte ist tenderrssfeed-plattformzeile-unter-jedem-mandanten-unsichtbar in runTendersAreaChecks (rls-scratch-check.mjs, im Bereich der Zeilen 1041-1061 zu suchen, nicht abzuschreiben): sie besteht, WEIL die plattformweite Zeile unter beiden Mandantenkontexten fehlt, und haelt in ihrem Meldetext zusaetzlich die Handlungsanweisung fest, drei RSS-Pfade deshalb nicht zu binden. Sie faellt mit der Reparatur und ist wie die beiden anderen umzukehren. Gemessen mit grep -n "GELUNGEN\|nicht-verhindert\|erlaubt\|unsichtbar" ueber die Werkzeugdatei; ausserdem wurde die Datei nach weiteren Pruefungen durchgesehen, deren bestandenes Ergebnis das ALTE Verhalten ist — es gibt keine vierte.

Befund C — eine der beiden benannten Pruefungen LIEFERT eine Zeile, auf der zwei spaetere Pruefungen aufsetzen. Das ist die gefaehrlichste Einzelheit dieses Durchlaufs. Die Pruefung, die T-JTS-03 festhaelt, fuegt dabei die Zeile grant-foreign-group ein (Mandant TENANT-A, Gruppe group-b). Zwei Pruefungen des Bereichs module-registry bauen darauf auf: eine weist nach, dass der gebundene Drei-Tabellen-Weg diese Zeile ausschliesst, die andere weist ueber die Wartungsrolle nach, dass der Ausschluss von der Bindung kommt und nicht vom Aufbau. Nach der Reparatur wird das Einfuegen abgewiesen, die Zeile entsteht nicht mehr — die erste Pruefung bestuende dann aus dem falschen Grund (es gibt nichts auszuschliessen) und die zweite SCHEITERT. Gemessen mit grep -n "grant-foreign-group" apps/api/scripts/rls-scratch-check.mjs: fuenf Fundstellen, eine Einfuegestelle und vier Verwendungen. Die Gegenprobe fuer die zweite loch-behauptende Zeile faellt anders aus: membership-foreign-user hat ausser ihrer Einfuegestelle keine Verwendung.

Befund D — die Extraktion liest die Regel aus der Migrationsdatei, und sie kennt nur einen Namen. extractPolicySql() sucht woertlich nach CREATE POLICY tenant_isolation_policy ON "<Tabelle>" und liefert GENAU EINEN Treffer. Daraus folgen zwei Dinge, die sonst still schiefgehen: erstens muss die Extraktion fuer die abgeloesten Regeln auf die NEUE Migrationsdatei zeigen, sonst misst das Werkzeug weiter die abgeloeste Regel und die umgekehrten Pruefungen scheitern aus einem verwirrenden Grund; zweitens braucht die Tabelle mit vier befehlsgetrennten Regeln eine Extraktion, die mehr als einen Treffer liefern kann.

Befund E — die Praemisse von #19 stimmt nur zur Haelfte. Fuer TenderRssFeedSource gibt es plattformweite Zeilen, sie sind gewollt (D-06) und sie sind da: in der lokalen Datenbank zwei Zeilen insgesamt, davon eine mit leerem Benutzer UND leerem Mandanten. Fuer SearchProvider gilt das nicht. Gemessen: dashboard.service.ts ist der einzige Ort, der die Tabelle beruehrt (vier Zugriffe), und der einzige Schreibweg dorthin nimmt die Mandantenkennung als Pflichtparameter entgegen und setzt sie. Die Vorgabe-Suchmaschinen kommen aus der Konstanten DEFAULT_SEARCH_PROVIDERS und werden erst nach dem Lesen davorgehaengt — sie sind keine Datenbankzeilen (Entscheidung 05-02). Lokal gemessen: null Zeilen insgesamt, null mit leerer Mandantenkennung. Sowohl der Kopfkommentar der ausgelieferten Migration als auch die Klassifikation sagen das bereits; allein der Ledger-Eintrag #19 behauptet "von der Administration gepflegte Suchanbieter". Folge fuer diesen Plan: SearchProvider behaelt seine strenge Regel, und die Schliessung von #19 schliesst diese Haelfte als WIDERLEGTE PRAEMISSE, nicht als geloestes Problem. Eine Lockerung waere hier die falsche Richtung: sie wuerde eine kuenftige mandantenlose Zeile jedem Mandanten zeigen.

Befund F — die Behauptung "kein Anwendungscode muss sich aendern" trifft fuer GENAU EINEN Pfad nicht zu. TenderRssFeedSourceService.listForUser ist heute bewusst ungebunden, mit einem Kopfkommentar, der als Begruendung die alte Regel nennt. Nach der Reparatur kehrt sich die Lage um. Heute liefert dieser Pfad ungebunden nach dem Scharfschalten NICHTS (die Gleichheitsbedingung vergleicht den leeren Kontext mit nichts). Danach liefert er die plattformweiten Zeilen — und nur die. Aus einer leeren Liste, die schreit, wird eine kurze Liste, die luegt: der Nutzer saehe die plattformweiten Feeds und haette keinen Anlass zu melden, dass seine eigenen fehlen. Gebunden liefert derselbe Pfad genau das Richtige, eigene plus plattformweite. Die Reparatur ERZEUGT hier also die stille Falschantwort, gegen die dieses Vorhaben laeuft, wenn dieser Pfad nicht mitgebunden wird. Der aufrufende Endpunkt hat den Mandanten bereits zur Hand (er reicht ihn eine Methode weiter beim Anlegen eines eigenen Feeds durch), die Aenderung ist deshalb klein und ohne neue Aufloesung.

Befund G — zwei Pfade bleiben unter der Anwendungsrolle unmoeglich, in der alten wie in der neuen Regel. Das Anlegen einer plattformweiten Zeile setzt die Mandantenkennung leer; die Schreibbedingung verlangt einen Mandanten — vor wie nach der Reparatur abgewiesen. Das Entfernen einer plattformweiten Zeile laeuft ungebunden und trifft nach dem Scharfschalten nichts. Beides ist keine Folge dieses Plans und wird von ihm auch nicht geloest: die vom Ledger vorgegebene Semantik lautet ausdruecklich, dass Schreibzugriffe weiterhin einen Mandanten verlangen. Es ist damit ein Verwaltungsweg, der in Etappe 4 gebaut werden muss, und es darf nicht mit #19 zusammen verschwinden.

Befund H — der Bestand ist sauber, lokal gemessen. Ueber die lokale Datenbank gezaehlt: keine Mitgliedschaft, deren Gruppe und Benutzer zu verschiedenen Mandanten gehoeren; keine Freigabe, deren Gruppe oder Benutzer zu einem anderen Mandanten gehoert als die Zeile selbst; drei Mitgliedschaften, vier Freigaben, ein Mandant. Die schaerferen Regeln machen also lokal keine vorhandene Zeile unsichtbar. Fuer das Testsystem ist das NICHT gemessen und darf nicht angenommen werden — dieselbe Zaehlung gehoert als Vorabpruefung in Etappe 4, denn eine Zeile, die die neue Regel nicht mehr sieht, waere danach weder sichtbar noch loeschbar.

Befund I — zwei Buchhaltungszeilen bewegen sich. Wird listForUser gebunden, sinkt die ungebundene Rohtrefferzahl des Bereichs tenders um eins und die gebundene steigt um eins; die Uebersichtszeile und die Summenzeile der Klassifikation nennen beide Zahlen. Gemessen mit den beiden Anweisungen, die die Klassifikation selbst als maszgeblich fuehrt: tenders steht heute auf 36 ungebunden und 26 gebunden, die Summenzeile auf 108 und 134. Diese Zahlen stehen hier als ERWARTUNG, nicht als Vorgabewert — die Pruefung leitet sie zur Laufzeit erneut aus dem Quelltext ab.

Befund J — welche Aufzeichnungen die Reparatur unwahr macht. Vollstaendig gesucht mit grep -rn "T-JTS-02\|T-JTS-03" und grep -rn "#19" ueber Quelltext und Dokumente. Im Quelltext vier Stellen: der Kopfkommentar zur Zugriffsaufloesung in module-access.service.ts, der Kommentar an der Benutzerpruefung in groups.service.ts, der Kommentar an der Mandanten-Gegenpruefung in module-grants.service.ts und die Beschreibungszeile fuer GroupMembership in der Ausnahmeliste von rls-coverage.spec.ts. Dazu die drei Kopfkommentare in tender-rss-feed.service.ts. In der Klassifikation: der #19-Block, die Bestandsaufnahme-Zeilen fuer searchProvider, groups.service.ts/user, module-grants.service.ts/moduleGrant und tender-rss-feed.service.ts/tenderRssFeedSource, die Uebersichtszeile tenders, die Summenzeile und der Punkt in "Was diese Etappe NICHT entscheidet", der die #19-Regel ausdruecklich als offen fuehrt. In der Kritikschrift: der Punkt in (g4), der beide Regeln als einseitig beschreibt; der Punkt in (t4), der #19 als nicht geloest fuehrt; die Zeile der Signaltabelle des Bereichs tenders zu den drei RSS-Pfaden; die beiden aufgezeichneten Werkzeugausgaben, die die alten Meldetexte woertlich zitieren; und der Punkt im Abschnitt module-registry, der beide Befunde als offen fuehrt. Dazu eine Stelle in der Betriebsanleitung zur Datenbankrolle.

Befund K — bei getrennter Lese- und Schreibbedingung entscheidet der Befehl, nicht der Ausdruck. Ein einziger permissiver Ausdruck, der die plattformweiten Zeilen einschliesst, gilt in PostgreSQL auch fuer UPDATE und DELETE — ein Mandant duerfte eine plattformweite Zeile dann aendern und entfernen. Deshalb wird die Regel dieser Tabelle nach BEFEHL getrennt: eine Leseregel, die die plattformweiten Zeilen einschliesst, und je eine Regel fuer Einfuegen, Aendern und Entfernen, die einen Mandanten verlangen. Vier ausdrueckliche Regeln statt einer mit stillschweigender Wirkung; beide Fehlerrichtungen werden gemessen, nicht erschlossen.

Gemessene Ausgangslage (2026-09-10, HEAD 4843058): Arbeitsbaum sauber; Datenbankcontainer tessera-ctl-db-1 unter 172.19.0.2 erreichbar (Adresse bei JEDEM Lauf neu ermitteln, nie abschreiben), Zugang tessera:tessera_dev, Datenbank tessera. Die Aufgabenstellung nennt als Baseline 833 Tests in 56 Dateien, eine saubere Typpruefung und 66 bestandene Pruefungen des Wegwerf-Werkzeugs; diese drei Zahlen sind zu Beginn von Aufgabe 1 SELBST zu messen und im Bericht mit dem gemessenen Wert zu nennen, nicht zu uebernehmen — eine Zahl aus zweiter Hand ist in diesem Vorhaben schon viermal geschrumpft.

</planning_time_findings>

Aufgabe 1: Die drei Regeln schreiben, lokal anwenden und an der echten Datenbank messen Der Container `tessera-ctl-db-1` laeuft und ist ueber seine zur Laufzeit ermittelte Container-Adresse mit `tessera:tessera_dev` erreichbar; der Arbeitsbaum ist sauber. apps/api/prisma/migrations/20260910120000_rls_widen_membership_grant_and_platform_read/migration.sql, apps/api/scripts/rls-scratch-check.mjs, apps/api/src/groups/migration-sql.spec.ts Diese Aufgabe ist der duenne, durchgehende Faden: sie beruehrt die Migrationsdatei, die lebende Datenbank, das Messwerkzeug und den Textabgleich und beweist damit von Ende zu Ende, dass die Regelaenderung traegt, bevor ein einziger Anwendungspfad angefasst wird. Sie aendert keinen Anwendungscode.

Die neuen und geaenderten Pruefungen des Wegwerf-Werkzeugs, jede mit dieser Kennung und jede mit dem tatsaechlich beobachteten Wert im Meldetext:

  • groupmembership-schreiben-fremder-benutzer-abgelehnt — die Umkehr der ersten loch-behauptenden Pruefung. Gemessen mit einem Benutzer, den es im anderen Mandanten TATSAECHLICH gibt (user-b), nicht mit einer erfundenen Kennung: nur so misst sie den Fall, den T-JTS-02 benannt hat, statt eines Fremdschluessel-Nichts. Der Meldetext nennt den alten Pruefungsnamen und den Befund, damit der Nachweis, dass das Loch existierte, erhalten bleibt.
  • groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich — die Gegenmessung: dasselbe Einfuegen ueber die Verwaltungsrolle mit Umgehungsrecht GELINGT. Damit steht fest, dass die Abweisung von der Regel kommt und nicht vom Aufbau. Praezedenzfall fuer diese Messform: gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit.
  • modulegrant-fremde-gruppe-abgelehnt — die Umkehr der zweiten loch-behauptenden Pruefung, mit demselben Verweis-Erfordernis.
  • modulegrant-fremder-benutzer-abgelehnt — NEU, der zweite Zweig des Entweder-oder (D-04): eine Freigabe mit eigener Mandantenkennung und fremder Benutzerkennung. T-JTS-03 hat nur den Gruppenzweig gemessen; die Regel muss beide decken, sonst bleibt die Haelfte des Lochs offen.
  • modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich — die Gegenmessung dazu. Diese Pruefung legt ZUGLEICH die Zeile grant-foreign-group an, die bisher als Nebenwirkung der loch-behauptenden Pruefung entstand und auf der die beiden Pruefungen des Bereichs module-registry aufsetzen (Befund C). Der Aufbau muss so sein, dass diese Zeile weiterhin vorhanden ist, wenn der Bereich module-registry gemessen wird.
  • tenderrssfeed-plattformzeile-gebunden-sichtbar — die Umkehr der dritten loch-behauptenden Pruefung: unter BEIDEN Mandantenkontexten ist die plattformweite Zeile jetzt sichtbar. Der Meldetext nennt den alten Pruefungsnamen, WINDOWS #19 und die beobachteten Kennungen beider Ergebnisse.
  • tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar — die andere Fehlerrichtung derselben Aenderung: der gebundene Lesezugriff liefert ZUSAETZLICH weiterhin die eigene Zeile des Mandanten und NICHT die des anderen. Ohne diese Pruefung waere eine zu weit gefasste Leseregel unbemerkt.
  • tenderrssfeed-ungebunden-nur-die-plattformzeile — die Belegzeile fuer Befund F: ohne gesetzten Mandantenkontext liefert der Lesezugriff jetzt genau die plattformweiten Zeilen und keine persoenliche. Das ist die neue Fehlerrichtung, die Aufgabe 2 traegt; sie gehoert gemessen, nicht behauptet.
  • tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt — BESTEHT BEREITS und muss bestanden bleiben. Ihr Meldetext ist an die neue, jetzt ausdrueckliche Schreibbedingung anzupassen.
  • tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt — NEU: ein gebundenes UPDATE auf die plattformweite Zeile wird abgewiesen.
  • tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt — NEU: ein gebundenes DELETE auf die plattformweite Zeile wird abgewiesen. Diese beiden sind die Messung der Richtung "zu locker" und damit der eigentliche Grund fuer die Trennung nach Befehl (Befund K).
  • searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar — NEU, mit einer eigenen Wegwerf-Tabelle und der UNVERAENDERT ausgelieferten Regel: eine mandantenlose Zeile bleibt hier bewusst unsichtbar. Der Meldetext haelt die Begruendung aus Befund E fest — es gibt keinen Codeweg, der eine solche Zeile erzeugt, die Vorgaben sind Konstanten — und benennt die Bedingung, unter der diese Entscheidung neu zu bewerten waere.

Die Extraktion der abgeloesten Regeln muss auf die NEUE Migrationsdatei zeigen (Befund D). Findet sie eine benoetigte Regel nicht, meldet der Abschnitt eine FEHLGESCHLAGENE Pruefung und bricht ab, statt mit einer geratenen Regel weiterzumessen — die Form, die die bestehenden Abschnitte bereits vormachen. Der Meldetext der Pruefung gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus im Bereich module-registry behauptet heute, die Regel lasse die fremde Zeile durch; das ist nach dieser Aufgabe unwahr und im selben Zug richtigzustellen. Zuerst die Ausgangslage SELBST messen und die drei Zahlen notieren (Testlauf, Typpruefung, Wegwerf-Werkzeug). Erst danach anfangen.

Dann die drei ausgelieferten Migrationen und die vier Modelle im Schema erneut lesen — die Angaben aus <planning_time_findings> leiten nur die Suche.

Eine EINZIGE neue Migration anlegen, Verzeichnisname mit dem Zeitstempel 20260910120000 und dem Namensteil rls_widen_membership_grant_and_platform_read; der Zeitstempel muss hinter dem juengsten vorhandenen liegen. Die bestehenden Migrationsdateien bleiben UNVERAENDERT — Prisma fuehrt ihre Pruefsummen, eine Aenderung braechte den Migrationslauf zum Abbruch; der Praezedenzfall dafuer und die Form des erklaerenden Kopfes stehen im Kopf von 20260909140000. Der Kopf der neuen Datei nennt: welche Regeln sie abloest und warum, dass die alten Dateien deshalb stehen bleiben, dass der Schalter weiterhin aus ist und die Regeln damit heute wirkungslos sind, sowie die Begruendung aus Befund E dafuer, dass SearchProvider ausdruecklich NICHT angefasst wird.

Inhalt der Migration, in dieser Reihenfolge:

(1) GroupMembership: die bestehende Regel entfernen und unter demselben Namen neu anlegen, mit einer zweiten Bedingung fuer die Benutzerseite nach dem Join-Muster, das PasswordResetToken in 20260618112133 vormacht — die Benutzerkennung muss unter den Benutzern des laufenden Mandanten liegen, ebenso wie die Gruppenkennung unter dessen Gruppen. Beide Bedingungen mit UND verknuepft.

(2) ModuleGrant: die bestehende Regel entfernen und unter demselben Namen neu anlegen. Die eigene Mandantenkennung wird wie bisher verglichen; zusaetzlich muss die Gruppenkennung entweder leer sein oder unter den Gruppen des laufenden Mandanten liegen, und die Benutzerkennung entweder leer sein oder unter dessen Benutzern. Die Leer-Zulassung ist zwingend, weil das Modell Gruppe und Benutzer als Entweder-oder fuehrt (D-04).

(3) TenderRssFeedSource: die bestehende Regel entfernen und durch VIER nach Befehl getrennte Regeln ersetzen (Befund K), mit den Namen tenant_platform_read_policy, tenant_insert_policy, tenant_update_policy und tenant_delete_policy. Die Leseregel gilt fuer SELECT und laesst zusaetzlich zur Gleichheit mit dem laufenden Mandanten die Zeilen ohne Mandantenkennung zu. Die drei Schreibregeln verlangen ausnahmslos Gleichheit mit dem laufenden Mandanten — die Einfuegeregel als Schreibbedingung, die Aenderungsregel in beiden Klauseln, die Loeschregel als Lesebedingung ihres Befehls.

(4) SearchProvider: keine Anweisung, nur der erklaerende Absatz im Kopf.

Danach die Migration LOKAL ANWENDEN — ohne diesen Schritt ist nichts gemessen, und Bau wie Typpruefung liefen auch ohne ihn gruen. Die Container-Adresse frisch ermitteln, nicht abschreiben. Anwenden ausschliesslich mit dem Befehl, der ausstehende Migrationen anwendet, NIEMALS mit der Entwicklungsvariante und niemals mit dem Ruecksetzbefehl — diese Datenbank traegt den lokalen Bestand. Danach den Status abfragen und die Regelliste der lebenden Datenbank aus dem Systemkatalog auslesen; diese Liste ist der Beleg, dass die Regel wirklich angekommen ist.

Zusaetzlich die Bestandszaehlung aus Befund H erneut ausfuehren (Mitgliedschaften und Freigaben, die ueber Mandantengrenzen zeigen) und das Ergebnis fuer den Bericht festhalten. Ist es nicht null, HALTEN und melden statt weitermachen — solche Zeilen waeren nach dem Scharfschalten weder sichtbar noch loeschbar.

Dann das Wegwerf-Werkzeug wie unter <behavior> beschrieben umbauen. Die Extraktion auf die neue Datei umleiten, eine Extraktionsform ergaenzen, die mehrere Regeln je Tabelle liefern kann, die drei loch-behauptenden Pruefungen UMKEHREN statt zu entfernen oder zu lockern, die Gegenmessungen und die vier Befehlsrichtungen ergaenzen, und die Bereitstellung von grant-foreign-group so umbauen, dass die beiden aufsetzenden Pruefungen weiterhin messen, was sie behaupten.

Zuletzt einen neuen Beschreibungsblock in apps/api/src/groups/migration-sql.spec.ts fuer die neue Migration, im Stil der beiden bestehenden Bloecke: reiner Textabgleich ohne Datenbank. Er prueft, dass die neue Datei die abgeloesten Regeln entfernt und neu anlegt, dass die Benutzerseite in beiden neuen Bedingungen vorkommt, dass die Tabelle mit nullbarer Mandantenkennung genau vier nach Befehl getrennte Regeln bekommt, und dass ausschliesslich die Leseregel die Zeilen ohne Mandant zulaesst. DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && DATABASE_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/tessera" npx prisma migrate status --schema apps/api/prisma/schema.prisma | tee /tmp/jab-migrate-status.txt && grep -qiE 'up to date|schema is up to date|keine ausstehenden' /tmp/jab-migrate-status.txt && POL=$(docker exec tessera-ctl-db-1 psql -U tessera -d tessera -At -c "SELECT tablename||'#'||policyname||'#'||coalesce(cmd,'')||'#'||coalesce(qual,'')||'#'||coalesce(with_check,'') FROM pg_policies WHERE tablename IN ('GroupMembership','ModuleGrant','TenderRssFeedSource','SearchProvider') ORDER BY tablename, policyname") && echo "$POL" && echo "$POL" | grep '^GroupMembership#' | grep -q '"User"' && echo "$POL" | grep '^ModuleGrant#' | grep -q '"User"' && echo "$POL" | grep '^ModuleGrant#' | grep -q '"Group"' && test 4 -eq "$(echo "$POL" | grep -c '^TenderRssFeedSource#')" && test 1 -eq "$(echo "$POL" | grep -c '^SearchProvider#')" && echo "$POL" | grep '^TenderRssFeedSource#' | grep '#SELECT#' | grep -q 'IS NULL' && test 0 -eq "$(echo "$POL" | grep '^TenderRssFeedSource#' | grep -vE '#SELECT#' | grep -c 'IS NULL')" && test 0 -eq "$(echo "$POL" | grep '^SearchProvider#' | grep -c 'IS NULL')" && OUT=$(TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs) && echo "$OUT" && for K in groupmembership-schreiben-fremder-benutzer-abgelehnt groupmembership-fremder-benutzer-ueber-die-wartungsrolle-weiterhin-moeglich modulegrant-fremde-gruppe-abgelehnt modulegrant-fremder-benutzer-abgelehnt modulegrant-fremde-gruppe-ueber-die-wartungsrolle-weiterhin-moeglich tenderrssfeed-plattformzeile-gebunden-sichtbar tenderrssfeed-eigene-zeile-gebunden-weiterhin-sichtbar tenderrssfeed-ungebunden-nur-die-plattformzeile tenderrssfeed-gebundenes-einfuegen-ohne-mandant-abgelehnt tenderrssfeed-gebundenes-aendern-der-plattformzeile-abgelehnt tenderrssfeed-gebundenes-loeschen-der-plattformzeile-abgelehnt searchprovider-mandantenlose-zeile-bleibt-unter-jedem-kontext-unsichtbar gruppenpfad-gebunden-schliesst-die-fremde-gruppe-aus gruppenpfad-ueber-die-wartungsrolle-liefert-die-fremde-gruppe-mit; do echo "$OUT" | grep -q "^$K: bestanden" || { echo "FEHLENDE ODER FEHLGESCHLAGENE PRUEFUNG: $K"; exit 1; }; done && echo "$OUT" | grep -qE '^Alle [0-9]+ Pruefungen bestanden.$' && for A in T-JTS-02 T-JTS-03; do echo "$OUT" | grep -q "$A" || { echo "VERWEIS AUF DEN ALTEN BEFUND FEHLT IM MELDETEXT: $A"; exit 1; }; done && ls -d apps/api/prisma/migrations/*_rls_widen_membership_grant_and_platform_read >/dev/null && npm --prefix apps/api run test -- src/groups/migration-sql.spec.ts src/prisma/rls-coverage.spec.ts && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma/schema.prisma apps/api/src/tenders apps/api/src/module-registry apps/api/src/dashboard apps/api/src/user apps/api/src/ldap apps/api/src/auth docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)" Eine neue Migration mit dem Namensteil rls_widen_membership_grant_and_platform_read existiert, ist LOKAL angewandt und der Migrationsstatus meldet keinen Rueckstand; die Regelliste der lebenden Datenbank zeigt fuer die Mitgliedschaftstabelle und die Freigabetabelle eine Bedingung, die die Benutzertabelle nennt, fuer die Freigabetabelle zusaetzlich die Gruppentabelle, fuer die RSS-Quellentabelle genau vier nach Befehl getrennte Regeln, von denen ausschliesslich die Leseregel die Zeilen ohne Mandantenkennung zulaesst, und fuer die Suchanbietertabelle unveraendert genau eine strenge Regel; die Bestandszaehlung ueber Mandantengrenzen zeigende Zeilen ist ausgefuehrt und ihr Ergebnis im Bericht genannt; das Wegwerf-Werkzeug meldet alle Pruefungen bestanden mit Rueckgabewert 0, die zwoelf neuen bzw. umgekehrten Kennungen stehen einzeln als bestanden in der Ausgabe, und die Meldetexte nennen beide alten Befundkennungen, damit der Nachweis ihrer Existenz erhalten bleibt; die beiden aufsetzenden Pruefungen des Bereichs module-registry stehen weiterhin auf bestanden, ihre Grundlage wird ueber die Wartungsrolle bereitgestellt und die Meldetexte behaupten nicht mehr, die Regel lasse die fremde Zeile durch; der neue Beschreibungsblock in der Migrations-Textpruefung ist vorhanden und gruen; der Gesamttestlauf ist gruen und die Typpruefung sauber; Schema, Anwendungscode und die Compose- und Beispiel-Umgebungsdateien sind unveraendert.

Aufgabe 2: Der eine Anwendungspfad, den die Reparatur still falsch machen wuerde — und die vier Aufzeichnungen im Quelltext apps/api/src/tenders/tender-rss-feed.service.ts, apps/api/src/tenders/tender-rss-feed.service.spec.ts, apps/api/src/tenders/tenders.controller.ts, apps/api/src/tenders/tenders.controller.spec.ts, apps/api/src/groups/groups.service.ts, apps/api/src/groups/module-grants.service.ts, apps/api/src/module-registry/module-access.service.ts, apps/api/src/prisma/rls-coverage.spec.ts Die Testerwartungen VOR der Umstellung, damit ein vergessener Bindungsaufruf rot wird statt aus einem anderen Grund zu scheitern:
  • Die Feed-Auflistung erzeugt genau EINEN gebundenen Klienten mit der Mandantenkennung aus dem Aufrufzusammenhang und fuehrt die Abfrage ueber ihn aus — nachgewiesen mit dem im Vorhaben etablierten Zwei-Klienten-Nachweis (zwei unterscheidbare Klienten, der ungebundene darf die Abfrage nicht sehen), nicht mit einer Identitaets-Attrappe. Eine Attrappe, die den Helfer als Identitaet abbildet, wuerde die Umstellung in KEINER Richtung bemerken; dieser Fehler ist in diesem Vorhaben bereits dreimal aufgetreten.
  • Der aufrufende Endpunkt reicht die Mandantenkennung aus dem Sitzungsnachweis durch — nachgewiesen an der Aufrufform, nicht an der Antwort.
  • Die Abbildung der Antwort bleibt unveraendert: plattformweite Zeilen werden weiterhin als solche gekennzeichnet und die Besitzerkennung weiterhin entfernt. Der bestehende Fall dazu bleibt inhaltlich erhalten.
  • Die beiden Pfade, die eine plattformweite Zeile anlegen bzw. entfernen, bleiben UNGEBUNDEN. Ein Fall haelt das fest, damit ein spaeterer Leser sie nicht "der Vollstaendigkeit halber" mitbindet — gebunden koennte niemand mehr eine plattformweite Quelle anlegen oder entfernen.
  • Die Mandanten-Gegenpruefung vor jedem Erteilen einer Modulfreigabe bleibt bestehen. Ein Fall haelt fest, dass ein Erteilen auf ein fremdes Ziel weiterhin im Anwendungscode scheitert — nicht erst in der Datenbank, die heute ohnehin nichts durchsetzt. Existiert ein solcher Fall bereits, wird er um einen Kommentar ergaenzt, der sagt, warum er nach dieser Regelaenderung NICHT entbehrlich geworden ist. Zuerst die Ausgangslage erneut messen (Testzahl, Typpruefung, Wegwerf-Werkzeug), dann die Testerwartungen aus <behavior> schreiben und rot sehen, dann umstellen.

Die Auflistung der Feeds nimmt statt der blossen Benutzerkennung einen Aufrufzusammenhang mit Benutzer- UND Mandantenkennung entgegen und fuehrt ihre Abfrage ueber einen gebundenen Klienten aus, benannt wie in den bereits umgestellten Bereichen. Der bestehende Filter auf eigene und plattformweite Zeilen bleibt stehen — er ist nach der Bindung nicht ueberfluessig, sondern das zweite Netz. Der aufrufende Endpunkt reicht die Mandantenkennung aus dem Sitzungsnachweis durch; er hat sie bereits zur Hand.

Die Begruendung fuer diese Bindung gehoert in den Kopfkommentar der Methode, und zwar als das, was sie ist: die Regelaenderung dreht die Fehlerrichtung dieses Pfades um. Ungebunden lieferte er nach dem Scharfschalten frueher gar nichts und liefert jetzt die plattformweiten Zeilen — eine kurze, glaubhafte Liste statt einer leeren. Der Kommentar nennt die neue Migration und die Pruefung, die das belegt.

Die beiden Kopfkommentare der Pfade, die eine plattformweite Zeile anlegen bzw. entfernen, werden an der neuen Regel richtiggestellt: sie bleiben ungebunden, aber aus dem jetzt ausdruecklichen Grund (die Schreibregeln verlangen einen Mandanten), und sie halten fest, dass beide Faelle unter der Anwendungsrolle ueberhaupt nicht mehr durchgehen — vor wie nach dieser Aenderung — und deshalb in Etappe 4 einen Verwaltungsweg brauchen. Ein Verweis auf den dafuer in Aufgabe 3 angelegten Ledger-Eintrag gehoert dazu.

Die vier Aufzeichnungen im Quelltext, die die alten Regeln beschreiben, werden an der Messung aus Aufgabe 1 richtiggestellt: der Kopfkommentar zur Zugriffsaufloesung im Modulbereich, der Kommentar an der Benutzerpruefung in der Gruppenverwaltung, der Kommentar an der Mandanten-Gegenpruefung bei den Modulfreigaben und die Beschreibungszeile fuer die Mitgliedschaftstabelle in der Ausnahmeliste des Abdeckungstests, die die Absicherung heute nur ueber die Gruppenseite beschreibt und jetzt beide Seiten nennen muss. Jede dieser vier Stellen nennt die neue Migration.

Die Mandanten-Gegenpruefung selbst wird NICHT entfernt und nicht abgeschwaecht. Ihr Kommentar sagt kuenftig, dass die Datenbank die Grenze inzwischen ebenfalls zieht, dass diese zweite Ziehung aber erst nach dem Scharfschalten wirkt und die Pruefung im Anwendungscode bis dahin der einzige und danach der erste Schutz bleibt.

Zum Abschluss den Falsifizierungsnachweis: den Bindungsaufruf der Feed-Auflistung versuchsweise zuruecknehmen, den roten Testnamen samt Fehlermeldung notieren, die Ruecknahme rueckgaengig machen. Ohne diesen Nachweis ist nicht belegt, dass der neue Test die Umstellung ueberhaupt bemerken wuerde. Er gehoert in den Bericht. DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs | grep -qE '^Alle [0-9]+ Pruefungen bestanden.$' && grep -q "from '../prisma/prisma-tenant.extension'" apps/api/src/tenders/tender-rss-feed.service.ts && B=$(grep -c 'tenantPrisma.tenderRssFeedSource.' apps/api/src/tenders/tender-rss-feed.service.ts) && { test "$B" -ge 3 || { echo "BINDUNG: nur $B gebundene Zugriffe in tender-rss-feed.service.ts, erwartet mindestens 3 (zwei bestehende plus die Auflistung)"; exit 1; }; } && U=$(grep -c 'this.prisma.tenderRssFeedSource.' apps/api/src/tenders/tender-rss-feed.service.ts) && { test "$U" -eq 2 || { echo "UNGEBUNDEN: $U ungebundene Zugriffe in tender-rss-feed.service.ts, erwartet genau 2 (Anlegen und Entfernen plattformweiter Zeilen)"; exit 1; }; } && A=$(grep -c 'assertTargetBelongsToTenant' apps/api/src/groups/module-grants.service.ts) && { test "$A" -ge 3 || { echo "GEGENPRUEFUNG: nur $A Vorkommen von assertTargetBelongsToTenant, die Pruefung wurde entfernt oder ihre Aufrufe reduziert"; exit 1; }; } && MIG=$(basename $(ls -d apps/api/prisma/migrations/*_rls_widen_membership_grant_and_platform_read)) && for F in apps/api/src/tenders/tender-rss-feed.service.ts apps/api/src/groups/groups.service.ts apps/api/src/groups/module-grants.service.ts apps/api/src/module-registry/module-access.service.ts apps/api/src/prisma/rls-coverage.spec.ts; do grep -q "$MIG" "$F" || { echo "AUFZEICHNUNG NENNT DIE NEUE MIGRATION NICHT: $F"; exit 1; }; done && grep -q 'tenantId' apps/api/src/tenders/tenders.controller.ts && npm --prefix apps/api run test -- src/tenders/tender-rss-feed.service.spec.ts src/tenders/tenders.controller.spec.ts src/groups/module-grants.service.spec.ts src/prisma/rls-coverage.spec.ts && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma apps/api/src/dashboard apps/api/src/user apps/api/src/ldap apps/api/src/auth apps/api/src/dkv docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)" Die Feed-Auflistung laeuft ueber einen gebundenen Klienten, nimmt die Mandantenkennung aus dem Aufrufzusammenhang entgegen und wird vom aufrufenden Endpunkt entsprechend bedient; die beiden Pfade fuer plattformweite Zeilen bleiben ungebunden und tragen die richtiggestellte Begruendung samt Verweis auf den in Aufgabe 3 angelegten offenen Eintrag; der Zwei-Klienten-Nachweis liegt in der Testdatei des Dienstes vor, keine Identitaets-Attrappe; die Mandanten-Gegenpruefung bei den Modulfreigaben steht unveraendert und ist durch einen Fall gehalten, der beim Rueckbau rot wird; alle fuenf Aufzeichnungen im Quelltext nennen die neue Migration und beschreiben die neue Regel statt der alten; der Falsifizierungsnachweis ist durchgefuehrt, zurueckgenommen und im Bericht mit Testnamen und Fehlermeldung festgehalten; der Gesamttestlauf ist gruen mit mindestens der zu Beginn dieser Aufgabe gemessenen Testzahl, die Typpruefung sauber, das Wegwerf-Werkzeug weiterhin vollstaendig bestanden; Schema, Migrationen und die Bereiche dashboard, user, ldap, auth, dkv sowie die Compose- und Beispiel-Umgebungsdateien sind unveraendert.

Aufgabe 3: Den Aktenstand kohaerent machen — Ledger, Klassifikation, Kritikschrift, Betriebsanleitung .planning/WINDOWS.md, docs/mandantentrennung-zugriffsklassifikation.md, docs/mandantentrennung-etappe2-fehlerrichtung.md, docs/mandantentrennung-datenbankrolle.md Der Ledger. WINDOWS #19 wird auf `fixed` gesetzt, in der Markdown-Tabelle UND im JSON-Block, mit Aufloesungszeitpunkt und einem Beleg, der die neue Migration namentlich nennt und die Pruefungen benennt, die beide Fehlerrichtungen des Lese-/Schreibsplits messen. Der Eintrag haelt zusaetzlich fest, dass seine Praemisse fuer die Suchanbietertabelle WIDERLEGT wurde (Befund E) und diese Haelfte deshalb nicht geloest, sondern richtiggestellt ist. #18, #20, #21 und #23 bleiben unveraendert offen.

Ein NEUER offener Eintrag kommt hinzu, ebenfalls in Tabelle und JSON-Block: unter der Anwendungsrolle laesst sich eine plattformweite RSS-Quelle weder anlegen noch entfernen — in der alten wie in der neuen Regel, weil Schreibzugriffe ausdruecklich einen Mandanten verlangen. Der Eintrag benennt die beiden betroffenen Pfade, sagt, dass dies KEINE Folge dieser Reparatur ist, und benennt die konkrete Vorabpruefung fuer Etappe 4. Er ist der Grund, warum die Schliessung von #19 nichts verschwinden laesst.

Die Zaehler im Kopf der Datei werden aus dem JSON-Block ABGELEITET, nicht weitergezaehlt — vier von vier Kopfzahlen dieses Vorhabens sind bei genauerem Hinsehen schon einmal geschrumpft.

Die Klassifikation. Der Block zu #19 wird von einer offenen Frage zu einer beantworteten: er nennt die neue Migration, die getroffene Semantik (Lesen schliesst die plattformweiten Zeilen ein, Schreiben verlangt einen Mandanten, getrennt nach Befehl weil ein Lesebedingung allein auch Aendern und Entfernen regelt) und die Widerlegung fuer die Suchanbietertabelle. Der Punkt in "Was diese Etappe NICHT entscheidet", der genau diese Regelform als offen fuehrt, wird entsprechend aufgeloest statt stehengelassen.

In der Bestandsaufnahme werden die vier betroffenen Zeilen nachgezogen: die Zeile zur Suchanbietertabelle (die Begruendung bleibt richtig, bekommt aber die Messung und den Verweis), die beiden Zeilen, die die einseitigen Regeln als Grund fuer eine zusaetzliche Anwendungspruefung nennen, und die Zeile zu den RSS-Quellen, deren Stand-Begruendung jetzt einen gebundenen Pfad mehr und zwei ungebundene mit neuer Begruendung nennt. Uebersichtszeile und Summenzeile werden aus dem Quelltext neu abgeleitet, nicht aus diesem Plan uebernommen.

Die Kritikschrift bekommt einen neuen Abschnitt ## Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19, gesetzt nach dem Abschnitt zum Bereich module-registry und vor den Verweis am Ende, mit den fuenf im Dokument ueblichen Unterabschnitten unter den Kennungen (r1) bis (r5): die TATSAECHLICH beobachtete Ausgabe des Werkzeuglaufs und der Regelliste aus der lebenden Datenbank; eine Signaltabelle, die fuer jede der drei Regeln BEIDE Fehlerrichtungen fuehrt (zu streng und zu locker) samt dem Signal, an dem man sie erkennen wuerde; die namentliche Liste der Stellen, an denen Leere weiterhin als Abwesenheit gedeutet wird, einschliesslich der neuen Stelle aus Befund F und der Begruendung, warum sie gebunden wurde; was dieser Durchlauf bewusst nicht loest (der Verwaltungsweg fuer plattformweite Zeilen, die fehlende Benutzerdimension der Ausschreibungsregeln, die plattformweite Eindeutigkeit von Anmeldename und Adresse); und was er bewusst nicht anfasst.

Zur fehlenden Benutzerdimension gehoert eine eigene Messung statt einer Uebernahme: es ist selbst nachzusehen, ob es irgendwo eine zweite Sitzungsvariable fuer den Benutzer gibt, und das Ergebnis mit der ausgefuehrten Anweisung festzuhalten. Gebaut wird sie hier nicht.

Ausserdem werden die vier ueberholten Bestandsstellen der Kritikschrift mit einem Nachtrag versehen — die Form dafuer macht der Abschnitt zur Fortschreibung des ldap-Abschnitts bereits vor: der Punkt, der beide Regeln als einseitig beschreibt; der Punkt, der #19 als nicht geloest fuehrt; die Zeile der Signaltabelle des Bereichs tenders zu den drei RSS-Pfaden; und die beiden aufgezeichneten Werkzeugausgaben, die die alten Meldetexte woertlich zitieren. Die zitierten alten Ausgaben werden NICHT umgeschrieben — sie sind ein Messprotokoll und bleiben, was sie waren; der Nachtrag steht daneben und sagt, seit wann und wodurch die Aussage ueberholt ist. Ebenso die eine Stelle in der Betriebsanleitung zur Datenbankrolle, die #19 als offen fuehrt. DB_IP=$(docker inspect tessera-ctl-db-1 --format '{{range $k,$v := .NetworkSettings.Networks}}{{$v.IPAddress}}{{end}}') && TESSERA_SCRATCH_ADMIN_URL="postgresql://tessera:tessera_dev@${DB_IP}:5432/postgres" node apps/api/scripts/rls-scratch-check.mjs | grep -qE '^Alle [0-9]+ Pruefungen bestanden.$' && MIG=$(basename $(ls -d apps/api/prisma/migrations/_rls_widen_membership_grant_and_platform_read)) && python3 - "$MIG" <<'PY' import json, re, sys mig = sys.argv[1] t = open('.planning/WINDOWS.md', encoding='utf-8').read() m = re.search(r'{3,}json\s*\n(\[.*?\])\s*\n{3,}', t, re.S) if not m: sys.exit('JSON-Block in WINDOWS.md nicht gefunden') rows = json.loads(m.group(1)) by_id = {r['id']: r for r in rows} if by_id[19]['status'] != 'fixed': sys.exit('WINDOWS #19 steht nicht auf fixed') if not by_id[19].get('resolved_at'): sys.exit('WINDOWS #19 hat keinen Aufloesungszeitpunkt') for i in (18, 20, 21, 23): if by_id[i]['status'] != 'open': sys.exit(f'WINDOWS #{i} ist nicht mehr offen') new = [r for r in rows if r['id'] > 23] if len(new) != 1: sys.exit(f'genau ein neuer Eintrag erwartet, gefunden: {len(new)}') if new[0]['status'] != 'open': sys.exit('der neue Eintrag ist nicht offen') open_n = sum(1 for r in rows if r['status'] == 'open') fixed_n = sum(1 for r in rows if r['status'] == 'fixed') waived_n = sum(1 for r in rows if r['status'] == 'waived') head = t.split('---')[1] for key, want in (('open_count', open_n), ('fixed_count', fixed_n), ('waived_count', waived_n), ('total_count', len(rows))): got = re.search(rf'^{key}:\s(\d+)\s*$', head, re.M) if not got or int(got.group(1)) != want: sys.exit(f'{key} im Kopf stimmt nicht mit dem JSON-Block: {got and got.group(1)} statt {want}') for r in rows: line = re.search(rf'^|\s*{r["id"]}\s*|.$', t, re.M) if not line: sys.exit(f'Eintrag {r["id"]} fehlt in der Markdown-Tabelle') if f'| {r["status"]} |' not in line.group(0): sys.exit(f'Status von Eintrag {r["id"]} weicht zwischen Tabelle und JSON-Block ab') if mig not in (by_id[19].get('reason') or '') + (by_id[19].get('description') or ''): sys.exit('der Beleg zu #19 nennt die neue Migration nicht') print(f'WINDOWS.md kohaerent: {open_n} offen, {fixed_n} behoben, {waived_n} zurueckgestellt, {len(rows)} gesamt') PY && grep -q '^## Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19$' docs/mandantentrennung-etappe2-fehlerrichtung.md && for S in r1 r2 r3 r4 r5; do grep -qE "^### ($S) " docs/mandantentrennung-etappe2-fehlerrichtung.md || { echo "FEHLENDER UNTERABSCHNITT: ($S)"; exit 1; }; done && for F in docs/mandantentrennung-etappe2-fehlerrichtung.md docs/mandantentrennung-zugriffsklassifikation.md docs/mandantentrennung-datenbankrolle.md .planning/WINDOWS.md; do grep -q "$MIG" "$F" || { echo "DOKUMENT NENNT DIE NEUE MIGRATION NICHT: $F"; exit 1; }; done && U=$(grep -ro "this.prisma.[a-zA-Z]" apps/api/src/tenders | grep -v spec | wc -l | tr -d ' ') && B=$(grep -ro "tenantPrisma.[a-zA-Z]*." apps/api/src/tenders | grep -v spec | wc -l | tr -d ' ') && { grep -qE "^| tenders | ${U} | ${B} |" docs/mandantentrennung-zugriffsklassifikation.md || { echo "UEBERSICHTSZEILE tenders nennt nicht die neu gemessenen Zahlen ${U}/${B}"; exit 1; }; } && awk -F'|' '$2 ~ /^ [a-z][a-z-] *$/ && $3 ~ /^ *[0-9]+ *$/ && $4 ~ /^ *[0-9]+ *$/ { su+=$3; sb+=$4; n++ } $2 ~ /^ ***Summe** *$/ && $4 !~ /^ *$/ { g3=$3; g4=$4; gsub(/[^0-9]/,"",g3); gsub(/[^0-9]/,"",g4); ru=g3+0; rb=g4+0; seen=1 } END { if (!seen || n+0 != 12) { print "UEBERSICHTSTABELLE nicht erkannt, Bereichszeilen: " n; exit 1 } if (su+0 != ru || sb+0 != rb) { print "SUMMENZEILE stimmt nicht: Bereichszeilen ergeben " su "/" sb ", Summenzeile nennt " ru "/" rb; exit 1 } }' docs/mandantentrennung-zugriffsklassifikation.md && npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts && npm --prefix apps/api run test && npm --prefix apps/api run type-check && test -z "$(git diff --name-only HEAD -- apps/api/prisma/schema.prisma docker-compose.yml docker-compose.dev.yml docker-compose.prod.yml docker-compose.ci.yml .env.example .env.prod.example)" WINDOWS #19 steht in Tabelle UND JSON-Block auf behoben, mit Zeitpunkt, mit dem Namen der neuen Migration im Beleg und mit der ausdruecklichen Feststellung, dass die Haelfte zur Suchanbietertabelle als widerlegte Praemisse und nicht als geloestes Problem schliesst; #18, #20, #21 und #23 sind unveraendert offen; genau ein neuer offener Eintrag beschreibt den fehlenden Verwaltungsweg fuer plattformweite Zeilen samt Vorabpruefung fuer Etappe 4; die vier Kopfzahlen stimmen mit dem JSON-Block ueberein und sind daraus abgeleitet; die Klassifikation fuehrt den #19-Block als beantwortet, hat den zugehoerigen Punkt in "Was diese Etappe NICHT entscheidet" aufgeloest, die vier Bestandsaufnahme-Zeilen nachgezogen und Uebersichts- wie Summenzeile aus dem Quelltext neu abgeleitet; die Kritikschrift traegt den neuen Abschnitt mit allen fuenf Unterabschnitten, der tatsaechlich beobachteten Ausgabe, einer Signaltabelle mit BEIDEN Fehlerrichtungen je Regel, der selbst ausgefuehrten Messung zur fehlenden Benutzerdimension und Nachtraegen an den vier ueberholten Bestandsstellen, ohne die alten Messprotokolle umzuschreiben; die Betriebsanleitung nennt #19 nicht mehr als offen; die Inventarpruefung, der Gesamttestlauf und die Typpruefung sind gruen; Schema und die Compose- und Beispiel-Umgebungsdateien sind unveraendert.

<threat_model>

Trust Boundaries

Boundary Description
Mandant A → Datenzeilen des Mandanten B Die Grenze, die dieser Plan an zwei Stellen von einseitig auf beidseitig zieht
Mandant → plattformweite Zeile (leere Mandantenkennung) Neue, ausdrueckliche Grenze: lesen ja, schreiben nein — und sie muss nach BEIDEN Seiten stimmen
Anwendungscode → Datenbankregel Die Regel ist ein ZWEITES Netz. Wird der Anwendungscode im Vertrauen darauf abgebaut, faellt der einzige heute wirksame Schutz weg (Schalter ist aus)
Aufzeichnung → spaeterer Leser Eine Aufzeichnung, die ein geschlossenes Loch als offen fuehrt oder eine ueberholte Handlungsanweisung gibt, ist ein Angriffsweg auf den naechsten Durchlauf

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-JAB-01 Elevation of Privilege Regel auf GroupMembership high mitigate Die Regel prueft nach Aufgabe 1 beide Seiten der Beziehung; gemessen mit einem Benutzer, den es im fremden Mandanten TATSAECHLICH gibt, samt Wartungsrollen-Gegenmessung, die belegt, dass die Abweisung von der Regel kommt
T-JAB-02 Elevation of Privilege Regel auf ModuleGrant, referenzierte Gruppe high mitigate Die Regel prueft zusaetzlich die referenzierte Gruppe; die anwendungsseitige Gegenpruefung bleibt bestehen und ist durch einen Fall gehalten, der beim Rueckbau rot wird
T-JAB-03 Elevation of Privilege Regel auf ModuleGrant, referenzierter Benutzer high mitigate Der zweite Zweig des Entweder-oder wird ausdruecklich mitgeprueft — T-JTS-03 hatte nur den Gruppenzweig gemessen, eine Reparatur nur dieses Zweigs liesse die Haelfte des Lochs offen
T-JAB-04 Elevation of Privilege Lese-/Schreibsplit zu locker gefasst high mitigate Vier nach Befehl getrennte Regeln statt einer permissiven; drei Pruefungen messen, dass Einfuegen, Aendern und Entfernen einer plattformweiten Zeile unter gebundenem Kontext abgewiesen werden, und die Regelliste der lebenden Datenbank belegt, dass ausschliesslich die Leseregel die Zeilen ohne Mandant zulaesst
T-JAB-05 Denial of Service Lese-/Schreibsplit zu streng gefasst high mitigate Zwei Pruefungen messen die Gegenrichtung: die plattformweite Zeile ist unter beiden Mandantenkontexten sichtbar UND die eigene Zeile des Mandanten bleibt es ebenfalls. Ohne die zweite waere eine Leseregel, die nur noch die plattformweiten Zeilen liefert, unbemerkt
T-JAB-06 Denial of Service Bestandszeilen, die die schaerfere Regel nicht mehr sieht medium mitigate Aufgabe 1 zaehlt vor der Anwendung, ob es Mitgliedschaften oder Freigaben ueber Mandantengrenzen gibt, und HAELT bei einem Ergebnis ungleich null; dieselbe Zaehlung wird als Vorabpruefung an Etappe 4 uebergeben, weil das Testsystem hier nicht gemessen ist
T-JAB-07 Information Disclosure Suchanbietertabelle faelschlich gelockert medium mitigate Die Tabelle behaelt ihre strenge Regel; die Praemisse von #19 ist fuer sie gemessen widerlegt. Eine Lockerung wuerde eine kuenftige mandantenlose Zeile jedem Mandanten zeigen — genau die falsche Richtung
T-JAB-08 Tampering Abbau der anwendungsseitigen Gegenpruefung im Vertrauen auf die neue Regel high mitigate Die Gegenpruefung bleibt unveraendert, ihr Kommentar sagt ausdruecklich, dass die zweite Ziehung erst nach dem Scharfschalten wirkt, und eine Zaehlung ihrer Vorkommen ist Teil der Abnahme von Aufgabe 2
T-JAB-09 Repudiation Der Nachweis, dass die Loecher existierten, geht verloren medium mitigate Die drei Pruefungen werden UMGEKEHRT statt geloescht; jede traegt im Meldetext den alten Pruefungsnamen und die Befundkennung, und die Abnahme prueft, dass beide Kennungen in der Ausgabe des Werkzeugs vorkommen
T-JAB-10 Spoofing Eine Pruefung besteht aus dem falschen Grund, weil ihre Grundlage weggefallen ist high mitigate Die Zeile, auf der zwei spaetere Pruefungen aufsetzen, wird ueber die Wartungsrolle bereitgestellt; die Abnahme fordert beide aufsetzenden Pruefungen namentlich als bestanden, und die Gegenmessung ueber die Wartungsrolle wuerde scheitern, wenn die Zeile fehlte
T-JAB-11 Tampering Die Regelaenderung wird geschrieben, aber nie angewandt high mitigate Blockierender Schritt in Aufgabe 1: Migrationsstatus ohne Rueckstand UND Auslesen der Regelliste aus dem Systemkatalog der laufenden Datenbank. Bau und Typpruefung liefen auch ohne die Anwendung gruen
T-JAB-12 Information Disclosure Zugangsdaten im versionierten Text low accept Die lokalen Entwicklungszugangsdaten stehen bereits als Vorgabewert in der Compose-Datei; es entsteht kein neues Geheimnis. Die Migration selbst vergibt kein Kennwort — derselbe Vorsatz wie in 20260909130000
T-JAB-13 Denial of Service Der Verwaltungsweg fuer plattformweite Zeilen fehlt nach dem Scharfschalten medium transfer Nicht durch diesen Plan geloest, weil die vorgegebene Semantik Schreibzugriffe an einen Mandanten bindet. Als eigener offener Ledger-Eintrag mit Vorabpruefung an Etappe 4 uebergeben, damit er nicht mit #19 verschwindet
T-JAB-14 Repudiation Handgepflegte Dokumentstellen werden still uebersprungen high mitigate Vollstaendige Fundstellenliste in Befund J, abgeleitete Pruefungen statt fest verdrahteter Zahlen fuer Uebersichts- und Summenzeile, und eine Pruefung, die fordert, dass jede der betroffenen Dateien die neue Migration namentlich nennt
T-JAB-15 Tampering npm/pip/cargo-Installationen low accept Dieser Plan installiert kein Paket; package.json und die Sperrdatei werden nicht angefasst. Das Paket-Legitimitaetsgatter faellt damit nicht an
</threat_model>
  1. Der Migrationsstatus der lokalen Datenbank meldet keinen Rueckstand, und die Regelliste aus dem Systemkatalog zeigt die neuen Regeln so, wie die Migration sie beschreibt — nicht nur die Datei auf der Platte.
  2. node apps/api/scripts/rls-scratch-check.mjs meldet alle Pruefungen bestanden, einschliesslich der zwoelf namentlich geforderten neuen bzw. umgekehrten und der beiden aufsetzenden Pruefungen des Bereichs module-registry.
  3. npm --prefix apps/api run test ist gruen, mit mindestens der zu Beginn von Aufgabe 1 selbst gemessenen Testzahl.
  4. npm --prefix apps/api run type-check ist sauber.
  5. npm --prefix apps/api run test -- src/prisma/rls-access-inventory.spec.ts ist gruen — die maschinelle Klammer zwischen Quelltext und Klassifikation.
  6. .planning/WINDOWS.md ist in Kopf, Tabelle und JSON-Block widerspruchsfrei; #19 behoben, ein neuer Eintrag offen, #18/#20/#21/#23 unveraendert offen.
  7. DATABASE_URL in allen Compose-Dateien und Beispiel-Umgebungsdateien zeigt unveraendert auf die Rolle ohne _app-Zusatz; prisma/schema.prisma ist unveraendert.
  8. Manuell zu lesen, nicht maschinell zu pruefen: der neue Abschnitt der Kritikschrift fuehrt fuer jede der drei Regeln BEIDE Fehlerrichtungen und nicht nur die, die repariert wurde.

<success_criteria>

  • Die drei Regeln greifen so weit, wie sie sollen, und das ist an der lebenden Datenbank gemessen statt aus der Migrationsdatei erschlossen.
  • Kein Test und keine Pruefung behauptet noch, eines der drei Loecher sei erwartetes Verhalten — und keiner ist dabei verloren gegangen.
  • Der eine Anwendungspfad, den die Reparatur still falsch gemacht haette, ist gebunden, und die Messung, die das belegt, laeuft bei jedem Werkzeuglauf mit.
  • Die anwendungsseitige Mandanten-Gegenpruefung steht unveraendert.
  • Keine Aufzeichnung beschreibt mehr ein Loch, das es nicht mehr gibt; keine gibt mehr eine Handlungsanweisung, die nach der Reparatur falsch waere.
  • Was die Reparatur nicht loest, ist als eigener offener Punkt aufgeschrieben statt mit dem geschlossenen zu verschwinden.
  • Der Schalter ist weiterhin aus.

</success_criteria>

Bericht nach `.planning/quick/260910-jab-mandantentrennung-die-drei-zu-kurz-greif/260910-jab-SUMMARY.md`.

Er nennt ausdruecklich: die drei zu Beginn SELBST gemessenen Ausgangszahlen und die drei am Ende gemessenen; das Ergebnis der Bestandszaehlung ueber Mandantengrenzen zeigender Zeilen; den Falsifizierungsnachweis aus Aufgabe 2 mit Testnamen und Fehlermeldung; das Ergebnis der eigenen Messung zur fehlenden Benutzerdimension; und — als eigener Abschnitt — die Abweichung von der Aufgabenstellung, dass die Behauptung "kein Anwendungscode muss sich aendern" gemessen fuer genau einen Pfad nicht zutrifft, mit der Begruendung, warum das Binden dieses Pfades zur Reparatur gehoert und nicht daneben.