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
53 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-260911-mkj | 01 | execute | 1 | true |
|
|
|
|
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_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>
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 ueberinclude: { module: true }auftenantPrisma.tenantModuleActivation.findManyerreicht (Zeilen 196, 253);Moduleplattformweit ohne Zeilenschutz, die Bindung des Elternaufrufs ist fuer die Unterabfrage wirkungslos, nicht schaedlich (gleiche Einordnung wiemodule-registry.service.ts/module). - NEU
ldap/ldap-config.service.ts | tenant | keine-mandantengebundene-tabelle | ungebunden—getAllActiveConfigs()(Zeile 308)include: { tenant: true }aufthis.prisma.ldapConfig.findMany;Tenanttraegt keinen Zeilenschutz (Aufgabe 1 in 260911-e2s,tenant-tabelle-ohne-zeilenschutz-bleibt-lesbar). - NEU
module-registry/module-access.service.ts | group | muss-mandantengebunden | gebundenund| groupMembership | muss-mandantengebunden | gebunden— Relationsfilterwhere: { tenantId, group: { memberships: { some: { userId } } } }auftenantPrisma.moduleGrant.findMany(Zeile 73): Prisma rendert das als Unterabfragen aufGroupundGroupMembershipunter DEREN Regeln; der Klient ist gebunden, beide Tabellen gehoeren demselben Mandanten. - NEU
tenders/tender-digest.scheduler.ts | tender | keine-mandantengebundene-tabelle | gebundenund| tenderSavedSearch | muss-mandantengebunden | gebunden—include: { tender: true, savedSearch: true }plusorderByuebersavedSearchauftenantPrisma.tenderMatch.findMany(Zeile 149) innerhalb der Mandantenschleife des Planers;Tenderist der plattformglobale Katalog,TenderSavedSearchmandantengebunden und ueber denselben gebundenen Klienten erreicht. - NEU
tenders/tenders.controller.ts | tenderSource | keine-mandantengebundene-tabelle | ungebunden—getTender(Zeile 612)include: { sources: { select: ... } }aufthis.prisma.tender.findUnique;TenderSourceplattformweit ohne Zeilenschutz (gleiche Einordnung wietender-dedup.service.ts/tenderSource). - GEAENDERT
ldap/ldap-config.service.ts | ldapFieldMapping: Klassemuss-mandantengebunden->beides, Standgebunden->gemischt. Begruendung ERGAENZEN, nicht ersetzen: die vierte Erkennung (260911-mkj) macht sichtbar, dass der bewusst uebergreifende Planer-LesepfadgetAllActiveConfigs()ueberinclude: { fieldMappings: true }inLdapFieldMappinghineinreicht — dieselbe Unterabfrage-Form wie die dreitenant-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 aufLdapFieldMappingueber Join aufLdapConfig, Migration 20260618112133). Die Klasse folgt der ElternzeileldapConfig(beides). - GEAENDERT
module-registry/module-registry.service.ts | module: Standungebunden->gemischt, Klasse bleibt; ergaenzen: die gebundene Haelfte stammt ausschliesslich ausinclude: { module: true }auftenantPrisma.tenantModuleActivation(Zeilen 56, 97, 148), fuer die schutzlose Katalogtabelle wirkungslos. - GEAENDERT
tenders/tender-matching.service.ts | tender: Standungebunden->gemischt, Klasse bleibt; ergaenzen: gebundene Haelfte ausinclude: { tender: true }auftenantPrisma.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; Kurzschreibweiseinclude: { 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 SpalteModellseither 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.
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 wederthis.prismanoch eineconst X = forTenant(-Zuweisung noch ein Transaktionsparameter sind, sind fuer ALLE vier Erkennungsformen der Bestandsaufnahme unsichtbar. Gemessen 260911-mkj:tenders/tenders.seed.ts(Funktionsparameterprisma: PrismaService,tenderRssFeedSource.findFirst/create, kein Eintrag in der Bestandsaufnahme) undtenders/backfill-tender-source.ts(eigenstaendiges Skript mitnew PrismaClient(),tender.findMany/update, durchRELATION_SPEC_EXCEPTIONSlaut 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-KommentarWINDOWS #TBD-MKJdurchWINDOWS #<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> |
<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
fixedmit 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>