# Codex-Brief β€” Korrekturen aus dem zstd-Messlauf Vier Befunde aus meinem Messlauf (14.07.). Reihenfolge = PrioritΓ€t. **1 und 2 sind Pflicht vor der ersten echten `@dr-gold.de`-Migration.** --- ## 1. πŸ”΄ REGRESSION: Alte plain-Archive sind im Viewer unsichtbar **Befund:** Die Nachrichtenliste kommt jetzt aus `mbox_index`. Archive, die **vor** dem zstd-Deploy geschrieben wurden, haben keine Index-Zeilen: ``` mbox_index: codex-abnahme.../INBOX -> 1 Zeile (Datei enthaelt 207 Mails) Viewer zeigt: "INBOX (1)" ``` Das Perfide: Es zeigt **β€ž(1)" statt eines Fehlers**. Ein halb-wahres Ergebnis ist schlimmer als gar keins β€” es sieht aus, als funktioniere es. Der im zstd-Brief geforderte Fallback auf den alten Parse-Pfad fehlt. **Fix β€” die mbox ist append-only, also reicht ein Fortschritts-Marker pro Datei:** ```sql CREATE TABLE IF NOT EXISTS mbox_index_state( account_id INTEGER NOT NULL, folder TEXT NOT NULL, indexed_bytes INTEGER NOT NULL DEFAULT 0, -- bis hierhin ist die Datei indiziert UNIQUE(account_id, folder) ); ``` - **Nachindizieren:** Wenn `dateigroesse > indexed_bytes` β†’ ab `indexed_bytes` bis EOF scannen, Index-Zeilen anhaengen, `indexed_bytes` fortschreiben. Deckt **leeren** und **teilweisen** Index gleichermassen ab. - Der **Migrationspfad** schreibt `indexed_bytes` beim Append mit fort β€” dann muss im Normalbetrieb nie nachgescannt werden. - **CLI `--reindex `** fuer den Bestand. - Der Scanner muss **beide** Formate koennen: plain (`From `-Trennzeilen) und `.zst` (Frame fuer Frame entpacken, dann innerhalb des Frames an den `From `-Trennzeilen splitten). Das ist genau die Logik, die Punkt 4 spaeter auch fuer Batch-Frames braucht β€” einmal richtig bauen. - **Nie wieder still halbe Wahrheiten:** Wenn `dateigroesse != indexed_bytes` und nicht nachindiziert werden kann β†’ **Fehler zeigen**, keine gekuerzte Liste. --- ## 2. πŸ”΄ Ziel-Dedup-Werkzeug (die Feuerwehr) **Befund:** In `vdevop-03/INBOX` existieren **199 Message-IDs doppelt** β€” die INBOX wurde irgendwann komplett ein zweites Mal migriert. Niemand hat es gemerkt, weil im Log nur `copied=199` stand und wie normaler Fortschritt aussah. Die *Ursachen* sind inzwischen dicht (Message-ID-Normalisierung, DB-Guard, Watch-Fail-Fast). **Aber es gibt kein Werkzeug, das Dubletten im Ziel findet oder repariert.** Wenn das bei einem echten dr-gold-Postfach passiert, gibt es keinen Ausweg β€” und das merkt man erst, wenn es zu spaet ist. **Fix:** `--dedup-target [--folders ...] [--apply]` (+ Admin-Route). - Ordner scannen, **header-only** (`Headers()`-Pfad, keine Bodies!), nach normalisierter Message-ID gruppieren. - Pro Gruppe mit >1 Kopie: **die niedrigste UID behalten** (die aelteste, zuerst eingespielte), den Rest `\Deleted` + `EXPUNGE`. **Sicherheitsregeln β€” das Ding loescht fremde Mail, ein Bug hier ist schlimmer als die Dubletten:** 1. **Dry-Run ist Default.** Ohne `--apply` wird nur berichtet: pro Ordner, wie viele Gruppen, wie viele ueberzaehlige Kopien, welche UIDs. 2. **Nie die letzte Kopie loeschen.** Pro Message-ID bleibt immer genau eine. 3. **Mails ohne Message-ID nie anfassen** β€” nicht sicher dedupliizierbar. Separat auszaehlen und berichten. 4. **Jede Loeschung protokollieren** (Konto, Ordner, UID, Message-ID) β€” es braucht eine Beweiskette. 5. Nur `requireAdmin`. --- ## 3. 🟑 `folder_map` ist toter Code Die Tabelle wird angelegt, aber **nirgends gelesen**: [`07-migrate.go:122`](backend/07-migrate.go) uebergibt den Override hart als `""`: ```go dstFolder := MapSourceToTarget(folder.Name, folder.Attrs, folder.Delim, dst.Delim(), targets, "") // ^^ immer leer ``` `MapSourceToTarget` hat den Parameter, `11-folders.go` dokumentiert β€žmanuelles folder_map hat Vorrang" β€” es tut es nur nicht. Ein dokumentiertes Feature, das stillschweigend nichts tut. (Genau daran ist mein Messlauf haengengeblieben: die Umleitung wurde ignoriert und 207 Mails landeten im echten Ziel-Ordner.) **Fix:** `folder_map` pro Konto laden und als `override` durchreichen. Plus Pflege in der UI (Konten-Formular: Quell-Ordner β†’ Ziel-Ordner). --- ## 4. 🟒 Kompressionsrate β€” gemessen, und die Entscheidung dazu | | Groesse | Faktor | |---|---|---| | plain mbox (207 Mails) | 4.372.002 | β€” | | **zstd, Frame pro Mail (heute)** | **1.470.412** | **2,97Γ—** | | zstd, ganze Datei am Stueck | 113.564 | 38,50Γ— | Der Abstand ist die **Cross-Message-Redundanz**, die Frame-pro-Mail wegwirft. (Mein Korpus uebertreibt ihn β€” 199 fast identische Benachrichtigungsmails. Auf echter Mail waere die Luecke kleiner, aber real.) **Was wir NICHT machen: zstd-Dictionary.** Das waere technisch der eleganteste Weg (genau dafuer sind Dictionaries da), aber es macht das Archiv **abhaengig von einer externen Datei**: Dictionary weg β†’ Archiv unlesbar. Fuer ein Beweis-Archiv, das **6–10 Jahre** halten und im Streitfall herausgegeben werden muss, ist β€ž`zstd -d` funktioniert einfach, ohne Beiwerk" mehr wert als jeder Kompressionsfaktor. **Selbstgenuegsamkeit schlaegt Rate.** **Was wir machen koennen: Batch-Frames mit kleinem Deckel (2 MB).** Der Index (`inner_offset`/`inner_len`) ist bereits dafuer gebaut. **Aber die Durability-Regel ist dabei nicht verhandelbar:** Bei Batching ist eine Mail nach `mbox.Append` **noch nicht auf der Platte** β€” sie liegt im Frame-Puffer. Deshalb: ``` Frame voll (oder Ordner zu Ende) -> Frame komprimieren -> KOMPLETTEN Frame an die Datei anhaengen + fsync -> DANN erst: Index-Zeilen schreiben -> DANN erst: MarkCopied fuer alle Mails des Batches (eine Transaktion) ``` `MarkCopied` darf **niemals** vor dem fsync passieren β€” sonst gilt eine Mail als gesichert, die nie im Archiv ankam. Das waere der schlimmstmoegliche Bug fuer ein Beweis-Archiv. Nebenwirkung, die man kennen muss: Ein Absturz mitten im Batch laesst bis zu 2 MB Mails unmarkiert β†’ sie werden beim naechsten Lauf erneut ins **IMAP-Ziel** kopiert β†’ Dubletten dort. Dasselbe Fenster wie heute, nur groesser (Batch statt Einzelmail). **Deshalb der kleine Deckel (2 MB, nicht 8) β€” und deshalb ist Punkt 2 (Dedup-Werkzeug) die Voraussetzung dafuer.** PrioritΓ€t: **niedriger als 1–3.** 2,97Γ— ist bereits ein Gewinn, und die Vorschau wurde durch den Index sogar **6Γ— schneller** (1,8 ms statt 10,8 ms). Korrektheit zuerst. --- ## Abnahme 1. `go test ./...` gruen. 2. **Reindex:** `--reindex all` β†’ der Viewer zeigt fuer das bestehende plain-Archiv wieder **alle 207** Mails (statt β€ž(1)"). Liste und Oeffnen funktionieren fuer plain **und** `.zst`. 3. **Dedup, Dry-Run:** `--dedup-target codex-abnahme-vdevop02-to-vdevop03` meldet fuer `INBOX` **199 Gruppen mit je einer ueberzaehligen Kopie** (das ist der bekannte, echte Bestand β€” ein guter Selbsttest). Mit `--apply` bleiben danach genau 365 eindeutige Mails uebrig. **Ohne `--apply` darf sich nichts aendern.** 4. **folder_map:** Ein Eintrag `INBOX β†’ TEST-ZIEL` fuehrt im Log zu `INBOX -> TEST-ZIEL` (heute: `INBOX -> INBOX`). 5. **Round-trip bleibt byte-identisch** β€” auch nach jeder Aenderung an der Frame-Struktur: `zstd -d x.mbox.zst` == plain-mbox, `cmp`-sauber. Das ist die Linie, die nie fallen darf. Die Messungen (Rate, Vorschau-Latenz, Round-trip) fahre ich danach erneut.