M0: arc42-Konzeptdokument, Gherkin-Anforderungen und OpenAPI-Vertrag

- doc/architecture.md: arc42-Detaildokument mit Mermaid-Diagrammen
  (Bausteinsicht, Klassendiagramm, Statusmodell, 5 Sequenzdiagramme),
  Speicherkonzept, DEC-01..19-Entscheidungen mit Trade-offs
- doc/requirements/: 7 Features als Gherkin-Szenarien (TDD-Grundlage)
  mit Traceability-Tabellen Szenario <-> Stdlib-Testname
- doc/openapi.yaml: API-Vertrag fuer alle 9 Endpunkte (Quelle der Wahrheit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Christoph Kroczek
2026-07-13 15:20:48 +02:00
parent f52e4f62b0
commit 6e6cb0dc8d
10 changed files with 1291 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
# Feature: Dokumenten-Upload über die API
Als API-Client möchte ich Dokumente (PDF, JPG, PNG, HEIC) hochladen,
damit sie automatisch verarbeitet, durchsuchbar gemacht und abgelegt werden.
```gherkin
Funktionalität: Dokumenten-Upload über die API
Szenario: Erfolgreicher PDF-Upload
Angenommen ein gültiger API-Token
Wenn ein PDF per POST /v1/documents (multipart, Feld "file") hochgeladen wird
Dann antwortet das System mit 202 und einer neuen Dokument-UUID
Und das Dokument hat den Status "received"
Und die Datei liegt unter data/staging/{uuid}.pdf
Und ein Event "DocumentReceived" mit origin "api" wurde aufgezeichnet
Szenario: Upload eines Bildes
Angenommen ein gültiger API-Token
Wenn eine JPG-, PNG- oder HEIC-Datei hochgeladen wird
Dann antwortet das System mit 202
Und der media_type des Dokuments entspricht dem Dateityp
Szenario: Duplikat-Upload wird abgelehnt
Angenommen ein bereits importiertes Dokument mit bekanntem SHA-256-Hash
Wenn eine byte-identische Datei erneut hochgeladen wird
Dann antwortet das System mit 409
Und die Antwort enthält error.code "duplicate_document" und die existing_document_id
Und es wurde kein neues Aggregat angelegt
Szenario: Upload ohne Token wird abgelehnt
Angenommen kein oder ein falscher API-Token
Wenn ein Dokument hochgeladen wird
Dann antwortet das System mit 401
Szenario: Nicht unterstützter Dateityp wird abgelehnt
Angenommen ein gültiger API-Token
Wenn eine Datei mit nicht unterstützter Endung (z.B. .docx) hochgeladen wird
Dann antwortet das System mit 422 und error.code "unsupported_media_type"
Szenario: Upload über dem Größenlimit wird abgelehnt
Angenommen ein konfiguriertes Upload-Limit von 100 MB
Wenn eine größere Datei hochgeladen wird
Dann antwortet das System mit 413
Und es verbleibt keine .part-Datei im Staging
```
## Traceability
| Szenario | Go-Test (Paket `internal/api`, `internal/ingest`) |
|---|---|
| Erfolgreicher PDF-Upload | `TestUpload_PDFReturns202AndReceived` |
| Upload eines Bildes | `TestUpload_ImageSetsMediaType` |
| Duplikat-Upload wird abgelehnt | `TestUpload_DuplicateReturns409` |
| Upload ohne Token wird abgelehnt | `TestUpload_MissingTokenReturns401` |
| Nicht unterstützter Dateityp wird abgelehnt | `TestUpload_UnsupportedTypeReturns422` |
| Upload über dem Größenlimit wird abgelehnt | `TestUpload_TooLargeReturns413AndCleansUp` |
+54
View File
@@ -0,0 +1,54 @@
# Feature: Automatischer Import aus dem überwachten Ordner
Als Nutzer möchte ich Dateien einfach im Import-Ordner ablegen,
damit sie ohne manuellen Eingriff verarbeitet werden.
```gherkin
Funktionalität: Automatischer Import aus dem überwachten Ordner
Szenario: Neue Datei wird automatisch erkannt und verarbeitet
Angenommen der Watcher überwacht den Import-Ordner
Wenn eine PDF-Datei im Import-Ordner gespeichert wird
Dann wird die Datei nach Abschluss des Schreibvorgangs in das Staging übernommen
Und ein Event "DocumentReceived" mit origin "import_folder" wurde aufgezeichnet
Und die Datei liegt nicht mehr im Import-Ordner
Szenario: Unvollständig geschriebene Datei wird nicht vorzeitig übernommen
Angenommen eine Datei wird langsam in den Import-Ordner geschrieben
Wenn sich Größe oder Änderungszeit innerhalb der Stabilitätsfrist noch ändern
Dann wird die Datei noch nicht übernommen
Und erst nach unveränderter Stabilitätsfrist beginnt die Verarbeitung
Szenario: Beim Start vorhandene Dateien werden übernommen
Angenommen im Import-Ordner liegen Dateien, während das System nicht läuft
Wenn das System startet
Dann werden diese Dateien durch den Start-Scan erkannt und verarbeitet
Szenario: Periodischer Rescan fängt verpasste Ereignisse
Angenommen eine Datei wurde vom Dateisystem-Watcher nicht gemeldet
Wenn der periodische Rescan läuft
Dann wird die Datei erkannt und verarbeitet
Szenario: Duplikat aus dem Import-Ordner wird aussortiert
Angenommen ein bereits importiertes Dokument mit bekanntem SHA-256-Hash
Wenn eine byte-identische Datei im Import-Ordner gespeichert wird
Dann wird die Datei nach import/rejected/ verschoben
Und eine Warnung mit der existing_document_id wird geloggt
Und es wurde kein neues Aggregat angelegt
Szenario: Nicht unterstützte und versteckte Dateien werden ignoriert
Angenommen der Watcher überwacht den Import-Ordner
Wenn eine .docx-, .part- oder versteckte Datei dort gespeichert wird
Dann wird sie ignoriert und verbleibt unverändert
```
## Traceability
| Szenario | Go-Test (Paket `internal/importer`) |
|---|---|
| Neue Datei wird automatisch erkannt und verarbeitet | `TestImporter_NewFileIsIngested` |
| Unvollständig geschriebene Datei wird nicht vorzeitig übernommen | `TestImporter_WaitsForStableFile` |
| Beim Start vorhandene Dateien werden übernommen | `TestImporter_StartupScanPicksUpExistingFiles` |
| Periodischer Rescan fängt verpasste Ereignisse | `TestImporter_RescanPicksUpMissedFile` |
| Duplikat aus dem Import-Ordner wird aussortiert | `TestImporter_DuplicateMovedToRejected` |
| Nicht unterstützte und versteckte Dateien werden ignoriert | `TestImporter_IgnoresUnsupportedAndHiddenFiles` |
+58
View File
@@ -0,0 +1,58 @@
# Feature: Textextraktion und OCR
Als System möchte ich den Textinhalt jedes Dokuments gewinnen,
ohne die Originaldatei jemals zu verändern (DEC-IMMUTABLE-01),
damit Suche und Anreicherung auf externem Kontext arbeiten können.
```gherkin
Funktionalität: Textextraktion und OCR
Szenario: Born-digital-PDF braucht kein OCR
Angenommen ein PDF mit maschinenlesbarem Textlayer
Wenn die Extract-Stage läuft
Dann wird der Text per pdftotext gewonnen
Und ein Event "TextExtracted" mit needs_ocr=false wird aufgezeichnet
Und die OCR-Stage wird übersprungen
Szenario: Gescanntes PDF wird per OCR erschlossen
Angenommen ein PDF ohne bzw. mit unzureichendem Textlayer
Wenn die OCR-Stage läuft
Dann wird der Text per ocrmypdf im Sidecar-Modus gewonnen
Und ein Event "OCRCompleted" mit engine "ocrmypdf" wird aufgezeichnet
Und es wird kein Ausgabe-PDF erzeugt
Szenario: Bilddateien werden immer per OCR erschlossen
Angenommen ein importiertes JPG-, PNG- oder HEIC-Bild
Wenn die Verarbeitung läuft
Dann liefert die Extract-Stage needs_ocr=true
Und der Text wird per tesseract direkt aus dem Bild gewonnen
Szenario: HEIC wird nur temporär konvertiert
Angenommen ein importiertes HEIC-Bild
Wenn die OCR-Stage läuft
Dann wird für die Erkennung ein temporäres PNG erzeugt
Und das temporäre PNG wird nach der Erkennung gelöscht
Und die archivierte Datei ist das unveränderte HEIC-Original
Szenario: Originaldatei bleibt byte-identisch
Angenommen ein Dokument mit bekanntem SHA-256-Hash bei der Aufnahme
Wenn das Dokument vollständig verarbeitet und abgelegt wurde
Dann ist der SHA-256-Hash der archivierten Datei identisch mit dem Hash bei der Aufnahme
Szenario: Korrupte oder passwortgeschützte Datei schlägt endgültig fehl
Angenommen eine nicht lesbare oder passwortgeschützte PDF-Datei
Wenn Extraktion und OCR fehlschlagen
Dann wird ein Event "ProcessingFailed" mit retryable=false aufgezeichnet
Und das Dokument hat den Status "failed" mit gesetzter failed_stage
```
## Traceability
| Szenario | Go-Test (Pakete `internal/pdf`, `internal/ocr`, `internal/pipeline`) |
|---|---|
| Born-digital-PDF braucht kein OCR | `TestExtractStage_TextPDFSkipsOCR` |
| Gescanntes PDF wird per OCR erschlossen | `TestOCRStage_ScannedPDFUsesSidecarOnly` |
| Bilddateien werden immer per OCR erschlossen | `TestExtractStage_ImageAlwaysNeedsOCR` |
| HEIC wird nur temporär konvertiert | `TestOCR_HEICTempConversionIsCleanedUp` |
| Originaldatei bleibt byte-identisch | `TestPipeline_OriginalHashUnchangedAfterProcessing` |
| Korrupte oder passwortgeschützte Datei schlägt endgültig fehl | `TestPipeline_CorruptPDFFailsNonRetryable` |
+47
View File
@@ -0,0 +1,47 @@
# Feature: Optionale LLM-Anreicherung
Als Nutzer möchte ich, dass Dokumente semantisch angereichert werden (Titel, Zusammenfassung,
Tags, Dokumentdatum, Sprache), ohne dass ein nicht erreichbares LLM die Verarbeitung blockiert.
```gherkin
Funktionalität: Optionale LLM-Anreicherung
Szenario: Erfolgreiche Anreicherung
Angenommen ein Dokument mit extrahiertem Text und ein erreichbares LLM
Wenn die Enrich-Stage läuft
Dann wird ein Event "DocumentEnriched" mit title, summary, tags, doc_date und language aufgezeichnet
Und das verwendete Modell und die Prompt-Version sind im Event dokumentiert
Szenario: LLM nicht erreichbar führt zu Wiederholung mit Backoff
Angenommen das LLM ist nicht erreichbar
Wenn die Enrich-Stage fehlschlägt
Dann wird ein Event "ProcessingFailed" mit retryable=true aufgezeichnet
Und der nächste Versuch wird mit exponentiellem Backoff eingeplant
Szenario: Nach erschöpften Versuchen läuft die Pipeline weiter
Angenommen das LLM bleibt über die maximale Versuchszahl hinaus nicht erreichbar
Wenn der letzte Versuch fehlschlägt
Dann wird ein Event "EnrichmentSkipped" aufgezeichnet
Und das Dokument wird trotzdem indexiert und abgelegt
Szenario: Unbrauchbare LLM-Antwort ist wiederholbar
Angenommen das LLM liefert kein valides JSON
Wenn die Enrich-Stage die Antwort nicht parsen kann
Dann wird der Fehler als retryable behandelt
Szenario: Übersprungene Anreicherung ist nachholbar
Angenommen ein abgelegtes Dokument mit "EnrichmentSkipped"
Wenn POST /v1/documents/{id}/reprocess mit from_stage "enrich" aufgerufen wird
Dann wird nur die Anreicherung erneut ausgeführt
Und bereits abgeschlossene Schritte laufen nicht erneut
```
## Traceability
| Szenario | Go-Test (Pakete `internal/llm`, `internal/pipeline`) |
|---|---|
| Erfolgreiche Anreicherung | `TestEnrichStage_SuccessEmitsDocumentEnriched` |
| LLM nicht erreichbar führt zu Wiederholung mit Backoff | `TestEnrichStage_UnreachableLLMIsRetryableWithBackoff` |
| Nach erschöpften Versuchen läuft die Pipeline weiter | `TestPipeline_EnrichExhaustedSkipsAndContinues` |
| Unbrauchbare LLM-Antwort ist wiederholbar | `TestEnricher_MalformedJSONIsRetryable` |
| Übersprungene Anreicherung ist nachholbar | `TestReprocess_FromEnrichOnlyRunsEnrich` |
+58
View File
@@ -0,0 +1,58 @@
# Feature: Volltextsuche und Index-Rebuild
Als API-Client möchte ich Dokumente über ihren Inhalt und ihre Metadaten finden
und den Suchindex jederzeit neu aufbauen können.
```gherkin
Funktionalität: Volltextsuche
Szenario: Dokument ist über seinen Inhalt findbar
Angenommen ein verarbeitetes Dokument, dessen Text "Stromabrechnung Stadtwerke" enthält
Wenn GET /v1/search?q=stadtwerke aufgerufen wird
Dann enthält das Ergebnis das Dokument mit id, title, original_name, archive_path und snippet
Szenario: Suche berücksichtigt Metadaten
Angenommen ein angereichertes Dokument mit dem Tag "versicherung"
Wenn GET /v1/search?q=versicherung aufgerufen wird
Dann wird das Dokument gefunden, auch wenn der Begriff nicht im Text vorkommt
Szenario: Umlaute und Diakritika werden tolerant behandelt
Angenommen ein Dokument, dessen Text "Gebührenbescheid" enthält
Wenn nach "gebuhrenbescheid" gesucht wird
Dann wird das Dokument gefunden
Szenario: Treffer sind nach Relevanz sortiert
Angenommen mehrere Dokumente enthalten den Suchbegriff unterschiedlich häufig
Wenn gesucht wird
Dann sind die Treffer nach BM25-Relevanz absteigend sortiert
Szenario: OCR-Inhalte sind durchsuchbar
Angenommen ein gescanntes Dokument, dessen Text nur per OCR gewonnen wurde
Wenn nach einem Begriff aus dem OCR-Text gesucht wird
Dann wird das Dokument gefunden
Funktionalität: Index-Rebuild über die API
Szenario: Rebuild stellt einen konsistenten Index wieder her
Angenommen ein inkonsistenter oder leerer FTS-Index bei gefüllter documents-Projektion
Wenn POST /v1/admin/search/rebuild aufgerufen wird
Dann wird der Index vollständig aus der Projektion neu aufgebaut
Und alle zuvor findbaren Dokumente sind wieder findbar
Szenario: Rebuild erfordert Authentifizierung
Angenommen kein oder ein falscher API-Token
Wenn POST /v1/admin/search/rebuild aufgerufen wird
Dann antwortet das System mit 401
```
## Traceability
| Szenario | Go-Test (Pakete `internal/search`, `internal/api`) |
|---|---|
| Dokument ist über seinen Inhalt findbar | `TestSearch_FindsDocumentByContent` |
| Suche berücksichtigt Metadaten | `TestSearch_FindsDocumentByTag` |
| Umlaute und Diakritika werden tolerant behandelt | `TestSearch_DiacriticsInsensitive` |
| Treffer sind nach Relevanz sortiert | `TestSearch_ResultsOrderedByBM25` |
| OCR-Inhalte sind durchsuchbar | `TestSearch_FindsOCRContent` |
| Rebuild stellt einen konsistenten Index wieder her | `TestSearchRebuild_RestoresConsistentIndex` |
| Rebuild erfordert Authentifizierung | `TestSearchRebuild_RequiresAuth` |
+48
View File
@@ -0,0 +1,48 @@
# Feature: Finale Ablage in Jahresordnern
Als Nutzer möchte ich, dass verarbeitete Dokumente unverändert und nachvollziehbar benannt
in einer Jahresstruktur abgelegt werden und über die UUID im Dateinamen jederzeit
ihrem Kontext in der Datenbank zuzuordnen sind.
```gherkin
Funktionalität: Finale Ablage
Szenario: Verarbeitetes Dokument landet im Jahresordner
Angenommen ein am 2026-07-12 importiertes Dokument
Wenn die File-Stage läuft
Dann liegt die Datei unter archive/2026/
Und ein Event "DocumentFiled" mit dem relativen archive_path wird aufgezeichnet
Und die Datei liegt nicht mehr im Staging
Szenario: Dateiname enthält UUID und optional den Importzeitstempel
Angenommen die Konfiguration filename_timestamp=true
Wenn ein Dokument "Rechnung Stadtwerke.pdf" abgelegt wird
Dann entspricht der Dateiname dem Muster {JJJJMMTT-HHMMSS}_rechnung-stadtwerke_{uuid}.pdf
Und bei filename_timestamp=false entfällt nur das Zeitstempel-Präfix
Szenario: Jahresordner folgt dem Importzeitpunkt
Angenommen ein Dokument mit LLM-erkanntem doc_date "2019-03-01", importiert 2026
Wenn es abgelegt wird
Dann liegt es in archive/2026/ (Importjahr, nicht Dokumentjahr)
Szenario: Ablage über Volume-Grenzen ist atomar sicher
Angenommen Staging und Archiv liegen auf unterschiedlichen Dateisystemen
Wenn die File-Stage läuft
Dann wird per Kopie mit fsync und anschließendem Rename verschoben
Und zu keinem Zeitpunkt ist im Archiv eine unvollständige Datei sichtbar
Szenario: Datei ist über die API abrufbar
Angenommen ein abgelegtes Dokument
Wenn GET /v1/documents/{id}/file aufgerufen wird
Dann wird die unveränderte Originaldatei mit korrektem Content-Type gestreamt
```
## Traceability
| Szenario | Go-Test (Pakete `internal/storage`, `internal/pipeline`, `internal/api`) |
|---|---|
| Verarbeitetes Dokument landet im Jahresordner | `TestFileStage_MovesToYearFolder` |
| Dateiname enthält UUID und optional den Importzeitstempel | `TestFiler_FilenamePatternWithUUIDAndTimestamp` |
| Jahresordner folgt dem Importzeitpunkt | `TestFiler_YearFromImportDateNotDocDate` |
| Ablage über Volume-Grenzen ist atomar sicher | `TestStorage_CrossDeviceMoveIsAtomic` |
| Datei ist über die API abrufbar | `TestGetFile_StreamsOriginal` |
@@ -0,0 +1,61 @@
# Feature: Nachvollziehbarkeit, Event Sourcing und Wiederaufnahme
Als Betreiber möchte ich jede Zustandsänderung eines Dokuments lückenlos nachvollziehen,
den Zustand aus den Ereignissen rekonstruieren und fehlgeschlagene Verarbeitung
gezielt wiederaufnehmen können.
```gherkin
Funktionalität: Event Sourcing und Audit-Trail
Szenario: Jede Zustandsänderung ist als Event nachvollziehbar
Angenommen ein vollständig verarbeitetes Dokument
Wenn GET /v1/documents/{id}/events aufgerufen wird
Dann enthält die Antwort die lückenlose Eventfolge von DocumentReceived bis DocumentFiled
Und jedes Event enthält seq, event_type, payload und created_at
Szenario: Der Zustand ist aus den Events rekonstruierbar
Angenommen die aufgezeichnete Eventfolge eines Dokuments
Wenn die Events per Replay gefaltet werden
Dann entspricht das Ergebnis exakt der documents-Projektion
Szenario: Konkurrierende Änderungen werden erkannt
Angenommen zwei Schreiber mit derselben erwarteten Version eines Aggregats
Wenn beide Events anhängen wollen
Dann gelingt genau ein Append und der zweite erhält einen Konsistenzfehler
Szenario: Events und Projektion sind transaktional konsistent
Angenommen ein Append von Events schlägt in der Projektion fehl
Wenn die Transaktion zurückgerollt wird
Dann sind weder Events noch Projektionsänderung gespeichert
Funktionalität: Wiederaufnahme nach Fehlern
Szenario: Neustart verliert keine Arbeit
Angenommen ein Dokument war beim Absturz des Systems in Verarbeitung
Wenn das System neu startet und der Claim-Lease abläuft
Dann wird die Verarbeitung automatisch wiederaufgenommen
Szenario: Wiederaufnahme setzt an der richtigen Stelle an
Angenommen ein Dokument mit fehlgeschlagener OCR-Stage und Status "failed"
Wenn POST /v1/documents/{id}/reprocess aufgerufen wird
Dann wird ein Event "RetryRequested" aufgezeichnet
Und die Verarbeitung setzt bei der OCR-Stage wieder an
Und bereits abgeschlossene Stages laufen nicht erneut
Szenario: Wiederaufnahme während laufender Verarbeitung wird abgelehnt
Angenommen ein Dokument, das aktuell von einem Worker geclaimt ist
Wenn POST /v1/documents/{id}/reprocess aufgerufen wird
Dann antwortet das System mit 409
```
## Traceability
| Szenario | Go-Test (Pakete `internal/events`, `internal/pipeline`, `internal/api`) |
|---|---|
| Jede Zustandsänderung ist als Event nachvollziehbar | `TestGetEvents_ReturnsCompleteTrail` |
| Der Zustand ist aus den Events rekonstruierbar | `TestReplay_MatchesProjection` |
| Konkurrierende Änderungen werden erkannt | `TestAppend_ConcurrencyConflictReturnsError` |
| Events und Projektion sind transaktional konsistent | `TestAppend_RollbackLeavesNoPartialState` |
| Neustart verliert keine Arbeit | `TestQueue_ExpiredLeaseIsReclaimed` |
| Wiederaufnahme setzt an der richtigen Stelle an | `TestReprocess_ResumesFromFailedStage` |
| Wiederaufnahme während laufender Verarbeitung wird abgelehnt | `TestReprocess_ClaimedDocumentReturns409` |
+27
View File
@@ -0,0 +1,27 @@
# Anforderungen als Gherkin-Szenarien
Anforderungen werden als Gherkin-Szenarien (deutsche Schlüsselwörter: *Angenommen / Wenn / Dann*)
dokumentiert und im Sinne von TDD umgesetzt (DEC-REQ-01, siehe [architecture.md](../architecture.md)):
1. **Szenario schreiben** (hier, vor der Implementierung)
2. **Roten Test schreiben** — Stdlib `testing`, Testname = Szenario-Referenz aus der Traceability-Tabelle
3. **Implementieren**, bis der Test grün ist
Regeln:
- Jedes Szenario hat genau einen gleichnamigen Go-Test; die Zuordnung steht in der
Traceability-Tabelle am Ende jeder Datei.
- Wird ein Test geändert, wird das Szenario im selben Commit nachgezogen (Drift-Kontrolle).
- Szenarien beschreiben fachliches Verhalten, keine Implementierungsdetails.
## Übersicht
| Datei | Feature |
|---|---|
| [01-import-api.md](01-import-api.md) | Dokumenten-Upload über die API |
| [02-import-ordner.md](02-import-ordner.md) | Automatischer Import aus dem überwachten Ordner |
| [03-textextraktion-ocr.md](03-textextraktion-ocr.md) | Textextraktion und OCR (PDF + Bilder), Unversehrtheit der Originale |
| [04-llm-anreicherung.md](04-llm-anreicherung.md) | Optionale LLM-Anreicherung |
| [05-volltextsuche.md](05-volltextsuche.md) | Volltextsuche und Index-Rebuild |
| [06-ablage.md](06-ablage.md) | Finale Ablage in Jahresordnern |
| [07-nachvollziehbarkeit.md](07-nachvollziehbarkeit.md) | Event Sourcing, Audit-Trail, Wiederaufnahme |