Spracherkennung für OCR-Texte und Qualitätsbewertung (Konfidenzwert), um
schlechte OCR-Ergebnisse kenntlich zu machen. Letztes Ticket vor QA-03.
- ocr/language.go: RecognizeWithLanguageAndConfidence erkennt ein Bild
einzeln je Kandidatensprache (deu/eng) im Tesseract-TSV-Modus — die
Sprache mit höherem Konfidenzwert gewinnt, derselbe Lauf liefert den
Konfidenzwert direkt mit.
- search: neue Felder ocr_language/ocr_confidence (Migrationen 0006/0007,
gleiches ALTER-Muster wie SRC-05), in Document/Result gespiegelt.
Client.AttachmentsBelowConfidence filtert gezielt auf niedrige
Konfidenz, schließt Dokumente ohne OCR-Anhang aus.
- Regressionsbug gefunden und behoben: reindex.go (SRC-09) kannte die
neuen OCR-Spalten nicht, Reindex wäre mit "unknown column"
fehlgeschlagen.
Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-10-PRUEFPROTOKOLL.md):
1. TestRecognizeWithLanguageAndConfidence_MultilingualCorpus: deutsches
und englisches Testbild real korrekt als deu/eng erkannt.
2. TestRecognizeWithLanguageAndConfidence_DegradedImageLowersConfidence:
künstliche Verschlechterung senkt Konfidenz real von 91,76 auf 28,21.
3. TestAttachmentsBelowConfidence_QueryReturnsExpectedResults: Abfrage
unterhalb Schwelle liefert real genau die erwarteten 2 von 4 Treffern.
Kein Umbau: Search/Facets/SearchWithFilters/Index/Delete-Verhalten sonst
unverändert, dedup/indexworker/storage/crypto/encstorage/savedsearch
unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
OCR-Pipeline für Bild-/PDF-Anhänge, eigener Mail-OCR-Pfad unabhängig vom
DMS-Board. Direkte Vorbedingung für SRC-10.
- ocr.go: zustandsloses Paket (wie crypto/dedup), kennt weder Mandant
noch Speicher. ExtractTextFromImage ruft tesseract (deu+eng) mit fester
Argumentliste auf. HasTextLayer/ExtractTextFromPDF nutzen pdftotext zur
Erkennung einer vorhandenen Textebene (direkt übernommen, kein
unnötiges OCR) und rastern nur bei fehlender Textebene über pdftoppm
(300dpi) jede Seite für Tesseract.
- Bekannten Fehler vermieden (archivmail-Sprintf-WHERE-Muster): keine
SQL-Klauselbildung, ausschließlich exec.CommandContext mit fester
Argumentliste, keine Shell.
- testpdf_test.go: Testfixtures (Vektor-Text-PDF, Bild-only-PDF mit
eingebettetem JPEG) vollständig in Go erzeugt, keine externe
Bibliothek, keine Testdateien im Repo.
Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-07-PRUEFPROTOKOLL.md):
1. TestExtractTextFromImage_KnownTextRecognized: reales gerastertes Bild,
Text real korrekt erkannt.
2. TestExtractTextFromPDF_SkipsOCRWhenTextLayerPresent: echtes Vektor-
Text-PDF, OCR real übersprungen.
3. TestExtractTextFromPDF_ThroughputIsAcceptable: 3,48s/Anhang real
gemessen (Ziel 8s/Anhang).
Zusätzlich TestExtractTextFromPDF_PerformsOCRWhenNoTextLayer für
Akzeptanzkriterium 1 (gescannte PDFs) end-zu-Ende.
Kein Umbau: search/dedup/indexworker/storage/crypto/encstorage/
savedsearch unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
Gespeicherte Suchanfragen: Suchkriterien inklusive aktiver Filter
benannt speichern und live wiederausführen.
- savedsearch/store.go: Postgres-Store, Save (Upsert über tenant_slug/
user_id/name), List/Get streng auf Mandant+Benutzer beschränkt,
Delete entfernt genau eine Zeile. Execute führt jede Ausführung LIVE
gegen search.Client aus, kein eingefrorener Snapshot.
- search/facets.go: kleinste nötige Erweiterung — Client.SearchWithFilters
(gemeinsame buildFilteredMust-Hilfsfunktion mit Facets extrahiert)
liefert tatsächlich gefilterte Treffer statt nur Zählungen, sonst gäbe
es keinen echten Weg, gespeicherte Filter beim Wiederausführen
anzuwenden.
Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-08-PRUEFPROTOKOLL.md):
1. TestExecute_SavedSearchWithMultipleFiltersReproducesCorrectly: 2
kombinierte Filter liefern real genau das eine passende Dokument.
2. TestList_UserSeesNoOtherTenantsSavedSearches: Mandant B sieht real
keine gespeicherten Suchen von Mandant A.
3. TestDelete_RemovesOnlyThatSavedSearch: Löschen entfernt real nur die
eine gespeicherte Suche, die andere bleibt unverändert.
Kein Umbau: Search/Index/Delete-Verhalten sonst unverändert,
dedup/indexworker/storage/crypto/encstorage unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
Tenant-KEK-Rotation ohne Neuverschlüsselung des Archivbestands
(Envelope-Encryption bleibt aus ARC-02 unverändert, Objekt-DEKs werden
nicht angefasst).
Core (API-10, RotateTenantKEK) ersetzt den Tenant-KEK durch einen neuen
Wert und hält keine Historie vor — TenantKEKHandler liefert immer nur den
aktuellen Schlüssel. Damit Mail Altbestand nach einer Rotation weiterhin
lesen kann, versioniert Mail selbst jeden bezogenen Tenant-KEK:
- crypto/kekversions.go: KEKVersionStore, lokal verschlüsselt mit
eigenem Wrap-Schlüssel (nur über Umgebungsvariable), erkennt Rotation
automatisch (RecordIfNew), erlaubt gezieltes Sperren einer Version
(Revoke).
- crypto/service.go: Service.WithVersionStore (optional, Open bleibt für
Rückwärtskompatibilität unverändert), Seal zeichnet die verwendete
KEK-Version auf, neue Methode OpenAtVersion liest mit historischer
statt aktueller Version.
- encstorage.go: neuer .dek.version-Sidecar (gleiches Muster wie der
bestehende .dek-Sidecar), GetDecrypted nutzt OpenAtVersion; fehlender
Sidecar (Altobjekte vor ARC-08) fällt auf Version 0 zurück, identisches
Verhalten wie vorher.
Prüfungen (alle real durchgeführt, siehe mail/docs/ARC-08-PRUEFPROTOKOLL.md):
1. TestRotation_OldArchiveStaysReadableAfterMasterKeyRotation: Altbestand
nach realer Rotation weiterhin lesbar über OpenAtVersion, naives Open
mit dem neuen Schlüssel schlägt für das alte Objekt real fehl.
2. TestRotation_CompromisedOldKeyCanBeRevoked: gesperrte Version blockiert
Lesezugriff real, andere Versionen bleiben unberührt.
3. Rotationsvorgang vollständig durchgespielt (siehe Prüfprotokoll).
Kein Umbau: storage/dedup/indexworker/search unverändert, bestehende
ARC-02-Tests (encstorage_test.go) unverändert weiterhin grün.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
Werkzeug für vollständigen Suchindex-Neuaufbau: neue Tabelle anlegen,
Dokumente aus der lebenden Tabelle kopieren, Trefferzahlen verifizieren,
erst dann per Manticore RENAME atomar umschalten.
- reindex.go: Reindexer.Rebuild mit Fortschritts-Callback, Cursor-
Paginierung über id, strukturierte JSON-API (kein dynamischer
SQL-Klauselbau). Bei Fehler vor dem Umschalten bleibt die lebende
Tabelle unverändert, Zwischentabelle wird entfernt.
- Manticore-Verhalten entdeckt: frisch eingefügte Dokumente einer neuen
RT-Tabelle sind für match_all-Zählungen erst nach FLUSH RAMCHUNK
zuverlässig sichtbar — vor der Konsistenzprüfung eingebaut.
- Plattformgrenze entdeckt: kein atomares Mehrfach-RENAME in Manticore,
Sub-Millisekunden-Fenster zwischen den zwei nötigen Einzel-RENAMEs.
Client.Search bekam einen begrenzten Retry auf "unknown local table".
- Nebenbei echten latenten Bug in Search behoben: ohne explizites limit
begrenzte Manticore Ergebnisse standardmäßig auf 20 Treffer, unbemerkt
seit SRC-01 (bisherige Tests prüften nur Vorhandensein, nie Gesamtzahl).
Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-09-PRUEFPROTOKOLL.md):
1. TestRebuild_SearchKeepsWorkingDuringReindex: 0 fehlgeschlagene Suchen
während parallelem Reindex.
2. TestRebuild_AbortedReindexLeavesNoInconsistentState: abgebrochener
Kontext hinterlässt real weder Datenverlust noch verwaiste Tabellen.
3. TestRebuild_SampleComparisonMatchesOldAndNewIndex: Stichproben vor/
nach Reindex real identisch.
Kein Umbau: Index/Delete/Facets-Verhalten sonst unverändert,
dedup/indexworker/storage/crypto/encstorage unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
Facetten- und Filter-API für Suche (Absender, Zeitraum, Postfach,
Anhangstyp, Tag), mandantengetrennt, mit UND-Verknüpfung mehrerer Filter.
- migrations/0002..0005: vier nummerierte ALTER-Migrationen für die neuen
Facettenfelder (Manticore erlaubt nur eine Spalte je ALTER-Anweisung),
EnsureSchema wendet sie idempotent nach.
- fields.go: FacetFields-Whitelist, einzige zulässige Facettendimensionen.
- facets.go: Client.Facets nutzt Manticores strukturierte aggs.terms/
aggs.range-API, kein dynamischer SQL-Klauselbau. Filter kombinieren als
zusätzliche equals-Klauseln in derselben bool.must-Liste wie der
Tenant-Filter. Zeitraum-Facette über feste Buckets via aggs.range.
Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-05-PRUEFPROTOKOLL.md):
1. TestFacets_CountsMatchActualHits: Facettenzahlen stimmen real mit der
tatsächlichen Treffermenge überein.
2. TestFacets_ThreeFiltersCombineWithAND: 3 kombinierte Filter schränken
4 Dokumente real auf genau 1 verbleibenden Treffer ein.
3. TestFacets_TenantSeparation: Facetten eines Mandanten enthalten real
keine Werte eines anderen.
Kein Umbau: Search/Delete/Index-Verhalten aus SRC-01/SRC-03 unverändert,
dedup/indexworker/storage/crypto/encstorage unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
Such-Oberfläche (Next.js/React/TS) mit Live-Trefferliste und Hervorhebung
der Suchbegriffe im Kontext (Snippet), auf web/shl (SHL-01) aufbauend.
- app/api/search/route.ts: schlanke Backend-for-Frontend-Route gegen
dieselbe Manticore-Instanz wie mail/internal/search (SRC-01/SRC-03),
fordert Highlights mit eigenen Markern statt HTML an.
- lib/highlight.ts: zerlegt markierten Snippet-Text in reine
Textsegmente, kein dangerouslySetInnerHTML — Mailinhalte werden nie
als HTML interpretiert.
- app/page.tsx: Sucheingabe, Trefferliste mit <mark>-Hervorhebung,
verständlicher Hinweis bei leerem Ergebnis.
- app/mail/[messageId]/page.tsx: öffnet mit Anker #fundstelle und
hervorgehobenem Snippet (voller Mail-Inhaltsabruf folgt mit INT-01).
- lib/contrast.ts: reale WCAG-2.1-Kontrastberechnung.
Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-04-PRUEFPROTOKOLL.md):
1. Manueller Test gegen echten next start + live Manticore auf
192.168.1.131: Hervorhebung real bestätigt.
2. lib/highlightColors.test.ts: echte WCAG-Berechnung, Hell 14,29:1,
Dunkel 6,43:1 (>= 4.5:1 AA).
3. Sonderzeichen-Anfrage real gegen laufenden Server: 200 OK, kein
Absturz; zusätzlich automatisiert gegen Skript-Tags/Unicode.
Kein Umbau: mail/internal/*, web/shl, web/retention-admin unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
Indexierungs-Worker, der neu archivierte Mails asynchron in den
Manticore-Index (SRC-01) einpflegt und Löschungen nachzieht.
- indexworker/queue.go: Postgres-Jobqueue (mail_index_jobs), FOR UPDATE
SKIP LOCKED, Stale-Lock-Wiedervorlage bei Worker-Absturz, arithmetischer
Backoff bei Fail (kein String-Concat für Intervalle) — Konvention aus
dms/internal/jobqueue (FDN-04), hier bewusst ohne DLQ (nicht Bestandteil
der Akzeptanzkriterien dieser Kachel).
- indexworker/worker.go: RunOnce verarbeitet index-/delete-Jobs über
search.Client.
- search: minimale Erweiterung um Client.Delete und deterministisches
DocumentID(tenantSlug, messageID), damit Index/Delete für dieselbe Mail
immer dasselbe Dokument treffen.
Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-02-PRUEFPROTOKOLL.md):
1. TestDequeue_WorkerCrashMidRunLosesNoJob: simulierter Worker-Absturz,
Job wird nach Ablauf der Stale-Lock-Frist real erneut zugestellt.
2. TestDeleteJob_RemovesMailFromSearchResults: Lösch-Job entfernt Mail
nachweislich aus Suchtreffern.
3. TestConsistency_DatabaseAndIndexMatchOnSample: DB-Job-Status und
Index-Inhalt stichprobenartig real abgeglichen.
Zusätzlich TestIndexJob_MakesMailSearchable für Akzeptanzkriterium 1.
Kein Umbau: storage/crypto/encstorage/dedup unverändert, bestehendes
SRC-01-Verhalten unverändert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
- mail/internal/dedup/hash.go: HashAndBuffer, SHA-256 auf Klartext VOR
Verschluesselung (ARC-02), liefert erneut lesbaren Reader zurueck.
- mail/internal/dedup/store.go: Store (Postgres, tenant_slug fest im
Primaerschluessel gebunden), Register: Duplikat referenziert Original
statt redundant zu speichern.
- Alle 3 Pflichtpruefungen real bestanden (siehe
mail/docs/ARC-03-PRUEFPROTOKOLL.md): Duplikat aus zwei Quellen
erkannt, zwei Mandanten mit identischem Inhalt nicht verknuepft,
knapp unterschiedliche Nachricht korrekt nicht erkannt.
- Kein Umbau: internal/storage, internal/crypto, internal/encstorage
unveraendert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- mail/internal/crypto: Envelope-Encryption (AES-256-GCM), DEK pro
Objekt, HTTPKEKProvider bezieht Tenant-KEK ueber Core API-12 -
bewaehrtes Muster aus DMS FDN-09, Neuimplementierung (Mail kann DMS
nicht importieren)
- mail/internal/encstorage: verbindet ARC-01 (storage.Service) mit
ARC-02 (crypto.Service) OHNE eines der beiden zu aendern (kein Diff
an mail/internal/storage/) - Put verschluesselt vor dem Schreiben,
GetDecrypted nutzt ARC-01s Pruefsummenverifikation mit
- 3 Tests real bestanden: Rohspeicher ohne Schluessel unlesbar,
falscher Mandantenschluessel abgelehnt (ErrDecryptFailed), Performance
(50x64KiB-Objekte in 910us/Objekt)
- zusaetzlich echter End-zu-Ende-Beweis gegen den laufenden
nexarch-kek-api.service (API-12): vollstaendiger Put->GetDecrypted-
Roundtrip ueber echten HTTP-KEK-Bezug, nicht-existenter Tenant real
abgelehnt (404)
- offener Punkt ehrlich vermerkt: internal/crypto/internal/encstorage
fehlen noch in QA-01s Pflichttest-Gate-Pfadmustern
Pruefungen siehe mail/docs/ARC-02-PRUEFPROTOKOLL.md
- mail/internal/storage: LocalDriver/S3Driver (bewaehrtes Muster aus
DMS FDN-03, bewusste Neuimplementierung - Mail kann DMS nicht
importieren), ObjectKey mit festem Pfadschema
- Service.Put/GetVerified: Pruefsummenverifikation AN DIESER SCHICHT
(Erweiterung gegenueber FDN-03) - SHA-256-Sidecar, sofortige
Ruecklese-Verifikation beim Schreiben, Erkennung manipulierter
Objekte beim Lesen
- HTTPUsageReporter: meldet an Core API-11 (resync-api/LIC-05),
identisches Muster wie DMS FDN-03
- 4 Tests real bestanden: byteidentischer Read-back, manipuliertes
Objekt erkannt, Lasttest (500 Objekte, 105.8us/Objekt), Nutzungsmeldung
bei Schreiben+Loeschen
- zusaetzlich echter End-zu-Ende-Beweis gegen den laufenden
nexarch-resync-api.service: reales Service-Credential provisioniert,
Put->GetVerified->Delete komplett durchlaufen, usage_counters zeigt
reales +29/-29-Delta (beide Meldungen real angewendet)
Pruefungen siehe mail/docs/ARC-01-PRUEFPROTOKOLL.md
- mail/go.mod: erstes eigenstaendiges Go-Modul fuer NEXARCH Mail
- mail/docs/TESTSTRATEGIE-MAIL.md: Testpyramide (Unit/Integration/
Protokoll-Zustandsmaschinen/E2E/Vertragstests), Pflichttest-Merge-Gate,
Bug-Tracking-Konvention (Gitea-Issues), analog Core QA-01
- mail/internal/example: ein reales, kleines Beispiel (Adress-
Normalisierung) mit je einem Test pro Testart (Unit/Integration/E2E),
6 Tests real bestanden
- mail/internal/pflichttestgate + cmd/pflichttestgate: Merge-Gate-CLI,
echter End-zu-Ende-Beweis (Binary lehnt Verstoss ab, akzeptiert
begleiteten Test), .gitea/workflows/mail-pflichttest-gate.yml
- Ehrlich dokumentiert: kein Gitea-API-Token verfuegbar, daher kein
echter Issue angelegt - Bug-Tracking-Vorgehen stattdessen anhand
eines realen, bereits dokumentierten Befunds (RET-10) durchgespielt,
als offener Punkt vermerkt
- Gegenlesen durch zweite Person (Nutzer) noch ausstehend
Pruefungen siehe mail/docs/TESTSTRATEGIE-MAIL.md