# go-documenter — Architekturdokumentation (arc42) > Detaildokument zur Architektur. Das [README](../README.md) bleibt der fachliche Überblick; > dieses Dokument konkretisiert Bausteine, Abläufe, Technologien und Entscheidungen (DEC-*). > Anforderungen als testbare Gherkin-Szenarien: [doc/requirements/](requirements/). > API-Vertrag: [doc/openapi.yaml](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 ```mermaid flowchart LR client["API-Client
(curl, Skripte, späteres Frontend)"] user["Nutzer
(legt Dateien ab)"] llm["LLM-Dienst
OpenAI-kompatibel
(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 in `doc/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`/`tesseract` sind 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 ```mermaid flowchart TB subgraph api_layer["API-Schicht"] api["internal/api
Router, Handler, Auth-Middleware"] end subgraph app["Anwendungslogik"] ingest["internal/ingest
Aufnahme: UUID, Hash, Dedup, Staging"] pipeline["internal/pipeline
Runner, Queue, Stages"] importer["internal/importer
fsnotify + Rescan + Stabilität"] end subgraph domain_layer["Domäne"] domain["internal/domain
Document, Status, MediaType"] events["internal/events
Event-Store, Replay, Projektion"] end subgraph infra["Infrastruktur"] db["internal/db
SQLite, Schema, Stores"] search["internal/search
FTS5-Indexer/Searcher"] storage["internal/storage
Datei-Moves, Jahresordner, Hash"] pdf["internal/pdf
pdftotext"] ocr["internal/ocr
ocrmypdf / tesseract"] llm["internal/llm
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) ```mermaid 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 { <> +Append(ctx, aggregateID, expectedVersion, payloads) error +Load(ctx, aggregateID) List~Event~ } class Stage { <> +Name() string +Skip(doc Document) bool +Run(ctx, doc Document) List~Payload~ } class Queue { <> +Enqueue(ctx, documentID, stage) error +Claim(ctx, workerID) *Work +Complete(ctx, documentID) error +Defer(ctx, documentID, notBefore) error } class TextExtractor { <> +Extract(ctx, path) ExtractResult } class OCRProcessor { <> +Recognize(ctx, path, mediaType) OCRResult } class Enricher { <> +Enrich(ctx, text) Enrichment } class Indexer { <> +Index(ctx, doc Document) error +Rebuild(ctx) error } class Filer { <> +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 ```mermaid 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 ```mermaid 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
in EINER Transaktion I->>Q: Enqueue(uuid, next_stage=extract) end ``` ### 6.2 Verarbeitung durch die Pipeline (Worker) ```mermaid 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
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:
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 ```mermaid 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
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 ```mermaid 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) ```mermaid 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
(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:
erste Stage der kanonischen Reihenfolge,
deren Ergebnis fehlt — fertige Stages laufen nie erneut R->>R: Pipeline ab OCR fortsetzen ``` ## 7. Verteilungssicht ```mermaid flowchart TB subgraph vserver["Strato VServer (Ubuntu 24.04)"] caddy["Caddy
:80/:443, TLS/ACME"] subgraph compose["/opt/documenter (Docker Compose)"] app["documenter-api-1
:9060 intern
debian-slim + ocrmypdf/tesseract/poppler/libheif"] vol1[("config/")] vol2[("data/ — SQLite, Staging, Logs")] vol3[("import/")] vol4[("archive/")] end caddy -- "proxy-Netzwerk
(Host-Header-Routing)" --> app app --- vol1 & vol2 & vol3 & vol4 end subgraph xps["XPS15 (NetBird-VPN, ggf. offline)"] localai["LocalAI :8080
OpenAI-kompatibel"] end subgraph git2["git2.adelante-it.de"] gitea["Gitea: Repo + Registry + Actions
act_runner (go-ci)"] end app -. "Chat-Completions
(optional)" .-> localai gitea -- "Release: Image push,
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) → Runtime `debian:trixie-slim` mit `ocrmypdf 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: 1. **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. 2. **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 `EnrichmentSkipped` und die Pipeline läuft weiter — ein offlines LLM blockiert nie die Ablage - Nicht-retryable Fehler (korrupte Datei, passwortgeschütztes PDF): Status `failed`, manuell per `POST /v1/documents/{id}/reprocess` wiederaufnehmbar ### 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 über `document_id` ### 8.7 Testbarkeit und TDD (DEC-REQ-01) - Anforderungen als **Gherkin-Szenarien** in [doc/requirements/](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 über `httptest.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 mit `crypto/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 |