Files
AfiS-Aktensystem/docs/13-entscheidungen.md
T
HarlekinandClaude Opus 5 4446e3f0ac Fachdokumente nach docs/, Entscheidungen festgehalten
CLAUDE.md und README.md verweisen auf docs/, die Dateien lagen aber im
Wurzelverzeichnis. Verschoben.

docs/13-entscheidungen.md hält fest, wo die Dokumente sich widersprechen und
wie es aufgelöst wurde. Der wichtigste Punkt: 04-datenmodell.md verlangt eine
Regel, die jedes UPDATE auf CaseEntryVersion verwirft, 05-akten-und-vorlagen.md
verlangt, dass eine Umklassifizierung rückwirkend auf alle Versionen wirkt.
Beides zusammen geht nicht. Aufgelöst über einen Trigger, der die Tabelle
anhängend hält, aber das Anheben von minClearance zulässt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-01 00:00:58 +02:00

7.1 KiB

Getroffene Entscheidungen

Stellen, an denen die Fachdokumente sich widersprechen, lückenhaft sind oder gegen eine Eigenschaft von Postgres laufen. Jede Zeile nennt den Konflikt, die Entscheidung und den Grund. Wo eine Entscheidung fachlich anders ausfallen kann, steht das ausdrücklich dabei.

Struktur

Konflikt Entscheidung
CLAUDE.md und README.md verweisen auf docs/, die Dateien lagen im Wurzelverzeichnis Nach docs/ verschoben
02-architektur.md zeigt app/ und lib/ im Wurzelverzeichnis, TASKS.md Phase 0 verlangt einen pnpm-Workspace mit web/ und worker/ Workspace mit web/, worker/, pdf/ und packages/db/. Die Struktur aus 02-architektur.md gilt innerhalb von web/
Der Worker braucht denselben Prisma-Client wie die Web-App Eigenes Workspace-Paket packages/db (@afis/db), von web und worker benutzt. Migrationen und Seed liegen dort

Datenmodell

Konflikt Entscheidung
CaseParticipant hat den zusammengesetzten Primärschlüssel [caseId, role, userId, personnelFileId], aber userId und personnelFileId sind beide optional. Postgres lässt NULL in einem Primärschlüssel nicht zu Eigener Schlüssel id, Eindeutigkeit über zwei partielle Unique-Indizes plus eine CHECK-Bedingung, dass mindestens eines der beiden Felder gesetzt ist
04-datenmodell.md nennt zugleich CREATE SEQUENCE case_number_2026 und die Tabelle CaseCounter(year, lastNumber) mit SELECT … FOR UPDATE CaseCounter. Eine Sequenz pro Jahr müsste jedes Jahr von Hand angelegt werden, und der Abteilungsleiter ist ausdrücklich nicht technisch
Das rohe SQL benutzt search_vector und updated_at in Schlangenschrift, die Prisma-Modelle Höckerschrift Prisma-Voreinstellung behalten: Tabellen in Höckerschrift-Pascal, Spalten in Höckerschrift. Das SQL quotet entsprechend ("searchVector", "updatedAt")
Kein Modell für Kanäle, obwohl ChatMessage.channelId existiert und 06-chat-und-realtime.md Kanäle mit eigener Mindeststufe verlangt Modell Channel ergänzt, dazu ChatReaction für den festen Reaktionssatz
/api/health soll den Discord-Gateway prüfen, aber die Web-App fragt nie live bei Discord an Modell ServiceHeartbeat. Der Worker schreibt alle 30 Sekunden seinen Zustand, web liest ihn

Der wichtigste Widerspruch

04-datenmodell.md verlangt CREATE RULE entry_version_no_update … DO INSTEAD NOTHING auf CaseEntryVersion. 05-akten-und-vorlagen.md verlangt unter „Umklassifizierung wirkt rückwirkend", dass beim Anheben von minClearance alle bestehenden Versionen den neuen Wert erben. Beides zusammen geht nicht: die Regel würde genau die Nachführung verhindern, die das Leck schließt.

Aufgelöst durch einen Trigger statt einer Regel. CaseEntryVersion bleibt anhängend, aber minClearance darf nachgeführt werden, und nur nach oben. Jede andere Änderung wirft einen Fehler. Das erhält beide Absichten: der Text ist unveränderlich, die Klassifizierung wirkt rückwirkend.

Ein zweiter Trigger schützt CaseEntry.authorId, createdAt, caseId und type auf Datenbankebene, nicht nur in der Anwendung.

Suche

