Files
Christoph Kroczek 6e6cb0dc8d 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>
2026-07-13 15:20:48 +02:00

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 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

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) → 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/, 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