Files
tessera-ctl/.planning/quick/260921-m34-288-any-im-backend-einzeln-beurteilen-un/260921-m34-SUMMARY.md
T
schalli ad834076c2
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Tests (push) Successful in 1m6s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 18s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m0s
docs(quick-260921-m34): 288 any auf 15 gesenkt, jede verbliebene mit Urteil
Zusammenfassung mit Urteilsregister und STATE.md zum Quick-Vorgang
260921-m34.

Der groesste Posten war ein einziges Missverstaendnis: 105 Stellen
trugen eine Zusicherung an der Mandantenbindung, die nie noetig war -
prisma.$extends() liefert laengst einen getypten Klienten.

Die tenantId-Frage wurde hergeleitet statt nach Bequemlichkeit
entschieden: string ohne Fragezeichen, belegt aus Pflichtspalte im
Schema, Bestandstyp und dem Super-Admin-Zweig der Mandantenpruefung -
der diesen Typ gar nicht liest und deshalb nicht zu totem Code werden
kann. Der Waechter blieb ueber den ganzen Lauf unveraendert.

Vier Befunde gemeldet statt still repariert. Zwei brauchen eine
Entscheidung, beide wuerden Verhalten aendern - darunter ein
sicherheitsrelevanter: die IMAP-Einstellung STARTTLS erzwingt nichts,
weil die gesetzte Option in imapflow 1.4.3 nicht existiert.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
2026-09-21 17:35:00 +02:00

415 lines
22 KiB
Markdown

---
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<unknown>`. |
| **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<User, keyof typeof
PLATFORM_USER_SELECT>` 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<User, ...>` 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<PkcsEnvelopedData | PkcsSignedData>` 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<T>,
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.