08-suche-und-export.md beschreibt einen search_vector je Akte aus Titel, Abschnitten und Verlaufseinträgen. Ungeklärt bleibt, was mit Inhalten oberhalb der Stufe des Suchenden passiert. Ein gemeinsamer Vektor über alle Abschnitte würde einen Nutzer mit Stufe 1 einen Begriff finden lassen, der nur in einem als [[c4]]…[[/c4]] geschwärzten Bereich steht. Das verstößt gegen harte Regel 6.

Vorläufig aufgenommen wird nur, was jeder lesen darf, der die Akte überhaupt sehen darf: Aktenzeichen und Titel (Gewicht A), Abschnitte und Verlaufseinträge mit minClearance = 0 (B und C), Schwärzungs-Auszeichnungen vorher entfernt. Zurückgezogene Einträge fallen heraus.

Das ist sicher, aber für hohe Stufen zu sparsam: ein Nutzer mit Stufe 5 findet geheime Inhalte nicht über die Volltextsuche. Phase 7 muss das auflösen, sinnvollerweise über getrennte Vektorspalten je Stufe. Diese Entscheidung ist bewusst konservativ getroffen, weil ein Leck hier schwerer zu bemerken ist als eine Lücke.

Prisma 7

Die Dokumente nennen nur „Prisma + PostgreSQL 16". Eingesetzt wird Prisma 7, die aktuelle Fassung. Sie unterscheidet sich in drei Punkten, die den Aufbau betreffen:

Änderung Folge hier
Verbindungsdaten stehen nicht mehr im Schema packages/db/prisma.config.ts. Migrationen laufen über DIRECT_DATABASE_URL, also an pgbouncer vorbei
Keine Rust-Query-Engine mehr, stattdessen Treiber-Adapter @prisma/adapter-pg über pg. Der Treiber wird für LISTEN/NOTIFY ohnehin gebraucht, und die Images müssen keine Engine nachladen
Neuer Generator prisma-client statt prisma-client-js Der Client wird als ECMAScript-Module nach packages/db/src/generated erzeugt statt nach node_modules/.prisma. Nötig mit importFileExtension = "js", sonst schreibt er endungslose Importe, die Node im ESM-Modus nicht auflöst

Der erzeugte Client ist ein Bauartefakt und liegt nicht im Repository. pnpm --filter @afis/db run build erzeugt ihn, die CI tut das vor lint, typecheck und test.

Für prisma migrate deploy braucht es weiterhin eine Rust-Schema-Engine. Sie wird beim Bauen des Web-Images heruntergeladen und liegt unter /app/migrator im Image. Beim Containerstart wird nichts nachgeladen: ein Container, der nur startet, wenn gerade eine Registry erreichbar ist, wäre auf einem Homeserver die falsche Bauart.

Betrieb

Konflikt Entscheidung
11-betrieb.md verlangt „Jeder Dienst braucht einen Healthcheck", die abgedruckte compose.yaml hat für db-backup und offsite-backup keinen Ergänzt. db-backup über seinen HTTP-Port 8080, offsite-backup über Cron-Prozess und restic version
Das Startkommando npx prisma migrate deploy würde im Container aus dem Netz nachladen Auf die im Image unter /app/migrator mitgelieferte Prisma-CLI umgestellt, zusammen mit Schema, Migrationsordner und Schema-Engine
pnpm deploy zieht die optionalen Peers von @prisma/client mit ins Image (CLI, Studio, TypeScript, rund 240 MB) --no-optional beim Bündeln. Zur Laufzeit wird nichts davon gebraucht

Protokollierung

09-performance-und-ux.md verlangt eine Puffertabelle für Audit-Einträge, die alle zwei Sekunden gebündelt geschrieben wird, zugleich aber, dass schreibende Aktionen ihren Eintrag in derselben Transaktion erzeugen. Umgesetzt wird beides getrennt: schreibende Aktionen protokollieren synchron in der Transaktion, Lesezugriffe ab Stufe 3 laufen über einen Puffer. Der Puffer liegt im Prozessspeicher, nicht in einer Tabelle — eine Puffertabelle hätte dieselbe Verlustgefahr bei einem Absturz, aber zusätzliche Schreiblast. Kommt in Phase 2.

Vorlagen im Seed

CLAUDE.md beschreibt pnpm db:seed als „Testdaten inkl. Vorlagen", TASKS.md ordnet die vier Startvorlagen aber Phase 3 zu. Der Seed legt vorerst nur an, was die Anwendung zum Laufen braucht: die sechs Freigabestufen, die Änderungsrichtlinie und den Aktenzeichen-Zähler. Die Vorlagen kommen mit dem Vorlagen-Editor in Phase 3.