- 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>
26 KiB
go-documenter — Architekturdokumentation (arc42)
Detaildokument zur Architektur. Das README bleibt der fachliche Überblick; dieses Dokument konkretisiert Bausteine, Abläufe, Technologien und Entscheidungen (DEC-*). Anforderungen als testbare Gherkin-Szenarien: doc/requirements/. API-Vertrag: doc/openapi.yaml.
1. Einführung und Ziele
Schlankes, serverseitiges Dokumentenmanagement-System (Single-User) in Go:
- Import von PDF- und Bilddateien (JPG, PNG, HEIC) über API oder überwachten Ordner
- Verarbeitung: Textextraktion, OCR, optionale LLM-Anreicherung, Volltextindexierung
- Finale Ablage in Jahresordnern; Originaldateien werden niemals inhaltlich verändert
- Nachvollziehbarkeit aller Zustandsänderungen durch Event Sourcing
- API-first: kein Frontend in der ersten Ausbaustufe
Qualitätsziele (Rangfolge): Einfachheit, Testbarkeit, Nachvollziehbarkeit, Zuverlässigkeit, Erweiterbarkeit.
2. Randbedingungen
| Randbedingung | Ausprägung |
|---|---|
| Sprache | Go (~1.26) |
| Betrieb | Eigener VServer (Strato, 8 GB RAM) hinter Caddy-Reverse-Proxy, Docker Compose |
| CI/CD | Gitea + Actions (git2.adelante-it.de, go-ci-Runner-Image, gespiegelte Actions) |
| Vorgehen | Dokumentation vor Implementierung (dieses Dokument ist das Gate), TDD über Gherkin-Szenarien |
| Dokumentation | arc42, deutsch; Diagramme in Mermaid (DEC-DOC-01) |
| Referenzprojekte | pamietnik (Go-Konventionen), infranas (Betriebsumgebung) |
3. Kontextabgrenzung
flowchart LR
client["API-Client<br/>(curl, Skripte, späteres Frontend)"]
user["Nutzer<br/>(legt Dateien ab)"]
llm["LLM-Dienst<br/>OpenAI-kompatibel<br/>(LocalAI via VPN oder Cloud)"]
subgraph host["Server (Docker)"]
sys["go-documenter"]
import["import/ (überwacht)"]
archive["archive/{Jahr}/"]
data["data/ (SQLite, Staging, Logs)"]
end
client -- "HTTPS + Bearer-Token" --> sys
user -- "Datei speichern" --> import
sys -- "liest / verschiebt" --> import
sys -- "legt ab (unverändert)" --> archive
sys -- "Events, Projektion, FTS-Index" --> data
sys -- "Chat-Completions (optional)" --> llm
Externe Schnittstellen:
- API-Clients: REST/JSON, Bearer-Token (
API_TOKEN), Vertrag indoc/openapi.yaml - Dateisystem:
import/(Eingang),data/staging/(Verarbeitung),archive/(Ablage) - LLM: OpenAI-kompatible Chat-Completions-API; erreichbar oder nicht — das System funktioniert ohne
- OCR: keine externe Schnittstelle —
ocrmypdf/tesseractsind im Container gebündelt
4. Lösungsstrategie
| Bereich | Wahl | Begründung |
|---|---|---|
| HTTP | github.com/go-chi/chi/v5 |
minimal, stdlib-kompatibel, pamietnik-Standard |
| Persistenz | modernc.org/sqlite (pure Go) |
eine Datei, WAL, kein CGo, kein DB-Server |
| Volltextsuche | SQLite FTS5 (external content, BM25) | transaktional konsistent mit den Daten, kein Zweitsystem |
| OCR | ocrmypdf --sidecar --output-type none (PDF), tesseract (Bilder) |
nur Text wird gewonnen, Original bleibt byte-identisch |
| PDF-Text | pdftotext (poppler) via exec.Command |
beste Extraktionsqualität, Interface-gekapselt |
| LLM | eigener Minimal-Client (Stdlib-HTTP) | LocalAI ↔ Cloud nur per Config umschaltbar, kein SDK |
| Event Sourcing | handgerollt, nur Dokument-Aggregat | erfüllt Nachvollziehbarkeit ohne Framework-Komplexität |
| Watcher | fsnotify + periodischer Rescan |
Rescan fängt verpasste Events und Startbestand |
| Config | YAML + Env-Override (Secrets nur Env) | pamietnik-Muster |
| Logging | log/slog JSON + lumberjack-Rotation |
strukturiert, pamietnik-Muster |
| Tests | nur Stdlib testing, handgeschriebene Fakes |
keine Mock-Frameworks, Interfaces als Test-Seams |
Vollständige Dependency-Liste: chi/v5, modernc.org/sqlite, fsnotify, google/uuid, yaml.v3, lumberjack.v2. Sonst nichts.
5. Bausteinsicht
5.1 Überblick
flowchart TB
subgraph api_layer["API-Schicht"]
api["internal/api<br/>Router, Handler, Auth-Middleware"]
end
subgraph app["Anwendungslogik"]
ingest["internal/ingest<br/>Aufnahme: UUID, Hash, Dedup, Staging"]
pipeline["internal/pipeline<br/>Runner, Queue, Stages"]
importer["internal/importer<br/>fsnotify + Rescan + Stabilität"]
end
subgraph domain_layer["Domäne"]
domain["internal/domain<br/>Document, Status, MediaType"]
events["internal/events<br/>Event-Store, Replay, Projektion"]
end
subgraph infra["Infrastruktur"]
db["internal/db<br/>SQLite, Schema, Stores"]
search["internal/search<br/>FTS5-Indexer/Searcher"]
storage["internal/storage<br/>Datei-Moves, Jahresordner, Hash"]
pdf["internal/pdf<br/>pdftotext"]
ocr["internal/ocr<br/>ocrmypdf / tesseract"]
llm["internal/llm<br/>OpenAI-kompatibler Client"]
config["internal/config"]
end
api --> ingest
api --> db
api --> search
api --> events
importer --> ingest
ingest --> events
ingest --> storage
ingest --> pipeline
pipeline --> events
pipeline --> pdf
pipeline --> ocr
pipeline --> llm
pipeline --> search
pipeline --> storage
events --> db
search --> db
Abhängigkeitsregel: domain und events importieren keine Infrastruktur. Stages und Handler
hängen nur von schmalen Interfaces ab; die konkreten Implementierungen (exec, SQLite, HTTP)
werden in cmd/server/main.go verdrahtet.
5.2 Pakete
| Paket | Verantwortung |
|---|---|
cmd/server |
Wiring: Config → Logger → DB → Stores → Worker + Importer → HTTP-Server; graceful shutdown |
internal/domain |
Reine Typen: Document, Status-Konstanten, MediaType — keine Abhängigkeiten |
internal/events |
Event-Envelope + Payload-Structs, Store-Interface, SQLite-Store (Append + synchrone Projektion in einer Transaktion), Replay |
internal/pipeline |
Stage-Interface, kanonische Reihenfolge, Worker-Loop, DB-backed Work-Queue mit Lease |
internal/ingest |
Gemeinsame Aufnahme für API und Ordner: UUID, SHA-256, Dedup, Staging, DocumentReceived, Enqueue |
internal/pdf |
TextExtractor-Interface + pdftotext-Implementierung, needs_ocr-Heuristik |
internal/ocr |
Processor-Interface; ocrmypdf-Sidecar (PDF), tesseract (Bilder), HEIC→temp-PNG |
internal/llm |
Client-Interface, OpenAI-kompatible Implementierung, Enricher (Prompt + JSON-Parsing) |
internal/search |
Indexer/Searcher-Interfaces, FTS5-Implementierung inkl. Rebuild |
internal/importer |
Ordnerüberwachung: fsnotify, Stabilitäts-Detektor, periodischer Rescan |
internal/storage |
Datei-Lebenszyklus: Staging-Pfade, Jahresordner, Umbenennung, Hash, atomare Moves — nie Inhaltsänderung |
internal/api |
NewRouter(RouterDeps), Handler je Ressource, Bearer-Token-Middleware, writeError/writeJSON |
internal/db |
Open (WAL, busy_timeout, foreign_keys), idempotentes Schema (go:embed), Store-Interfaces + Sentinel-Errors |
internal/config |
YAML + Env-Override; Secrets ausschließlich per Env |
5.3 Zentrale Typen (Klassendiagramm)
classDiagram
class Document {
+string ID «UUIDv4»
+Status Status
+string OriginalName
+MediaType MediaType
+string Origin «api | import_folder»
+string ContentHash «SHA-256»
+int64 SizeBytes
+int PageCount
+string StoragePath
+string ArchivePath
+string Text
+bool NeedsOCR
+string Title
+string Summary
+List~string~ Tags
+string DocDate
+string Language
+bool Enriched
+string FailedStage
+string LastError
+int Attempts
+int Version «Seq des letzten Events»
}
class Event {
+string AggregateID
+int Seq
+string Type
+json.RawMessage Payload
+time.Time CreatedAt
}
class Store {
<<interface>>
+Append(ctx, aggregateID, expectedVersion, payloads) error
+Load(ctx, aggregateID) List~Event~
}
class Stage {
<<interface>>
+Name() string
+Skip(doc Document) bool
+Run(ctx, doc Document) List~Payload~
}
class Queue {
<<interface>>
+Enqueue(ctx, documentID, stage) error
+Claim(ctx, workerID) *Work
+Complete(ctx, documentID) error
+Defer(ctx, documentID, notBefore) error
}
class TextExtractor {
<<interface>>
+Extract(ctx, path) ExtractResult
}
class OCRProcessor {
<<interface>>
+Recognize(ctx, path, mediaType) OCRResult
}
class Enricher {
<<interface>>
+Enrich(ctx, text) Enrichment
}
class Indexer {
<<interface>>
+Index(ctx, doc Document) error
+Rebuild(ctx) error
}
class Filer {
<<interface>>
+File(ctx, doc Document) string «archive_path»
}
Store ..> Event
Store ..> Document : Replay(events) → Document
Stage ..> Document
Stage <|.. ExtractStage
Stage <|.. OCRStage
Stage <|.. EnrichStage
Stage <|.. IndexStage
Stage <|.. FileStage
ExtractStage --> TextExtractor
OCRStage --> OCRProcessor
EnrichStage --> Enricher
IndexStage --> Indexer
FileStage --> Filer
5.4 Event-Modell
| Event | Payload (snake_case) | Wirkung im Replay |
|---|---|---|
DocumentReceived |
original_name, media_type, origin, content_hash, size_bytes, staging_path, imported_at | Aggregat entsteht, Status received |
ProcessingStarted |
attempt | Status processing |
TextExtracted |
text, page_count, char_count, method, needs_ocr | Text/Seiten gesetzt |
OCRCompleted |
text, engine, languages, duration_ms | Text gesetzt, Status ocr_done |
DocumentEnriched |
title, summary, tags[], doc_date, language, model, prompt_version | Metadaten gesetzt, Status enriched |
EnrichmentSkipped |
reason, attempts | enriched=false, Pipeline läuft weiter |
DocumentIndexed |
char_count, indexed_at | Status indexed |
DocumentFiled |
archive_path, filed_at, timestamp_applied | Status filed (Endzustand) |
ProcessingFailed |
stage, attempt, error, retryable | Status failed, FailedStage/LastError gesetzt |
RetryRequested |
requested_by, from_stage | Status processing, Wiederaufnahme |
Replay(events) → Document ist eine pure Fold-Funktion — der Zustand ist vollständig aus
Events rekonstruierbar. Der Volltext liegt im Event-Payload (DEC-ES-02), damit auch die
Projektion und der Suchindex jederzeit aus dem Event-Store neu aufgebaut werden können.
5.5 Statusmodell
stateDiagram-v2
[*] --> received : DocumentReceived
received --> processing : ProcessingStarted
processing --> ocr_done : OCRCompleted
processing --> enriched : DocumentEnriched
ocr_done --> enriched : DocumentEnriched
processing --> indexed : DocumentIndexed (Enrich übersprungen)
ocr_done --> indexed : DocumentIndexed (Enrich übersprungen)
enriched --> indexed : DocumentIndexed
indexed --> filed : DocumentFiled
processing --> failed : ProcessingFailed
ocr_done --> failed : ProcessingFailed
enriched --> failed : ProcessingFailed
indexed --> failed : ProcessingFailed
failed --> processing : RetryRequested
filed --> [*]
6. Laufzeitsicht
6.1 Import über überwachten Ordner
sequenceDiagram
actor N as Nutzer
participant W as importer (Watcher)
participant I as ingest
participant S as storage
participant E as events (Store)
participant Q as work_queue
N->>W: Datei in import/ speichern
W->>W: fsnotify-Event → Datei "pending"
loop bis Größe+mtime stable_seconds unverändert
W->>W: Stabilität prüfen
end
W->>I: Ingest(pfad, origin=import_folder)
I->>S: SHA-256 berechnen (streaming)
I->>E: Dedup-Check (content_hash)
alt Duplikat
I->>S: Datei nach import/rejected/ verschieben
I-->>W: Warnung geloggt, kein Aggregat
else neu
I->>I: UUID vergeben
I->>S: Move nach data/staging/{uuid}.{ext}
I->>E: Append(DocumentReceived)
Note over E: Event + documents-Projektion<br/>in EINER Transaktion
I->>Q: Enqueue(uuid, next_stage=extract)
end
6.2 Verarbeitung durch die Pipeline (Worker)
sequenceDiagram
participant Q as work_queue
participant R as pipeline (Runner)
participant E as events (Store)
participant X as ExtractStage (pdftotext)
participant O as OCRStage (ocrmypdf/tesseract)
participant L as EnrichStage (LLM)
participant F as IndexStage / FileStage
loop Poll alle 2 s
R->>Q: Claim (Lease 15 min)
end
Q-->>R: document_id, next_stage
R->>E: Load + Replay → Document
alt Stage = extract (nur lesend!)
R->>X: Run(doc)
X-->>R: TextExtracted{needs_ocr}
else Stage = ocr (Original bleibt byte-identisch)
R->>O: Run(doc)
Note over O: PDF: ocrmypdf --sidecar --output-type none<br/>Bild: tesseract direkt (HEIC → temp-PNG)
O-->>R: OCRCompleted{text}
else Stage = enrich (optional)
R->>L: Run(doc)
alt LLM erreichbar
L-->>R: DocumentEnriched{title, tags, …}
else LLM offline
L-->>R: Fehler (retryable)
R->>E: Append(ProcessingFailed{retryable})
R->>Q: Defer(not_before = now + Backoff)
Note over R,Q: nach max. Versuchen:<br/>EnrichmentSkipped → weiter zu index
end
else Stage = index / file
R->>F: Run(doc)
F-->>R: DocumentIndexed / DocumentFiled{archive_path}
end
R->>E: Append(Events, expectedVersion)
R->>Q: next_stage weiterschalten bzw. Complete bei filed
6.3 Import über API
sequenceDiagram
actor C as API-Client
participant A as api (Handler)
participant I as ingest
participant Q as work_queue
C->>A: POST /v1/documents (multipart, Bearer-Token)
A->>A: Auth-Middleware (constant-time Vergleich)
A->>I: Ingest(stream, origin=api)
Note over I: Stream → staging/{uuid}.{ext}.part<br/>TeeReader → SHA-256, dann Rename
alt Duplikat
I-->>A: ErrDuplicate + existing_id
A-->>C: 409 {error, existing_document_id}
else neu
I->>Q: DocumentReceived + Enqueue
A-->>C: 202 {id, status: received}
end
Note over C: weiterer Ablauf identisch zu 6.2
6.4 Suche
sequenceDiagram
actor C as API-Client
participant A as api
participant S as search (FTS5)
C->>A: GET /v1/search?q=rechnung+stadtwerke
A->>S: Search(q, limit, offset)
S->>S: MATCH + bm25() + snippet()
S-->>A: [{id, title, snippet, rank}, …]
A-->>C: 200 {results, total}
6.5 Fehlerfall und Wiederaufnahme (Retry/Resume)
sequenceDiagram
actor C as API-Client
participant A as api
participant E as events
participant Q as work_queue
participant R as Runner
Note over E: Dokument steht auf failed<br/>(ProcessingFailed{stage=ocr, retryable=false})
C->>A: POST /v1/documents/{id}/reprocess
A->>Q: geclaimt? → 409, sonst weiter
A->>E: Append(RetryRequested{from_stage})
A->>Q: Enqueue(id)
A-->>C: 202
R->>E: Load + Replay
Note over R: Resume-Stage ableitbar aus Zustand:<br/>erste Stage der kanonischen Reihenfolge,<br/>deren Ergebnis fehlt — fertige Stages laufen nie erneut
R->>R: Pipeline ab OCR fortsetzen
7. Verteilungssicht
flowchart TB
subgraph vserver["Strato VServer (Ubuntu 24.04)"]
caddy["Caddy<br/>:80/:443, TLS/ACME"]
subgraph compose["/opt/documenter (Docker Compose)"]
app["documenter-api-1<br/>:9060 intern<br/>debian-slim + ocrmypdf/tesseract/poppler/libheif"]
vol1[("config/")]
vol2[("data/ — SQLite, Staging, Logs")]
vol3[("import/")]
vol4[("archive/")]
end
caddy -- "proxy-Netzwerk<br/>(Host-Header-Routing)" --> app
app --- vol1 & vol2 & vol3 & vol4
end
subgraph xps["XPS15 (NetBird-VPN, ggf. offline)"]
localai["LocalAI :8080<br/>OpenAI-kompatibel"]
end
subgraph git2["git2.adelante-it.de"]
gitea["Gitea: Repo + Registry + Actions<br/>act_runner (go-ci)"]
end
app -. "Chat-Completions<br/>(optional)" .-> localai
gitea -- "Release: Image push,<br/>Deploy: compose pull && up -d" --> compose
- Keine veröffentlichten Container-Ports; Caddy erreicht den Container über das externe
proxy-Netzwerk - Secrets (
API_TOKEN,LLM_API_KEY) via/opt/documenter/.env, nie im Repo - Dockerfile: Builder
golang:1.26-alpine(CGO_ENABLED=0) → Runtimedebian:trixie-slimmitocrmypdf tesseract-ocr-deu tesseract-ocr-eng poppler-utils libheif-examples, non-root (DEC-RUNTIME-01) - Offener Punkt: Erreichbarkeit von LocalAI (VPN-Adresse) aus dem Container früh verifizieren
8. Querschnittliche Konzepte
8.1 Speicherkonzept (DEC-STORAGE-01)
Genau zwei Speicherorte, keine weiteren Systeme:
- Eine SQLite-Datei (
data/documenter.db): Event-Store (Quelle der Wahrheit),documents-Projektion (Read Model), FTS5-Index, Work-Queue. Eine Datei = eine Backup-Einheit; transaktionale Konsistenz gratis; WAL für parallele API-/Worker-Zugriffe. - Plain-Dateisystem:
import/(+rejected/),data/staging/,archive/{JJJJ}/. Keine Blobs in der DB — Dateien bleiben normale, menschenlesbare Dateien.
Robustheit:
- Originale byte-identisch; nur
os.Rename(cross-device: copy+fsync+rename),.part-Suffix während Schreibvorgängen — nie halbe Dateien sichtbar - Verknüpfung Datei↔Kontext doppelt: UUID im Dateinamen (Datei→DB) + SHA-256 in der DB (DB→Datei)
- Gestaffelte Wiederherstellung: FTS kaputt → Rebuild-API; Projektion kaputt → Replay aus Events; DB weg → Dateien bleiben lesbar und über die UUID identifizierbar
- Backup: genau zwei Verzeichnisse (
data/+archive/) per rsync/Snapshot
8.2 Unveränderlichkeit der Originale (DEC-IMMUTABLE-01)
Dateien werden ab Aufnahme nur gelesen, verschoben und umbenannt — nie inhaltlich verändert.
OCR erzeugt ausschließlich Sidecar-Text (ocrmypdf --output-type none bzw. tesseract-Ausgabe);
es wird kein Textlayer eingebettet, keine Bild→PDF-Konvertierung für die Ablage vorgenommen.
Kontext (Text, Tags, Titel, Events) lebt vollständig extern in der DB.
Verifikation: SHA-256 vor Import == SHA-256 der archivierten Datei (fester Testfall).
8.3 Datei-Lebenszyklus und Benennung (DEC-NAME-01, DEC-YEAR-01, DEC-DUP-01)
- Ablagepfad:
archive/{JJJJ}/{JJJJMMTT-HHMMSS_}bereinigter-name_{uuid}.{ext} - Jahr = Importzeitpunkt (deterministisch, LLM-unabhängig); Zeitstempel-Präfix per Config
- Dedup per SHA-256 (UNIQUE-Index): API →
409+existing_document_id; Watcher →import/rejected/ - Grenze: erneuter Scan desselben Papierdokuments erzeugt andere Bytes und wird nicht erkannt
8.4 Ereignisorientierung und Konsistenz (DEC-ES-01, DEC-ES-02)
Event Sourcing nur für das Dokument-Aggregat, handgerollt (~200 Zeilen):
append-only events-Tabelle, PRIMARY KEY(aggregate_id, seq) als optimistische
Nebenläufigkeitskontrolle, synchrone Projektion in derselben Transaktion — das Read
Model ist nie stale. Retries und Übersprünge sind selbst Events (RetryRequested,
EnrichmentSkipped), die Wiederaufnahme-Stage ist rein aus dem Replay-Zustand ableitbar.
8.5 Fehlerbehandlung und Wiederanlauf (DEC-QUEUE-01)
- DB-backed Work-Queue mit Lease (15 min): Neustart- und Crash-sicher, verwaiste Claims werden übernommen; 1 Worker (konfigurierbar), Poll-Intervall 2 s
- Retryable Fehler (LLM offline, OCR-Timeout): exponentieller Backoff 30s·2ⁿ (Cap 1 h), max. Versuche konfigurierbar (Default 10)
- Enrichment ist optional: nach erschöpften Versuchen
EnrichmentSkippedund die Pipeline läuft weiter — ein offlines LLM blockiert nie die Ablage - Nicht-retryable Fehler (korrupte Datei, passwortgeschütztes PDF): Status
failed, manuell perPOST /v1/documents/{id}/reprocesswiederaufnehmbar
8.6 Logging
log/slog, JSON-Handler; stdout + optional Datei mit lumberjack-Rotation (max_size_mb/max_backups/max_age_days); Level per Config/Env- HTTP-Zugriffe über chi-Middleware; jede Pipeline-Stage loggt strukturiert:
slog.Info("stage done", "document_id", id, "stage", name, "duration_ms", ms) - Fehler immer mit
"err","stage","attempt"— Korrelation überdocument_id
8.7 Testbarkeit und TDD (DEC-REQ-01)
- Anforderungen als Gherkin-Szenarien in doc/requirements/, je Szenario ein gleichnamiger Stdlib-Test; Traceability-Tabelle je Datei. Workflow: Szenario → roter Test → Implementierung
- Nur Stdlib
testing, table-driven, handgeschriebene In-Memory-Fakes; Interfaces als Seams - exec-basierte Komponenten (ocrmypdf, pdftotext, tesseract) über injizierbaren
runner func(ctx, name, args...)-Seam testbar; LLM überhttptest.Server - Echte SQLite (in-memory/tmpfile) inkl. FTS5 in DB- und Integrationstests
- Zwei Integrationstests im normalen
go test ./...: API-Upload →filed+ suchbar; Ordner-Import → gleiches Ergebnis
8.8 Sicherheit (DEC-AUTH-01)
- Statischer Bearer-Token (
API_TOKEN, nur Env), Vergleich mitcrypto/subtle.ConstantTimeCompare /healthzöffentlich, alles andere tokenpflichtig; TLS terminiert Caddy- Upload-Limit per Config (Default 100 MB); nur Whitelist-Endungen (
pdf|jpg|jpeg|png|heic)
9. Architekturentscheidungen (DEC-*)
| ID | Entscheidung | Begründung / Trade-off |
|---|---|---|
| DEC-STACK-01 | Go + chi + SQLite + slog, pamietnik-Konventionen | bewährt, minimal, ein Binary; bewusst kein Framework |
| DEC-DB-01 | modernc.org/sqlite, kein DB-Server |
pure Go, eine Datei; Grenze: eine Schreib-Verbindung (für Single-User irrelevant) |
| DEC-FTS-01 | SQLite FTS5 external-content, Sync explizit in Go, Rebuild per API | kein Zweitsystem; offen: trigram-Tokenizer, falls Recall bei deutschen Komposita schlecht |
| DEC-ES-01 | Event Sourcing nur fürs Dokument-Aggregat, synchrone Projektion | volle Nachvollziehbarkeit ohne ES-Framework; keine Eventual-Consistency-Probleme |
| DEC-ES-02 | Volltext im Event-Payload | vollständige Rekonstruierbarkeit; DB-Wachstum durch Upload-Limit + Text-Cap begrenzt |
| DEC-QUEUE-01 | DB-backed Work-Claim-Queue mit Lease statt Go-Channels | Neustart-/Crash-sicher, kein Verlust; Trade-off: Poll-Latenz ≤ 2 s (akzeptiert) |
| DEC-IMMUTABLE-01 | Originale nie verändern; OCR nur als Sidecar-Text; Kontext extern | Hashes/Signaturen bleiben gültig, keine Render-Regressionen; Trade-off: archivierte PDFs haben keinen eingebetteten Textlayer — Suche läuft immer über das System |
| DEC-MEDIA-01 | PDF und Bilder (JPG/PNG/HEIC) in identischer Pipeline | ein Ablauf, eine Ablage; HEIC nur temporär für OCR konvertiert |
| DEC-NAME-01 | UUID im Archiv-Dateinamen + SHA-256 in DB | bidirektionaler, DB-unabhängiger Link; Trade-off: längere Dateinamen |
| DEC-YEAR-01 | Jahresordner aus Importdatum, nicht LLM-doc_date |
deterministisch, funktioniert ohne LLM |
| DEC-DUP-01 | Dedup via SHA-256, Duplikate abgelehnt | einfach, exakt; erkennt keine Re-Scans desselben Papiers |
| DEC-OCR-01 | ocrmypdf (PDF) / tesseract (Bilder) via exec hinter Interface | beste Qualität, ein Container; Trade-off: ~600-MB-Image |
| DEC-EXTRACT-01 | pdftotext (poppler) via exec statt pure-Go-Lib |
bessere Extraktion; Fat-Image ohnehin nötig |
| DEC-LLM-01 | OpenAI-kompatibler Minimal-Client; Anreicherung optional/retryable/nachholbar | LocalAI ↔ Cloud per Config; offlines LLM blockiert nie |
| DEC-RUNTIME-01 | Runtime-Image debian-slim statt distroless (Abweichung von pamietnik) | ocrmypdf/tesseract/poppler/libheif erforderlich |
| DEC-AUTH-01 | Statischer Bearer-Token, Single-User | ausreichend für v1; Revision bei Frontend/Multi-User |
| DEC-STORAGE-01 | Nur SQLite + Plain-Dateisystem | s. Kap. 8.1 |
| DEC-DOC-01 | Mermaid für alle Diagramme | nativ in Gitea gerendert, diffbar; Trade-off: kein Voll-UML (akzeptiert) |
| DEC-REQ-01 | Gherkin als Doku-Format + Stdlib-Tests (kein godog) | TDD-tauglich ohne Framework; Drift-Kontrolle über Namenskonvention + Traceability-Tabellen |
10. Qualitätsanforderungen
| Qualitätsziel | Konkretisierung / Prüfung |
|---|---|
| Einfachheit | 6 Dependencies, ein Binary, ein Container, eine DB-Datei |
| Testbarkeit | jedes Gherkin-Szenario hat einen benannten Test; go test ./... ohne externe Dienste |
| Nachvollziehbarkeit | GET /v1/documents/{id}/events zeigt lückenlosen fachlichen Verlauf |
| Konsistenz | Events + Projektion + FTS in einer Transaktion; Replay == Projektion (Testfall) |
| Zuverlässigkeit | Crash-Recovery über Queue-Lease; Stabilitäts-Detektor gegen halbe Dateien |
| Unversehrtheit | SHA-256 vor Import == SHA-256 im Archiv (Testfall) |
| Suchbarkeit | jedes verarbeitete Dokument per Inhalt findbar; Rebuild jederzeit möglich |
| Erweiterbarkeit | Frontend später gegen doc/openapi.yaml; neue Stages als weitere Stage-Implementierung |
11. Risiken und offene Punkte
| Risiko / offener Punkt | Einordnung / Maßnahme |
|---|---|
| OCR-Qualität schwankt | Original bleibt erhalten → jederzeit Re-OCR per reprocess möglich |
| LocalAI-Erreichbarkeit aus dem Container (VPN) | früh in M8 verifizieren; ggf. extra_hosts/Host-Forwarding |
| FTS5-Recall bei deutschen Komposita | beobachten; ggf. trigram-Tokenizer (DEC-FTS-01 offen) |
| Passwortgeschützte/korrupte PDFs | non-retryable failed; Szenario definiert |
| RAM bei parallelem OCR (VServer 8 GB) | v1: 1 Worker; OCR-Parallelität erst bei Bedarf |
| Spec-Drift Gherkin ↔ Tests | Namenskonvention + Traceability-Tabelle; optional später Lint-Skript |
| Backup-Automatisierung | offen; Muster: rsync wie pamietnik (infranas) |
| Watcher-Unterordner | v1: nein (nur Top-Level von import/) |
| Domain/Caddyfile-Eintrag | vor M9 festlegen |
12. Glossar
| Begriff | Bedeutung |
|---|---|
| Aggregat | Das Dokument als Einheit der Event-Sourcing-Konsistenz (eine UUID = ein Event-Stream) |
| Projektion / Read Model | documents-Tabelle, abgeleitet aus Events, für Abfragen und Suche |
| Sidecar-Text | OCR-Ausgabe als reine Textdatei, ohne das Quelldokument zu verändern |
| Staging | data/staging/ — Zwischenbereich während der Verarbeitung |
| Stage | Ein Pipeline-Schritt (extract, ocr, enrich, index, file) hinter dem Stage-Interface |
| Work-Queue | DB-Tabelle, aus der der Worker Arbeit per Lease claimt |
| Lease | Zeitlich begrenzter Claim; läuft er ab, übernimmt ein Worker die Arbeit erneut |
| FTS5 | SQLite-Volltextsuchmodul (BM25-Ranking, Snippets) |
| external content | FTS5-Modus, bei dem der Index auf die documents-Tabelle zeigt statt Text zu duplizieren |