--- phase: quick-260921-m34 plan: 01 subsystem: apps/api (Typdisziplin) tags: [typescript, any, refactor, mandantentrennung, prisma, node-forge, imapflow] status: complete requires: [] provides: - "apps/api/src/auth/types/auth-user.ts (AuthUser, AuthenticatedRequest, JwtPayload, UploadedFileLike)" - "apps/api/src/prisma/prisma-error.ts (prismaErrorCode, prismaErrorTarget)" affects: - "apps/api/src (48 Dateien)" - "apps/web/src/test/setup.ts" tech-stack: added: [] patterns: - "catch (e: unknown) plus Form-Eingrenzung statt catch (e: any)" - "Mitgelieferte Bibliothekstypen vor eigenen Handschnittstellen" - "Ehrliches any mit geschriebener Begruendung statt erzwungener Zusicherung" key-files: created: - apps/api/src/auth/types/auth-user.ts - apps/api/src/prisma/prisma-error.ts modified: - apps/api/src/prisma/prisma-tenant.extension.ts - apps/api/src/cert-manager/cert-manager.service.ts - apps/api/src/inbox/imap.provider.ts - apps/api/src/inbox/exchange-inbox.provider.ts - apps/api/src/calendar/providers/exchange.provider.ts - apps/api/src/user/user.service.ts - apps/api/src/groups/groups.service.ts - apps/api/src/ldap/ldap.service.ts - apps/api/src/mail/mail.service.ts decisions: - "AuthUser.tenantId ist string, aus drei Belegen hergeleitet; der SUPER_ADMIN-Zweig in TenantGuard bleibt unangetastet" - "Fehlereingrenzung per Form-Pruefung statt instanceof PrismaClientKnownRequestError, weil alle Testdoppel angehaengte .code-Felder werfen" - "15 Befunde bleiben mit Urteil und Begruendung stehen; Null war ausdruecklich nicht das Ziel" metrics: duration: "1h 20min (16:09 bis 17:29 Uhr, 21.09.2026)" completed: 2026-09-21 actuals: tokens: 151201 tasks: 3 commits: 7 plan_head_before: 8d845e7 --- # Quick-Aufgabe 260921-m34: 288 any im Backend einzeln beurteilen Summary Die 288 `any`-Befunde in `apps/api` sind auf **15** gefallen, jeder der 288 hat ein Urteil mit Begruendung, und die Arbeit hat sieben falsche Annahmen im Bestandscode aufgedeckt, die alle gemeldet und keine still repariert wurden. ## Die Zahl, und was sie bedeutet | Groesse | Ausgang | Ende | |---|---|---| | `lint/suspicious/noExplicitAny` in `apps/api/src` | **288** (48 Dateien) | **15** (7 Dateien) | | dasselbe in `apps/web/src` | 1 | 0 | | `lint/style/noNonNullAssertion` in `apps/api/src` | 56 | 56 | | `as unknown as` in `apps/api/src` | 33 | 33 | | `ts-expect-error` / `ts-ignore` | 0 / 0 | 0 / 0 | | Lint-Unterdrueckungsmarker | 1 | 1 | | Befunde der Schwere `error` | 0 | 0 | Die vier Zeilen in der Mitte sind die wichtigsten der Tabelle. Sie belegen, dass die 273 verschwundenen Befunde tatsaechlich getypt und nicht bloss stillgelegt wurden: haette die Arbeit die bequeme Abkuerzung genommen, waere mindestens einer dieser Zaehlwerte gestiegen. Keiner ist gestiegen. ### Aufteilung der 288 auf die drei Urteile (D-01) | Urteil | Anzahl | Was dahintersteckt | |---|---:|---| | **typisiert** | 252 | Die Zusicherung war ueberfluessig oder der richtige Typ war ableitbar — aus Prismas Erweiterungstypen, aus den beiden Signierstellen des Tokens, aus `schema.prisma`, oder aus den mitgelieferten Typen einer Fremdbibliothek. | | **auf `unknown` umgestellt** | 21 | 18 Fehlerfaenger, die beiden `.then((results: unknown[]) => ...)` in `prisma-tenant.extension.ts` und `intercept(): Observable`. | | **bleibt** | 15 | Register unten. Jede Stelle traegt ihre Begruendung ausserdem direkt im Code. | | | **288** | | Der Plan hat 20 bis 40 verbleibende Befunde erwartet; es sind 15 geworden. Der Unterschied kommt nicht daher, dass hier mehr erzwungen wurde, sondern aus drei Messungen, die guenstiger ausfielen als die Vorschau: `@types/node-forge` beschreibt PKCS7 besser als angenommen (die vier `p7: any` liessen sich mit dem mitgelieferten `Captured<...>`-Typ aufloesen), `imapflow` deklariert `node.parameters` bereits vollstaendig, und `expect.extend(matchers)` in `apps/web` brauchte seine Zusicherung schlicht nicht mehr. Die Zaehlwerte oben sind der Beleg, dass dabei nichts gegen eine Behauptung getauscht wurde. ## Urteilsregister: die 15 Stellen, die bleiben Jede dieser Zeilen steht so auch als Kommentar an der Codestelle. Wer spaeter hier aufraeumen will, findet die Begruendung dort, wo er zuerst nachsieht. | # | Datei:Zeile | Form | Begruendung (gemessen) | |---|---|---|---| | 1 | `cert-manager/cert-manager.service.ts:296` | `cert.publicKey as any` | `@types/node-forge` kennt nur `PublicKey = rsa.PublicKey \| ed25519.Key` (index.d.ts:232). Der EC-Zweig darunter liest `curve` und `params.curve.q.bitLength()` — Felder, die node-forge zur Laufzeit liefert, die der mitgelieferte Typ aber gar nicht kennt. Eine Umdeutung ueber zwei Stufen wuerde dieselbe Luecke verdecken und zusaetzlich geprueft aussehen. | | 2 | `cert-manager/cert-manager.service.ts:318` | `(e: any)` | `Certificate.extensions` ist in `@types/node-forge` als `any[]` deklariert (index.d.ts:435). Der mitgelieferte Typ sagt ueber den Inhalt einer Erweiterung nichts aus. | | 3 | `cert-manager/cert-manager.service.ts:319` | `(sanExt as any)` | wie 2 — `sanExt` stammt aus demselben `any[]`. | | 4 | `cert-manager/cert-manager.service.ts:319` | `(n: any)` | wie 2. Eine eigene Schnittstelle fuer `altNames` waere unbelegt: der Compiler koennte sie an keiner Stelle gegen etwas pruefen, sie saehe aber geprueft aus (D-02). | | 5 | `cert-manager/cert-manager.service.ts:589` | `null as any` | node-forge 1.4.0 nimmt hier einen fehlenden Schluessel an und erzeugt ein reines Zertifikatsbuendel; `@types/node-forge` schliesst `null` aus. Die mitgelieferten Typen beschreiben die Bibliothek an dieser Stelle nachweislich falsch. | | 6 | `cert-manager/cert-manager.service.ts:752` | `null as any` | wie 5, zweite Aufrufstelle. | | 7 | `dkv/dkv-scheduler.service.ts:144` | `job as any` | Ohne die Zusicherung meldet `tsc`, dass das lokale `job` nur die Form `{ start(): void }` hat, waehrend `addCronJob()` einen vollstaendigen `CronJob` verlangt. Ursache ist der `require()`-Umweg aus 07-04 (pnpm-Isolation, `cron` ist nur mittelbare Abhaengigkeit). Aufloesen hiesse die Beschaffung der Klasse aendern (Verhaltensaenderung, D-03) oder `cron` direkt aufnehmen (neue Abhaengigkeit, D-04). | | 8 | `tenders/tender-digest.scheduler.ts:94` | `job as any` | wie 7. | | 9 | `tenders/tender-scheduler.service.ts:143` | `job as any` | wie 7. | | 10 | `groups/groups.service.ts:373` | `(u: any)` | Gefolge von 12: `tx` ist selbst `any`, und `any.map()` gibt dem Parameter keine kontextuelle Typisierung. Faellt automatisch mit 12. | | 11 | `groups/groups.service.ts:390` | `(a: any)` | wie 10. | | 12 | `prisma/prisma-tenant.extension.ts:264` | `fn: (tx: any)` | In Aufgabe 1 gemessen und verworfen: `Prisma.TransactionClient` erzwingt an den vier Aufrufstellen vollstaendige Prisma-Erzeugungstypen und bricht das absichtlich unvollstaendige Testdoppel in `prisma-tenant.extension.spec.ts` (TS2322). Das waere eine Aenderung an einer Teststruktur, kein ehrliches Typisieren. | | 13 | `prisma/prisma-tenant.extension.ts:266` | `async (tx: any)` | wie 12, dieselbe Funktion. | | 14 | `inbox/imap.provider.ts:78` | `(node as any).disposition...` | **Befund B-05.** Bleibt absichtlich sichtbar: der Ausdruck liest `.parameters` von einer Zeichenkette und ist zur Laufzeit immer `undefined`. Umbiegen auf `dispositionParameters` waere eine Verhaltensaenderung. | | 15 | `inbox/imap.provider.ts:402` | `} as any` | **Befund B-06.** Bleibt absichtlich sichtbar: die Zusicherung verdeckt, dass `requireTLS` in imapflow 1.4.3 gar nicht existiert. Die Option zu entfernen waere eine stille Reparatur. | Die Gruppen dahinter sind klein: sechs Stellen an node-forge, drei an der Cron-Beschaffung, vier an der Transaktionshilfe, zwei absichtlich stehen gelassene Befunde an imapflow. ## Die aufgedeckten falschen Annahmen (D-03) Diese Aufgabe hat ohne bekannten Fehler begonnen. Das hier ist ihr eigentlicher Ertrag: sieben Stellen, an denen der Bestandscode etwas annimmt, was nicht stimmt. **Keine davon wurde still repariert** — das Verhalten der API ist unveraendert. **B-01 — `tenders/tender-matching.service.ts:159` (Aufgabe 1).** Die Handannotation `(match: { tender: unknown })` verengte den Wert falsch, sobald der Prisma-Klient richtig getypt war. Sie existierte nur, um unter dem `any`-Klienten TS7006 zu vermeiden. Annotation geloescht, damit der hergeleitete Typ durchkommt; keine Zusicherung an ihrer Stelle. **B-02 — `dashboard/dashboard.controller.ts:74` (Aufgabe 2b).** Der Handler las `req.user?.role` nach `extractContext()` und gab die Rolle an `getWidgets(role: Role)` weiter, das sie zwingend verlangt. Die Annahme "hier gibt es immer einen Aufrufer" stimmt — die Pruefung "No user context" erzwingt sie —, aber sie stand in einer anderen Methode, wo der Compiler sie nicht sehen konnte. `extractContext()` gibt die Rolle jetzt mit zurueck: keine neue Pruefung, kein erfundener Wert, gleiche Reihenfolge, gleiche Meldungen. **B-03 — `tenders/tenders.controller.ts:142` (Aufgabe 2b).** `resolveRequestingTenantId()` erklaerte `string | undefined`, liest aber `req.tenantId`, das `TenantGuard` fuer einen SUPER_ADMIN ohne Mandanten auf `null` setzt. Die Erklaerung war nie vollstaendig. Auf `string | null | undefined` erweitert — reine Erklaerung: die Funktion entscheidet seit jeher ueber den Wahrheitswert und faellt bei `null` wie bei `undefined` zu (nur global sichtbare Ausschreibungen). **B-04 — Falle im Mandantentrennungs-Erkenner (Aufgabe 3b).** Die naheliegende Prisma-Schreibweise `Prisma.UserGetPayload<{ select: typeof X }>` laesst `rls-access-inventory.spec.ts` rot werden: der Erkenner zaehlt **jede** `select:`-Angabe ausserhalb eines erkannten Modellaufrufs als Verstoss und unterscheidet Typposition nicht von Aufrufposition. Beim ersten Versuch gemessen. Der Erkenner ist die Mandantenkontrolle (T-M34-03) und wurde **nicht** aufgeweicht — stattdessen leitet der Zeilentyp ueber `Pick` her, was ohne das Wort `select` auskommt. Wer kuenftig `UserGetPayload` einsetzen will, muss zuerst den Erkenner erweitern, nicht die Ausnahmeliste. **B-05 — `inbox/imap.provider.ts:78` (Aufgabe 3c). Sicherheitsnah, offen.** `(node as any).disposition?.parameters?.filename` liest `.parameters` von einer **Zeichenkette**: imapflow deklariert `disposition` als `string` (`imap-flow.d.ts:448`, also "attachment"/"inline"), die zugehoerigen Parameter liegen in einem eigenen Feld `dispositionParameters` (:450). Der Ausdruck ist zur Laufzeit **immer** `undefined`, `dispositionFilename` ist stets `''`. Folge: Anhaenge, die als `application/octet-stream` kommen (typisch fuer Outlook), werden ueber den Dateinamen aus Content-Disposition **nicht** erkannt — nur ueber den aus Content-Type. Betrifft den DKV-Rechnungseinzug. Nicht repariert, weil das Umbiegen erstmals Anhaenge einsammeln wuerde, die heute uebersprungen werden — eine Verhaltensaenderung, die ein Mensch entscheiden muss. **B-06 — `inbox/imap.provider.ts:402` (Aufgabe 3c). Sicherheitsnah, offen.** `requireTLS: config.encryption === 'starttls'` wird an `new ImapFlow(...)` uebergeben, aber `requireTLS` kommt in imapflow 1.4.3 **nirgends** vor — weder in `ImapFlowOptions` (`lib/imap-flow.d.ts`) noch im Laufzeitcode (`lib/imap-flow.js`), beides durchsucht. Die Option wird still verworfen; ein STARTTLS-Zwang entsteht durch sie nicht. Genau das `} as any` hat es verdeckt. Die Einstellung "STARTTLS" in der Postfach-Konfiguration bewirkt damit nicht das, was ihr Name verspricht. Nicht repariert (D-03), Zusicherung bleibt sichtbar stehen, damit der Befund in der Zaehlung nicht verschwindet. **B-07 — httpntlm-Antwortrumpf (Aufgabe 3c). Ohne Auswirkung, aber falsch.** Beide Exchange-Wege riefen `res.body?.toString('utf-8')` auf und nahmen damit einen Buffer an. Gemessen: httpntlm reicht an httpreq durch, und httpreq gibt den Rumpf als **Zeichenkette** zurueck, solange die Option `binary` nicht gesetzt ist (`httpreq@1.1.1/lib/httpreq.js:391`) — keiner der beiden Aufrufer setzt sie. Das ging bisher nur gut, weil `String.prototype.toString()` sein Argument ignoriert. Die Testdoppel reichen umgekehrt wirklich einen Buffer herein, beide Formen kommen also vor. `NtlmResponse.body` nennt jetzt beide; die Fallunterscheidung liefert fuer jede exakt dasselbe Ergebnis wie zuvor. ## Die `tenantId`-Entscheidung im gemeinsamen Aufrufer-Typ `AuthUser.tenantId` ist **`string`**, nicht `string | undefined`. Die Messung allein haette in die falsche Richtung gedraengt (`string | undefined` erzeugte acht Fehler im Produktivcode, `string` keinen); entschieden wurde auf drei Belegen: 1. `apps/api/prisma/schema.prisma` deklariert `User.tenantId String` **ohne** `?`. Die Spalte ist Pflicht, und beide Signierstellen schreiben genau diesen Spaltenwert — seit dem ersten Commit des Anmeldedienstes (6190f3d) gibt es keine Token-Erzeugung ohne diesen Anspruch. 2. Der Bestand beschreibt dasselbe Objekt in `SessionUser` (`bug-reports.service.ts`) bereits als `tenantId: string`. `SessionUser` ist jetzt ein `Pick<>` von `AuthUser`, damit es keine zweite, abweichende Beschreibung desselben Objekts gibt. 3. `TenantGuard` haelt fuer SUPER_ADMIN einen Zweig ohne Mandanten vor und setzt dort `req.tenantId = null`. Beleg 3 spricht **nicht** gegen `string`, und genau daran haengt die Sicherheitsfrage: der Zweig in `TenantGuard` ist eine Tiefenverteidigung gegen ein Token ohne diesen Anspruch, und er liest `AuthUser` gar nicht — der Waechter holt sein Anfrageobjekt ungetypt. Dieser Typ kann den Zweig also nicht zu totem Code machen. Dass es den Zweig gibt, steht ausserdem weiterhin im Typsystem, nur an der richtigen Stelle: `AuthenticatedRequest.tenantId` ist `string | null | undefined`. Am Typ steht dazu eine ausdrueckliche Warnung fuer spaetere Leser, dass der SUPER_ADMIN-Zweig ein Schutzzweig ist und nicht entfernt oder wegtypisiert werden darf (T-M34-01). `tenant.guard.ts` wurde in diesem ganzen Lauf **nicht angefasst** — in Aufgabe 2 per `git diff --stat` nachgewiesen. ## Was die Arbeit sonst noch geaendert hat **Zwei neue Dateien, beide klein und begruendet.** `apps/api/src/auth/types/auth-user.ts` traegt den gemeinsamen Aufrufer-Typ; jedes Feld hat seine Herkunft als Kommentar. `apps/api/src/prisma/prisma-error.ts` traegt `prismaErrorCode()` und `prismaErrorTarget()`. **Warum die Fehlereingrenzung Form-Pruefungen macht und kein `instanceof`.** Der naheliegende Weg waere `err instanceof Prisma.PrismaClientKnownRequestError` gewesen. Gemessen: samtliche Testdoppel in `apps/api` werfen `new Error(...)` mit angehaengtem `.code` (groups, user, ldap, tenders, module-grants, admin-seed) und `dashboard.service.spec.ts:451` ein reines `{ code: 'P2002' }`. Ein `instanceof`-Test haette all diese Werte in den jeweils **anderen** Zweig geschickt — eine Verhaltensaenderung, und nach T-M34-06 genau die Art von Aenderung, die einen Hintergrundlauf kuenftig abbrechen laesst, der heute weiterlaeuft. Die Helfer bilden `err?.code` und `err?.meta?.target` deshalb eins zu eins ab, nur ohne `any`. **Nebengewinne ohne neue Zusicherungen.** In `cert-manager.service.ts` sind 25 Zusicherungen der Form `file.buffer as Buffer` weggefallen, weil `file` kein `any` mehr ist. In `dkv` und `settings` fielen `req.tenantId as string | undefined` weg. `as unknown as` ist trotzdem bei 33 geblieben — dieselbe Zahl wie zu Beginn. ## Abweichungen vom Plan **1. [Regel 3 — blockierend] Neue Datei `prisma/prisma-error.ts` statt 18 Eingrenzungen von Hand.** Der Plan nennt fuer Aufgabe 3 Schritt A keine neue Datei. 18 Fehlerfaenger einzeln mit einer vierzeiligen Form-Pruefung zu versehen haette dieselbe Logik achtzehnmal wiederholt und die Begruendung, warum kein `instanceof` verwendet wird, achtzehnmal daneben. Die Datei liegt in `prisma/`, weil alle Aufrufer Prisma-Fehlercodes pruefen. Keine neue Abhaengigkeit. Commit: `32591b6`. **2. [Regel 1 — Fehler] Erste Fassung von `user.service.ts` machte die Mandanten-Spec rot.** `Prisma.UserGetPayload<{ select: typeof X }>` hat `rls-access-inventory.spec.ts` gebrochen (siehe B-04). Sofort im selben Schritt auf `Pick` umgestellt, der Erkenner blieb unangetastet, Spec wieder 30/30. Commit: `3892c5f`. **3. [Messung weicht vom Plan ab] `calendar.service.ts:206/255` sind keine Prisma-JSON-Eingaben.** Der Plan vermutete `Prisma.InputJsonValue`. Gemessen: es sind dynamisch gebaute Erzeugungs- und Aenderungseingaben fuer `CalendarSource`. Richtig getypt mit `Prisma.CalendarSourceUncheckedCreateInput` / `...UncheckedUpdateInput`. Commit: `3892c5f`. **4. [Messung weicht vom Plan ab] node-forge war besser beschrieben als erwartet.** Der Plan rechnete damit, dass ein Teil der elf Stellen bleibt. Gemessen: die vier `p7: any` liessen sich mit dem mitgelieferten `Captured` aufloesen, `cert.siginfo` war ohnehin getypt. Sechs bleiben, fuenf wurden getypt. Commit: `d8fb9ae`. ## Pruefungen — die echten Ausgaben ### `noExplicitAny`, `noNonNullAssertion`, `error`-Befunde ``` $ npx biome lint --reporter=json --max-diagnostics=2000 apps/api/src | python3 -c "..." any 15 nonnull 56 error 0 Rueckgabewert: 0 ``` Schranke der Aufgabe: `any <= 45`, `nonnull <= 56`, `error == 0`. Alle drei eingehalten. ### Vollstaendige Restliste (Pruefung 2 der Aufgabe) ``` apps/api/src/cert-manager/cert-manager.service.ts:296:40 | const pubKey = cert.publicKey as any; apps/api/src/cert-manager/cert-manager.service.ts:318:48 | const sanExt = cert.extensions?.find((e: any) => e.name === 'subjectAltName'); apps/api/src/cert-manager/cert-manager.service.ts:319:41 | const san: string[] = ((sanExt as any)?.altNames ?? []).map((n: any) => apps/api/src/cert-manager/cert-manager.service.ts:319:71 | const san: string[] = ((sanExt as any)?.altNames ?? []).map((n: any) => apps/api/src/cert-manager/cert-manager.service.ts:589:19 | null as any, // cert-only PFX - null key accepted by node-forge 1.4.0 apps/api/src/cert-manager/cert-manager.service.ts:752:19 | null as any, // cert-only PFX - null key accepted by node-forge 1.4.0 apps/api/src/dkv/dkv-scheduler.service.ts:144:55 | this.schedulerRegistry.addCronJob(jobName, job as any); apps/api/src/groups/groups.service.ts:373:33 | data: users.map((u: any) => ({ apps/api/src/groups/groups.service.ts:390:39 | data: activations.map((a: any) => ({ apps/api/src/inbox/imap.provider.ts:78:15 | ((node as any).disposition?.parameters?.filename as string | undefined)?.toLowerCase() ?? ''; apps/api/src/inbox/imap.provider.ts:402:10 | } as any); apps/api/src/prisma/prisma-tenant.extension.ts:264:12 | fn: (tx: any) => Promise, apps/api/src/prisma/prisma-tenant.extension.ts:266:41 | return prisma.$transaction(async (tx: any) => { apps/api/src/tenders/tender-digest.scheduler.ts:94:63 | this.schedulerRegistry.addCronJob(this.JOB_NAME, job as any); apps/api/src/tenders/tender-scheduler.service.ts:143:61 | this.schedulerRegistry.addCronJob(this.JOB_NAME, job as any); TOTAL 15 ``` Jede dieser 15 Zeilen steht im Urteilsregister oben. `apps/web/src` liefert keine Zeile mehr. ### Unterdrueckungsmarker ``` ### 3. Unterdrueckungsmarker keine stillgelegten Stellen ``` Das heisst im Einzelnen: `ts-expect-error` 0, `ts-ignore` 0, `biome-ignore` 1 (der eine Bestandsmarker), `as unknown as` 33. ### `biome.json` und Abhaengigkeiten ``` ### 4. biome.json biome.json unberuehrt ### 5. Abhaengigkeiten keine neuen Abhaengigkeiten, keine Versionsspruenge ``` Geprueft ueber `git diff --stat HEAD` gegen `biome.json`, alle drei `package.json` und `pnpm-lock.yaml` — jede Ausgabe leer. ### Typlauf und Lint ``` ### 6. type-check / lint type-check 4/4, lint 5/5 ``` ### Testsuiten ``` $ pnpm --dir apps/api run test Test Files 72 passed (72) Tests 1143 passed (1143) Duration 8.05s $ pnpm --dir apps/web run test Test Files 73 passed (73) Tests 531 passed (531) Duration 23.48s ``` Unveraenderte Zahlen gegenueber dem Ausgang (72/1143 und 73/531). ### Mandantentrennungs-Erkenner, ausdruecklich einzeln ``` $ pnpm --dir apps/api exec vitest run src/prisma/rls-access-inventory.spec.ts Test Files 1 passed (1) Tests 30 passed (30) Duration 557ms ``` Das ist die Kontrolle aus T-M34-03. Sie war in Aufgabe 3b einmal rot (B-04) und wurde nicht durch Aufweichen, sondern durch eine andere Typschreibweise wieder gruen. ## Commits | Commit | Aufgabe | `any` in `apps/api` danach | |---|---|---:| | `b188946` | 1 — Mandantenbindung entzaubert, 105 unnoetige Zusicherungen | 149 | | `f2fc39f` | 2a — gemeinsamer Aufrufer-Typ, aus den Signierstellen abgeleitet | 137 | | `7c9d7c1` | 2b — getypte Anfrage in elf Controllern, zwei Befunde gemeldet | 66 | | `52668c2` | 2c — Hochladewege getypt, 25 Zusicherungen fallen mit | 56 | | `32591b6` | 3a — 18 Fehlerfaenger auf `unknown`, mit echter Eingrenzung | 38 | | `3892c5f` | 3b — Prisma-nahe Formen getypt, Erkenner-Falle gemeldet | 31 | | `d8fb9ae` | 3c — Randschicht beurteilt, drei Befunde gemeldet | **15** | Jeder Commit war fuer sich gruen: nach jedem lief `pnpm type-check` 4/4, `pnpm lint` 5/5 mit 0 Befunden der Schwere `error`, und beide Testsuiten mit unveraenderten Zahlen (D-05, D-06). ## Offene Punkte fuer den Menschen Zwei der sieben Befunde brauchen eine Entscheidung, die diese Aufgabe nicht treffen durfte, weil beide das Verhalten aendern wuerden: - **B-05** (`imap.provider.ts:78`): sollen Anhaenge mit `application/octet-stream` kuenftig auch ueber den Dateinamen aus Content-Disposition erkannt werden? Heute werden sie es nicht. - **B-06** (`imap.provider.ts:402`): soll die Postfach-Einstellung "STARTTLS" tatsaechlich einen Zwang bewirken? Heute wird die Option von imapflow verworfen. Beide sind im Code markiert und stehen in der Zaehlung, verschwinden also nicht aus dem Blick. ## Self-Check **BESTANDEN.** Geprueft, nicht behauptet: - Beide neu angelegten Dateien existieren auf der Platte (`apps/api/src/auth/types/auth-user.ts`, `apps/api/src/prisma/prisma-error.ts`). - Alle sieben genannten Commits sind in `git log` vorhanden. - `apps/api/src/tenant/tenant.guard.ts` ist ueber den gesamten Lauf (`git diff --stat 8d845e7..HEAD`) unveraendert — keine einzige Zeile. - Die 15 Zeilen der Restliste stammen aus der maschinellen Biome-Ausgabe, nicht aus dem Gedaechtnis, und stimmen eins zu eins mit dem Urteilsregister ueberein. - Die Aufteilung 252 + 21 + 15 ergibt 288.