Files
dionotes-client/docs/research/historie-nachruesten.md
MDietermann d470756920 docs: Rechercheergebnisse zum Notiz-Handling uebernehmen
Beide Recherchen zur Map #16 lagen nur auf lokalen Wegwerf-Branches. Die
Kurzfassungen stehen an den Tickets #21 und #22, die Langfassungen sind mit
Quellen und der Trennung zwischen Belegtem, Abgeleitetem und Unbelegtem aber
das, worauf man zurueckkommt.

Beide enthalten Messungen, die keine Dokumentation hergibt: dass trash 5.2.6
auf Netzlaufwerken gar nicht funktioniert und Windows dort sonst still und
endgueltig loeschen wuerde, und dass ein ungeprunter Append-Log im
Yjs-Benchmark das Siebzigfache des Rohtexts misst.

Ausserdem /.claude in die .gitignore - dort liegen lokale Einstellungen und
die temporaeren Worktrees der Recherche-Agenten.
2026-08-28 12:48:20 +02:00

67 KiB
Raw Blame History

Was es kostet, Historie später möglich zu halten

Research zu Gitea-Issue #22. Stand: 2026-08-28. Recherchiert gegen Primärquellen (Hersteller-Doku, Quellcode auf GitHub, Microsoft Learn, docs.rs, crates.io). Nichts davon wurde gebaut oder kompiliert. Es wurde kein Anwendungscode geschrieben und keine Dependency hinzugefügt. Alles unter „Belegt" stammt aus der jeweils genannten Quelle; alles Abgeleitete ist als solches markiert.

Historie ist nicht geplant. Dieses Dokument beschafft nur die Fakten für die Schreib-Entscheidung in #19.


Kurzantwort

Nein — ein Schreibverfahren „temporäre Datei schreiben, dann umbenennen" schließt eine spätere Historie nicht aus. Es ist im Gegenteil das einzige der beiden zur Wahl stehenden Verfahren, das Historie überhaupt billig macht.

Die Begründung in einem Satz: Historie braucht die Vorfassung, und beim Umbenennen liegt die Vorfassung zum Zeitpunkt des Schreibens noch unversehrt auf der Platte — ein direktes fs::write hat sie da schon vernichtet.

Der Einwand aus der Beratungsrunde trifft eine reale Gefahr, aber am falschen Ort. Teuer wird Historie nicht durch das Wie des Schreibens, sondern durch das Wo: Wenn nach einem halben Jahr fünf Stellen im Code eine Notiz auf die Platte schreiben, muss man fünf Stellen anfassen. Wenn es genau eine gibt, ist Historie eine Änderung innerhalb dieser einen Funktion — und alles Weitere (Format, Ablageort, Aufbewahrung) eine Entscheidung, die man dann trifft, nicht heute.

Der Preis dafür, Historie möglich zu halten, beträgt also: eine Funktion. Keine Datei, kein Format, kein Byte auf der Platte, keine Dependency.

Und die Asymmetrie läuft genau andersherum als behauptet: Nicht das schlichte Schreiben ist die schwer umkehrbare Entscheidung, sondern der Append-Log. Von „Datei ist die Wahrheit" zu „Datei plus Sicherungen" ist eine Ergänzung. Von „Log ist die Wahrheit" zurück ist eine Migration. Wer die Option offenhalten will, hält sie gerade dadurch offen, dass er den Log nicht baut.

Dass der Log die Einbahnstraße ist, ist dabei nicht nur ein Architektur-Argument: Die Automerge-Maintainer schreiben selbst, dass sich Historie nicht verwerfen lässt, ohne das Dokument zu ersetzen, und Yjs begründet in seiner README, warum Tombstones konstruktionsbedingt liegenbleiben müssen (3.2).


1. Was Historie technisch überhaupt braucht

Bevor man Kosten schätzt, lohnt es, die Anforderung auf ihre Bestandteile zu zerlegen. Eine Historie — gleich welcher Bauart — braucht genau vier Dinge:

  1. Einen Auslöser. Ein Zeitpunkt, an dem entschieden wird: jetzt entsteht eine neue Fassung.
  2. Die Vorfassung. Der Inhalt, der gleich ersetzt wird.
  3. Einen Ablageort. Irgendwo müssen die alten Fassungen hin.
  4. Eine Aufräumregel. Ohne sie wächst der Ablageort unbegrenzt.

Nur Punkt 1 und 2 hängen am Schreibverfahren. Punkt 3 und 4 sind reine Folgeentscheidungen und komplett unabhängig davon, wie die .md auf die Platte kommt.

Und beide, Auslöser und Vorfassung, fallen beim temp+rename-Verfahren ohnehin an:

  • Der Auslöser ist der Aufruf der Schreibfunktion. Präziser geht es nicht.
  • Die Vorfassung liegt in dem Moment, in dem die temporäre Datei fertig geschrieben ist und das Umbenennen ansteht, noch vollständig und unangetastet unter ihrem Pfad. Man muss sie nur lesen, bevor man umbenennt.

Bei einem direkten fs::write auf die Zieldatei ist die Vorfassung dagegen nach dem ersten Byte weg. Wer dort später Historie nachrüsten will, muss die Datei zusätzlich vorher lesen — was möglich, aber ein Extra-Schritt ist, den das rename-Verfahren geschenkt bekommt.

Nebenbefund, der #19 unabhängig von Historie betrifft: #20 (Zeitstempel-Konflikt) verlangt ohnehin, dass die Schreibfunktion vor dem Schreiben den Zustand der Zieldatei prüft. Wer dort statt eines stat gleich den Inhalt liest, hat die Vorfassung ohne zusätzlichen Syscall in der Hand. Das ist kein Argument, es heute zu tun — nur der Beleg, dass es später auch nichts kostet.


2. Wie vergleichbare lokale Markdown-Editoren es lösen

Kurzüberblick, Details und Belege in den Unterabschnitten:

Werkzeug Wo liegt die Historie? Format Intervall Aufbewahrung Neben den .md?
Obsidian (File recovery) App-Systemordner, %APPDATA%\Obsidian\ — außerhalb des Vault Volltext-Snapshots 5 Min (Default) 7 Tage (Default) Nein
Obsidian Sync (Version history) Auf Obsidians Servern serverseitig — 1 Monat (Standard) / 12 Monate (Plus) Nein, kostenpflichtig
Joplin (Note History) Tabelle revisions in database.sqlite Diff-Kette (diff-match-patch, JSON) 10 Min 90 Tage (Default) Nein — es gibt gar keine .md
Logseq (0.10.9) logseq/bak/<datei>/ im Graph-Ordner Volltext-Kopien, Zeitstempel im Namen nur bei Konflikt oder Schreibfehler, nicht bei jedem Speichern 6 Fassungen pro Datei Ja
Zettlr — es gibt keine — — — —
Syncthing (kein Editor, aber das beste Vorbild) .stversions/ im Sync-Ordner Volltext-Kopien mit Zeitstempel im Namen pro Änderung konfigurierbar, logarithmisch ausgedünnt Ja

Das auffälligste Muster: Von den untersuchten Editoren legt keiner seine Historie neben die Notizdateien. Sie liegt entweder im App-Ordner (Obsidian), in einer Datenbank (Joplin) oder auf einem Server (Obsidian Sync). Wer neben den .md versioniert, ist ein Sync-Werkzeug, kein Editor.

2.1 Obsidian — Historie als aufgesetztes Core-Plugin, komplett außerhalb des Vault

Obsidian ist der direkteste Vergleichspunkt: lose .md-Dateien in einem Ordner, dasselbe Versprechen an den Nutzer, dieselbe Ausgangslage.

Belegt (Zitate aus der offiziellen Help-Seite https://obsidian.md/help/plugins/file-recovery):

  • „File recovery is a core plugin that protects your work from accidental deletions, file corruption, or unwanted changes by automatically saving complete snapshots of your notes at regular intervals."
  • „Snapshots capture the full content of your files, not just changes, allowing you to restore any previous version." — also Volltext, keine Diffs.
  • „By default, snapshots are saved a minimum of 5 minutes from each other, and kept for 7 days."
  • „Snapshots are kept in the Global settings, outside of the vault, to account for vault-related data loss. This means that snapshots are stored with the absolute path to the note. If you've moved your vault recently, you may need to move it back to the location where it was when the snapshot was taken."
  • „If you are using Obsidian Sync or other syncing services, File recovery snapshots will not sync between devices. Snapshots are device-specific and remain local to each device."
  • „File recovery is not a complete backup solution."
  • Nur .md und .canvas sind wiederherstellbar.
  • Aufräumen automatisch nach Ablauf der Tagesgrenze, manuell über Clear history.

Der Ablageort ist damit belegt als der Electron-userData-Ordner der App — Windows: %APPDATA%\Obsidian\ (https://obsidian.md/help/data-storage), nicht .obsidian/ im Vault und nicht neben den Notizen.

Vermutung (starkes Indiz, nicht wörtlich belegt): Die Snapshots liegen in Obsidians IndexedDB/LevelDB. Die Doku nennt bei IndexedDB nur Sync-State und Metadata-Cache, führt aber für File recovery dieselbe Apple-Lockdown-Mode-Einschränkung auf wie für IndexedDB. Ein Serialisierungsformat ist nirgends dokumentiert; Obsidian ist Closed Source.

Widerspruch in der Doku selbst, den man kennen sollte: Der Abschnitt „Storage and performance" derselben Seite behauptet, Snapshots „only store changed files", während der Haupttext „full content […] not just changes" sagt. Plausible Auflösung (Vermutung): Nur geänderte Dateien werden überhaupt gesnapshottet, jede davon aber als Volltext.

Zum Schreibverfahren: Es gibt keine offizielle Aussage, ob Obsidian atomar (temp+rename) oder in-place schreibt. Belegt ist nur die Plugin-API-Semantik: Vault.process ist dokumentiert als „Atomically read, modify, and save the contents of a note" und garantiert, „that the file doesn't change between reading the current content and writing the updated content" (https://docs.obsidian.md/Plugins/Vault). Das ist eine Sperre gegen konkurrierende Plugins innerhalb des Obsidian-Prozesses, keine dokumentierte Aussage über rename()-Atomarität gegenüber dem Dateisystem — diese Unterscheidung macht die Doku nirgends explizit (Lesart, nicht wörtlich belegt). Der Changelog zu 1.13.4 (30.07.2026) enthält den Eintrag „Fixed a rare race condition where a file could get stuck and fail to save until the app was restarted" (https://obsidian.md/changelog/2026-07-30-desktop-v1.13.4/) — ein Beleg dafür, dass der Save-Pfad auch nach über zehn Jahren nicht trivial ist.

Warum das für #22 der wichtigste Befund ist: Obsidians Historie ist ein nachträglich aufgesetztes Plugin, das mit dem Schreibverfahren des Kerns nichts zu tun hat. Es beobachtet Dateien und legt Kopien woanders ab. Genau das ist das „Historie ist eine Schicht über dem Schreiben"-Modell — von dem am längsten etablierten Werkzeug dieser Kategorie in Produktion vorgeführt.

Der Preis, den Obsidian für diesen Ort zahlt (alles wörtlich aus der Doku oben): absolute Pfade, die nach einem Vault-Umzug brechen; keine Mitnahme beim Backup; kein Sync zwischen Geräten. Ein Mittelweg — Historie in einem versteckten Ordner im Vault — hat von den untersuchten Werkzeugen keiner gewählt; für Obsidian existiert dazu ein offener Feature-Request (https://forum.obsidian.md/t/file-recovery-store-file-snapshots-in-the-vault-folder-instead-of-the-obsidians-system-folder/116677, Sekundärquelle, nur als Hinweis).

2.2 Joplin — der Gegenentwurf: keine .md, dafür Diff-Ketten in SQLite

Joplin ist als Vergleich nur begrenzt tauglich, weil es das Versprechen, das DioNotes gibt, gar nicht erst macht — aber es zeigt sehr genau, was man mit einer Datenbank dahinter gewinnt.

Belegt (https://joplinapp.org/help/dev/spec/user_profile/): database.sqlite ist „The central SQLite database file where all notes, notebooks, tags, and metadata are stored". Es gibt keine .md-Dateien. Unter Windows: %UserProfile%\.config\joplin-desktop.

Belegt (https://joplinapp.org/help/apps/note_history/):

  • „All the apps save a version of the modified notes every 10 minutes."
  • Aufbewahrung „by default, for 90 days".
  • „These revisions are then synced across all the devices." — anders als bei Obsidian.
  • Wiederherstellen kopiert in einen Ordner „Restored Notes", ohne die aktuelle Version zu verändern.
  • Bei unterschiedlichen Einstellungen pro Gerät gewinnt die kürzeste Aufbewahrungsdauer.

Belegt aus dem Quellcode (https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Revision.ts, https://github.com/laurent22/joplin/blob/dev/packages/lib/services/RevisionService.ts, https://github.com/laurent22/joplin/blob/dev/packages/lib/models/settings/builtInMetadata.ts):

  • Tabelle revisions, Felder u. a. title_diff, body_diff, metadata_diff, item_id, parent_id.

  • Diffs über Google diff-match-patch, serialisiert als JSON (createTextPatch → JSON.stringify(dmp.patch_make(old, new))).

  • Defaults: revisionService.ttlDays = 90, revisionService.intervalBetweenRevisions = 10 Min (nicht in der UI sichtbar), revisionService.oldNoteInterval = 7 Tage.

  • Die erste Revision einer Notiz ist ein Volltext-Patch gegen den Leerstring; Folgerevisionen sind Diffs mit parent_id auf die Vorgängerin. mergeDiffs() läuft die Kette bis zur Wurzel zurück.

  • Aufräumen mit Rebase am Kopf — der interessanteste Teil. Kommentar wörtlich aus Revision.ts:

    „When deleting old revisions, we need to make sure that the oldest surviving revision is a 'merged' one (as opposed to a diff from a now deleted revision). So every time we deleted a revision, we need to find if there's a corresponding surviving revision and modify that revision into a 'merged' one."

    Konkret: Die älteste überlebende Revision wird rekonstruiert und als Volltext-Patch gegen den Leerstring neu geschrieben, parent_id auf ''. Damit bleibt die Kettenlänge auf das TTL-Fenster begrenzt.

  • Änderungen, die aus Sync oder Entschlüsselung stammen, werden ausgefiltert — nur echte lokale Edits erzeugen Revisionen.

Die Lehre für DioNotes: Diff-Ketten sind sparsam, aber nicht umsonst. Sie kosten eine Rekonstruktion in O(Kettenlänge) bei jedem Anlegen und jedem Anzeigen einer Revision, plus eine nicht triviale Rebase-Logik beim Aufräumen, plus einen dokumentierten Fehlerpfad, wenn eine zu löschende Revision noch verschlüsselt ist. Obsidian hat für dasselbe Feature Volltext gewählt und dafür die Aufbewahrung auf 7 statt 90 Tage gesetzt. Retention-Dauer und Speicherstrategie hängen direkt zusammen — das ist die eigentliche Stellschraube, und sie liegt vollständig in der Zukunft.

2.3 Logseq — Sicherungskopien im Workspace, aber nur wenn es brenzlig wird

Logseq ist für DioNotes der lehrreichste Fall, weil es als einziges untersuchtes Werkzeug seine Sicherungskopien neben die Notizen legt — genau der Mittelweg aus der Ticket-Frage — und weil dabei sichtbar wird, wo dieser Weg scheuert.

Versionsvorbehalt: Der master-Branch von Logseq ist inzwischen die DB-basierte Neuentwicklung ohne handler/file.cljs. Alle Datei-relevanten Belege stammen deshalb aus Tag 0.10.9, der letzten dateibasierten Release-Linie.

Belegt — drei generierte Verzeichnisse im Graph-Ordner:

Pfad Zweck Beleg
logseq/bak Sicherung überschriebener Dateien backup_file.cljs#L7
logseq/version-files/local explizit angeforderte Versionen backup_file.cljs#L8
logseq/.recycle Papierkorb — Löschen ist ein renameSync dorthin handler.cljs#L55-L67

Namenskonvention — pro Datei ein eigener Unterordner, Zeitstempel im Dateinamen (backup_file.cljs#L10-L51):

<graph>/logseq/bak/pages/Foo/2024-01-15T10_23_45.678Z.Desktop.md
                    └ relativer Pfad ┘└ ISO-8601, ":" → "_" ┘└ Plattform ┘

Der wichtigste Befund: Es wird NICHT bei jedem Speichern gesichert. Es gibt genau drei Auslöser (belegt):

  1. Erkannte Abweichung zwischen Platte und DB beim Schreiben (fs/node.cljs#L54-L57) — und selbst dann filtert der Electron-Handler ein zweites Mal und sichert nur, wenn der Diff eine Löschung zeigt (handler.cljs#L75-L80):
    (when (and (string? db-content) (string? new-content)
               (string-some-deleted? db-content new-content))
      (backup-file/backup-file repo :backup-dir path ...))
    
  2. Schreibfehler (EBUSY, Lock, fehlende Rechte) — mit Notification „A backup file was saved to …" (handler.cljs#L117-L140).
  3. Externe Dateiänderung über den Watcher, bevor sie in die DB übernommen wird — Quellcode-Kommentar wörtlich: „save the previous content in a versioned bak file to avoid data overwritten." (watcher_handler.cljs#L44-L56).

Maintainer cnrpman fasst es zusammen: „Basically it writes to bak when there's a dangerous file operation… It's a safe net." (Issue #3370).

Das ist keine Historie, sondern ein Fangnetz — und genau das macht es für DioNotes interessant: Der billigste denkbare Nutzen (Datenverlust bei Konflikten verhindern) wird mit dem billigsten denkbaren Mechanismus erkauft, ohne dass irgendetwas am Schreibverfahren hängt.

Aufräumen — es gibt eine Grenze, sie greift nur zu lokal (belegt, backup_file.cljs#L27-L34):

(defn- truncate-old-versioned-files!
  "reserve the latest 6 version files"
  [dir]
  ... (drop 6 (reverse (sort files))) ... removeSync ...)

Warum der Ordner trotzdem wächst (Ableitung aus dem Code, deckt sich mit den Nutzerberichten):

  • Die Grenze gilt pro Datei-Unterordner; die Zahl der Unterordner ist unbegrenzt.
  • Aufgeräumt wird nur beim Schreiben eines neuen Backups in genau diesen Ordner. Kein Startup-Sweep, kein Hintergrundjob. Ordner von Dateien, die nie wieder konfligieren, bleiben ewig stehen.
  • Sortiert wird rein lexikografisch — funktioniert nur, weil ISO-8601 lexikografisch = chronologisch ist.

Die Retention-Zahl wanderte über die Jahre: nie getrunct (≤ 0.6.1) → 10 (PR #4421, Commit-Text: „Files in the logseq/bak folder are never truncated") → 3 → 6. Gründer tiensonqin empfiehlt in Issue #4819: „I suggest to cleanup backups using some cron scripts." Es gibt keine offizielle Doku zum bak-Ordner und keinen Off-Schalter (Forum, Aug 2025, unbeantwortet).

Korrektur einer verbreiteten Annahme: Beschwerden über Wachstum gibt es reichlich (#4819, #3588 „600 or more backup files", #3370 offen seit 2021, Discussion #8624 seit drei Jahren unbeantwortet). Aber keine Primärquelle nennt eine konkrete GB-Zahl. Belegt ist unbegrenztes Ordner-Wachstum, nicht unbegrenztes Größenwachstum.

Weitere belegte Details:

  • Nicht atomar geschrieben: fs/writeFileSync path content, mit vorherigem chmodSync auf 644, falls die Datei nicht schreibbar ist (handler.cljs#L117-L126).
  • Watcher: chokidar, awaitWriteFinish true (fs_watcher.cljs).
  • Konflikt: Vergleich vor dem Schreiben; bei Abweichung wird nicht geschrieben, sondern ein Modal gezeigt mit zwei editierbaren Textareas („On disk:" / „In Logseq:"), je einem Button „Select this" — kein Merge, kein Abbrechen (components/diff.cljs#L27-L90).
  • Undo überlebt den Neustart nicht. Der Stack ist ein reines In-Memory-Atom über DataScript-Datoms — (def ^:private undo-redo-states (atom {})) (undo_redo.cljs#L16-L28). Die offizielle Doku formuliert es vorsichtig: „during their editing sessions" (docs: Undo and Redo).
  • Es gibt keine UI, die bak-Versionen anzeigt oder wiederherstellt — nur „Open page backups directory", das den Explorer öffnet. Der Menüpunkt „View page history" ist dagegen Git-basiert (shell/get-file-latest-git-log) und setzt ein eigenes Git-Setup des Nutzers voraus (page_menu.cljs#L93-L159).

Die Lehre: Selbst das Werkzeug, das im Workspace sichert, hat sich gegen „bei jedem Speichern" und für „nur wenn es brenzlig wird" entschieden — und hat trotzdem ein Aufräumproblem, weil die Grenze pro Datei statt global gilt. Beides sind Entwurfsfehler, die man beim Nachrüsten vermeiden kann, weil sie mit dem Schreibverfahren nichts zu tun haben.

2.4 Zettlr — gar keine Historie, und das ist die offizielle Position

Belegt: Zettlr hat kein Snapshot-, History- oder Versions-Feature. Die Doku delegiert das Thema ausdrücklich an den Nutzer (https://docs.zettlr.com/en/first-time-users/workspaces.html):

„We strongly recommend that you regularly back up your workspaces. The simplest method is to use a cloud storage provider (such as Dropbox, Google Drive, OneDrive, or iCloud)… you should also back up the files onto an external storage device from time to time."

Vorhanden ist nur der System-Papierkorb via shell.trashItem (safe-delete.ts) und ein überschreibendes Autosave.

Schreibverfahren — nicht atomar (belegt, fsal/index.ts#L529-L533):

public async writeTextFile (filePath: string, contents: string): Promise<void> {
  await this._cache.del(filePath)
  await fs.writeFile(filePath, contents, 'utf-8')
}

Drei Details des Save-Pfads, die #18 bis #20 direkt betreffen (belegt, documents/index.ts#L1527-L1603):

  • Der Descriptor wird vor dem Schreiben aktualisiert, mit explizitem Kommentar zur Race Condition „User ändert → Save startet → User ändert mehr → Save beendet und macht die Änderungen rückgängig".
  • Self-Echo-Unterdrückung: this._ignoreChanges.push(filePath) — der eigene Write wird beim Watcher-Event wieder herausgefiltert.
  • BOM und Zeilenende werden rekonstruiert: intern arbeitet Zettlr immer mit \n, beim Schreiben wird der ursprüngliche Zustand wiederhergestellt (doc.descriptor.bom + content.split('\n').join(doc.descriptor.linefeed)).

Konfliktbehandlung, dreistufig (belegt, documents/index.ts#L1070-L1130) — für #20 unmittelbar verwertbar:

  1. mtime-Filter, bewusst modtime <= ourModtime → return statt Gleichheitsprüfung. Kommentar wörtlich: „In response to issue #1621: We will not check for equal modtime but only for newer modtime to prevent sluggish cloud synchronization services (e.g. OneDrive and Box) from having text appear to 'jump'."
  2. Inhaltsvergleich: if (diskContents === doc.lastSavedContent) return — fängt Cloud-Dienste ab, die nur Attribute anfassen.
  3. Dialog „File changed on disk" mit den Buttons „Keep editor contents" / „Load changes from disk" (kein Merge, kein Diff) und einer Checkbox „Always load changes from disk if there are no unsaved changes in the editor", die direkt die Einstellung alwaysReloadFiles schreibt.

Zur Git-Frage: Es gibt kein offizielles Zettlr-Git-Plugin (belegt). Zettlr/Zettlr-Git-Plugin liefert 404; die Organisation hat keine Plugin-Repos (https://github.com/orgs/Zettlr/repositories). Zettlr hat überhaupt kein Plugin-System — Maintainer nathanlesage: „Writing a plugin system is incredibly difficult, and thus we should do that extremely limited." (Discussion #3108). Das einzige Git-Feature ist reine Erkennung: dir.isGitRepository = (await fs.lstat(path.join(dir.path, '.git'))).isDirectory() (fsal-directory.ts#L103-L105). Issue #1050 „Integrate basic git functionality" ist seit Juli 2020 offen, Label „Priority: Low".

Die Lehre: Ein etablierter, ernstgemeinter Markdown-Editor kommt seit Jahren ohne jede eingebaute Historie aus und verweist stattdessen auf externe Werkzeuge. Das ist der empirische Beleg dafür, dass „Historie nicht bauen" eine tragfähige Produktentscheidung ist — nicht bloß ein Aufschub.

2.5 Syncthing — kein Editor, aber das beste Vorbild für Ablage und Aufräumen

Syncthing ist der einzige untersuchte Vertreter, der Versionen neben den Nutzdateien ablegt, und hat dafür die durchdachteste Aufräumstrategie. Alles hier belegt aus https://docs.syncthing.net/users/versioning.html:

  • Ablageort: Standardmäßig der Ordner .stversions innerhalb des freigegebenen Verzeichnisses. Ein eigener Pfad ist konfigurierbar, aber die Doku warnt ausdrücklich: „please ensure that it's on the same partition or filesystem as the regular folder path."

  • Namenskonvention: zeitgestempelte Dateinamen.

  • Vier Modi: Trash Can (letzte Fassung, cleanoutDays), Simple (N Fassungen pro Datei mit Zeitstempel), Staggered (altersbasiertes Ausdünnen), External (delegiert an ein Kommando).

  • Der Staggered-Versioner dünnt logarithmisch aus:

    Alter behalten wird
    1 Stunde die älteste Version pro 30-Sekunden-Intervall
    1 Tag die älteste Version pro Stunde
    30 Tage die älteste Version pro Tag
    bis „Maximum Age" die älteste Version pro Woche

    „Maximum Age" = 0 bedeutet: für immer behalten.

Das ist die Antwort auf die Aufräumfrage, falls DioNotes je Historie bekommt: konstanter Overhead statt unbegrenztem Wachstum, ohne Diff-Rekonstruktion, ohne Datenbank. Vier Zeilen Regel.


3. Was ein Append-Log konkret kosten würde

Der Vorschlag aus der Beratungsrunde: ein Append-Log neben jeder Notiz, aus dem die .md als lesbare Sicht materialisiert wird. Undo über Sitzungen, Historie und Konfliktauflösung fielen als Nebenprodukt an.

Der Vorschlag ist technisch seriös — das ist Event Sourcing, und es liefert tatsächlich alles drei. Die Frage ist nicht, ob er funktioniert, sondern was er kostet.

3.1 Größenwachstum — belegte Zahlen

Die maßgebliche Zahl stammt aus dem offiziellen Benchmark-Repo von Kevin Jahns, dem Yjs-Autor, das die Yjs-README selbst verlinkt (https://github.com/dmonad/crdt-benchmarks). Gemessen wird der bekannte B4-Editier-Trace: 182.315 Einfügungen, 77.463 Löschungen, 104.852 Zeichen im Endtext.

Yjs 13.6.11 Automerge 2.1.10
Dokumentgröße gemerged (docSize) 159.929 B ≈ 1,53× Rohtext 129.116 B ≈ 1,23× Rohtext
Summe aller Einzel-Updates (updateSize) 7.306.126 B ≈ 70× Rohtext —
Durchschnittliche Update-Größe 29 B —
Parse-Zeit 39 ms 1.805 ms

Das ist die eigentliche Kostenaussage, und sie ist zweigeteilt (belegt):

  • Ein zusammengefasstes CRDT-Dokument ist erstaunlich billig — 1,2 bis 1,5× des Rohtextes, mit voller Historie. Das ist deutlich günstiger, als man intuitiv erwartet.
  • Ein ungekürzter Append-Log — also genau das, was der Vorschlag beschreibt — kostet rund 70× den Rohtext. Der Unterschied zwischen 1,5× und 70× ist die Compaction. Ein Log, der nur angehängt und nie zusammengefasst wird, liegt beim oberen Wert.

Bei der Skalierung B4×100 (10,5 Mio. Zeichen) bleibt die Dokumentgröße bei 1,53×, der Arbeitsspeicher steigt aber auf 327,1 MB — etwa 31× des Textes (belegt, dieselbe Quelle).

Die offizielle Yjs-FAQ ordnet das ein (https://docs.yjs.dev/api/faq): „Size is very rarely a practical problem as long as you deal with human-entered text input." Das ist ein fairer Einwand gegen eine reine Größen-Argumentation — und der Grund, warum der Ausschlussgrund in 3.4 steht und nicht hier.

Was daraus für DioNotes folgt (Ableitung):

  • Eine Sidecar-Datei pro Notiz. Ein Workspace mit 500 Notizen enthält 1.000 Dateien statt 500. Der Notiz-Baum müsste die Hälfte davon ausblenden; jedes Backup-, Sync- und Suchwerkzeug des Nutzers sieht sie.
  • Autosave koppelt an den Speicherplatz. Die Auslöser-Entscheidung aus #18 würde zur Speicherplatz-Entscheidung: „Autosave nach Tipppause" erzeugt Größenordnungen mehr Log-Einträge als „manuell mit Strg+S". Diese Kopplung existiert bei keiner anderen Option aus Abschnitt 4.

3.2 Aufräumen: bei CRDT-Logs nicht vorgesehen

Der wichtigste Befund dieses Abschnitts, und er ist aus Maintainer-Mund belegt.

Automerge — Peter van Hardenberg (Maintainer) in Issue #799, 24.12.2023, wörtlich:

„'Compact' could mean a few different things here — one is whether you can reduce the on-disk size of an Automerge document. This happens automatically when you call save(). It quite literally compacts the history into a very efficient columnar representation.

Discarding the history is not currently possible without replacing the document: if you don't have the same history, you can't merge the same changes. That's because we use a git-like commit hash for each change to ensure integrity."

„Shallow clone" und „cherry pick" nennt er als nützlich, verwandt und „not yet implemented"; nachgefasst am 23.09.2024: „I don't know when it will happen." Der Automerge-2.0-Blogpost kündigt dasselbe schon seit Januar 2023 an (https://automerge.org/blog/automerge-2/, Abschnitt „History Management"):

„Today the best way to remove something from an Automerge document's history is to recreate the document from scratch or to reset to a time before that change went in."

Orion Henry (Automerge-Core) in Issue #348, 25.04.2022: „the solutions really are to either have the nodes coordinate a total document reset every so often — throwing out the old document and starting fresh". Issue #348 wurde am 21.08.2026 geschlossen mit dem Hinweis auf hexane, den neuen Storage-Engine — der laut offiziellem Blogpost (https://automerge.org/blog/2026-july/) „around 2–9× faster" ist, also Kompression und Performance liefert, keine History-Truncation. Die Schließung bedeutet nicht, dass das Feature existiert.

Yjs — die README nennt den Grund formal (https://github.com/yjs/yjs/blob/main/README.md, Abschnitt „Yjs CRDT Algorithm"):

„CRDTs that are suitable for shared text editing suffer from the fact that they only grow in size. […] We can't garbage collect deleted structs (tombstones) while ensuring a unique order of the structs."

Im Quellcode bestätigt: Item.gc() ersetzt beim normalen Löschen nur den content durch ContentDeleted(len) — das komplette Item-Struct mit id{client,clock}, origin, rightOrigin, parent und der Reihenfolge-Verlinkung bleibt stehen (https://github.com/yjs/yjs/blob/main/src/structs/Item.js). Und das Zusammenfassen von Updates hilft nicht: „Note that this feature only merges document updates and doesn't garbage-collect deleted content" (https://docs.yjs.dev/api/document-updates). Wer Snapshots will, muss GC ganz abschalten — der Code wirft sonst: if (originDoc.gc) { throw new Error('Garbage-collection must be disabled in \originDoc`!') }` (https://github.com/yjs/yjs/blob/main/src/utils/Snapshot.js).

Das Gegenargument, das fairerweise dazugehört. Das offizielle Automerge-Cookbook (https://automerge.org/docs/cookbook/modeling-data/) schreibt:

„Martin Kleppmann did some experiments with a benchmark document to see how much space would be saved by discarding history […] The savings are not all that great, which is why we haven't prioritised history truncation so far."

Das ist ernst zu nehmen — es gilt für Kleppmanns Messfall: ein großes, weitgehend linear getipptes Dokument mit wenigen Actor-IDs. Eine Notiz-App hat das umgekehrte Profil: viele kleine Dokumente, die über Jahre in vielen kurzen Sitzungen angefasst werden, also viele Actor-IDs pro Dokument. Ob die Ökonomie dort kippt, ist für dieses Dokument nicht belegt — die 70×-Zahl aus 3.1 legt es nahe, ist aber am B4-Trace gemessen, nicht am Notiz-Profil.

Fazit dieses Abschnitts: Aufräumen heißt bei einem CRDT-Log entweder gar nichts (Yjs behält Tombstones konstruktionsbedingt) oder „Dokument wegwerfen und neu anfangen" (Automerge). Genau denselben Kompromiss fährt Joplin mit dem Rebase der ältesten überlebenden Revision (2.2) — nur innerhalb einer Datenbank, wo er billig ist. Neben jeder Notiz im Dateisystem ist er es nicht.

3.3 Sidecar-Dateien in Sync-Ordnern — belegte Risiken

Ob die bloße Verdopplung der Dateizahl schadet, ist nicht belegt; die harten Limits sprechen sogar dagegen (Dropbox: „Users shouldn't sync more than 300.000 files" und „1.000.000 write operations per namespace in a 24 hour period", https://help.dropbox.com/plans/large-deployments; OneDrive: „no more than a total of 300.000 items", https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa). Bei typischen Workspace-Größen ist das unkritisch.

Belegt sind dagegen zwei andere Fallstricke, und beide treffen ein Sidecar-Design härter als eine einzelne Datei:

  • Platzhalter-Dateien werden als gelöscht interpretiert. Obsidians offizielle Sync-FAQ (https://help.obsidian.md/sync/faq): „Cloud storage services like OneDrive and Dropbox offer 'Files on-demand' or 'Online-only files' features […] However, since these files are not always available locally, Obsidian Sync will interpret them as deleted, leading to their removal from your remote vault." Ergänzend (https://help.obsidian.md/sync-notes): „iCloud Drive on Windows may lead to file duplication or corruption." Und die architektonische Begründung: „Obsidian requires access to the entire vault for its features […] This makes it difficult for some services to function reliably with Obsidian."
  • Das bloße Nachsehen, ob eine Datei existiert, kann die App einfrieren. Apples Technote TN3150 (https://developer.apple.com/documentation/technotes/tn3150-getting-ready-for-data-less-files): „Materializing a dataless file can take time […] the app may become unresponsive. If an app's main queue remains unresponsive for a long time, the system may terminate the app and trigger a watchdog crash." Als auslösende Aktionen nennt Apple ausdrücklich „enumerating a directory's contents and checking whether a file exists", und: „stat and getattrlist both trigger the materialization of any intermediate folders." Für jede Notiz die zugehörige Log-Datei zu suchen oder zu statten ist genau dieses Muster. (Die Technote gilt für Apple-Plattformen; DioNotes ist Windows-only, aber der Mechanismus — Platzhalter-Dateien, die beim Zugriff nachgeladen werden — ist bei OneDrive Files On-Demand derselbe. Übertragung: Vermutung.)

Apples eigene Empfehlung für mehrteilige Dokumente ist bezeichnend: File Packages statt nebeneinander- liegender Dateien („If your document data format consists of multiple distinct pieces, use a file package", https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/DesigningForDocumentsIniCloud.html). Für DioNotes untauglich — dann wäre die .md keine eigenständige Datei mehr, und genau das ist das Versprechen.

Nicht als Beleg verwendbar (gegengeprüft und verworfen): Zotereos Warnung „extremely likely to corrupt your database" betrifft SQLite und File Locking, nicht Sidecar-Strukturen. git-annex' Warnung gegen Dropbox-Ordner beruht auf Case-Insensitivity und Symlink-Handling. Für DVC gibt es keine offizielle Warnung, nur Community-Threads.

3.4 Das Zwei-Wahrheiten-Problem — der eigentliche Einwand

Dieser Punkt braucht keine externe Quelle, er folgt aus den Rahmenbedingungen des Projekts.

DioNotes hat keinen Filewatcher (#16, Rahmenentscheidung) und rechnet ausdrücklich damit, dass Dropbox, Syncthing oder Git die Dateien ändern, während die App läuft (#20).

Damit gilt für einen Append-Log:

  • Ändert jemand die .md von außen, weiß der Log nichts davon. Der Log sagt „Inhalt X", die Datei sagt „Inhalt Y". Zwei Wahrheiten.
  • Beim nächsten Materialisieren gewinnt der Log — und überschreibt die externe Änderung stillschweigend. Das ist kein Randfall, sondern der Normalfall bei jedem Sync-Konflikt.
  • Der Ausweg wäre, die externe Änderung als neuen Eintrag in den Log einzuspeisen (Diff gegen den zuletzt materialisierten Stand). Das ist machbar — aber es setzt genau die Konflikterkennung voraus, die #20 erst noch entscheiden muss, und macht sie von einer Kann- zu einer Muss-Anforderung.

Die entscheidende Beobachtung: Beim „Datei ist die Wahrheit"-Modell gibt es dieses Problem nicht. Eine Sicherungskopie, die veraltet ist, ist eine weniger nützliche Sicherungskopie — kein inkonsistenter Zustand. Die Datei ist immer richtig, weil sie per Definition die Wahrheit ist. Der Append-Log führt eine Korrektheitslücke ein, die das schlichte Verfahren gar nicht erst hat.

3.5 Bewertung am Versprechen

Der Bewertungsmaßstab aus dem Projektkontext: „Der Nutzer kann das Werkzeug jederzeit verlassen, seine Dateien sind mit jedem Editor lesbar."

Ein Append-Log, aus dem die .md materialisiert wird, degradiert die .md zur Ausgabe. Formal bleibt sie lesbar — praktisch ist sie es nicht mehr im Sinne des Versprechens, denn:

  • Eine Änderung an ihr ist wirkungslos oder wird verworfen, sobald DioNotes das nächste Mal materialisiert. Der Nutzer darf seine Dateien dann nicht mehr mit einem anderen Editor bearbeiten, sondern nur noch lesen.
  • Das Werkzeug zu verlassen, hieße die Historie zurückzulassen — sie steckt im Log, nicht in der Datei.

Der Unterschied zu einer Sicherungskopie ist damit kein gradueller: Eine Sicherungskopie ist zusätzliche Information neben der Wahrheit. Ein Log ist die Wahrheit, und die Datei wird zur Kopie. Das ist genau die Umkehrung, die das Versprechen bricht.


4. Die billigeren Mittelwege

Zur Einordnung: Alle drei sind Aufsätze auf ein unverändertes Schreibverfahren. Keiner verlangt, dass #19 heute irgendetwas anders entscheidet. Sie sind hier aufgeführt, damit die Größenordnung des „später" bekannt ist — nicht als Empfehlung, heute etwas davon zu bauen.

4.1 Eine einzelne Sicherungskopie der Vorfassung

Der billigste Weg: Vor dem Umbenennen die Zieldatei nach <name>.md.bak (oder in .dionotes/bak/) kopieren. Eine Fassung, überschrieben bei jedem Speichern.

  • Kosten: eine Datei pro je gespeicherter Notiz, Größe = Größe der Notiz. Bei 500 Notizen à 4 KB also ~2 MB. Kein Wachstum über die Zeit, weil jede Sicherung die vorige ersetzt. Aufräumen entfällt.
  • Nutzen: deckt genau einen Fall ab — „ich habe gerade etwas kaputtgespeichert". Kein Undo über Sitzungen, keine Historie.
  • Präzedenzfall: Logseq macht im Kern nichts anderes, nur mit sechs statt einer Fassung und nur bei Konflikt (2.3).
  • Fallstrick: Wenn die Sicherung neben der Notiz liegt, sieht der Nutzer sie im Ordner und im Sync — und der Notiz-Baum müsste sie ausblenden, ohne die Punkt-Regel aufzuweichen. In .dionotes/ liegt sie außer Sichtweite, wird aber beim Löschen einer Notiz zur Waise. Beides lösbar, beides eine Entscheidung für später.

4.2 Zeitstempel-Ordner mit logarithmischer Ausdünnung (das Syncthing-Modell)

Ein Ordner .dionotes/versions/<relativer-pfad>/<zeitstempel>.md, ausgedünnt nach dem Muster aus 2.5.

  • Kosten: Volltext-Kopien, aber durch die Ausdünnung mit konstantem Overhead statt unbegrenztem Wachstum. Bei den Syncthing-Buckets landet man pro Notiz nach einem Jahr bei grob 50–100 Fassungen (Abschätzung, nicht belegt) — für eine 4-KB-Notiz also ein paar hundert Kilobyte.
  • Nutzen: echte Historie mit Zeitpunkten, Undo über Sitzungen, kein Diff-Algorithmus nötig.
  • Belegtes Vorbild in Produktion: Syncthings .stversions mit vier Modi und den Ausdünn-Buckets (2.5, https://docs.syncthing.net/users/versioning.html).
  • Warum das die beste Nachrüst-Option wäre: Es braucht keine Datenbank, kein Diff-Format, keine Kettenrekonstruktion wie bei Joplin (2.2) — und es vermeidet Logseqs Aufräumfehler, weil die Ausdünnung altersbasiert statt anzahlbasiert-pro-Datei ist.

4.3 Git-Repository im Workspace

Die Variante, die manche Nutzer heute schon von Hand fahren — und die Logseqs „View page history" bereits voraussetzt (2.3).

Belegte Kosten:

  • Pro Commit entstehen mindestens drei lose Objekte (Blob + Root-Tree + Commit), pro Verzeichnisebene ein weiteres Tree-Objekt. Format: zlib-komprimierter Header + Inhalt unter .git/objects/<2>/<38> (https://git-scm.com/book/en/v2/Git-Internals-Git-Objects).
  • Lose Objekte enthalten den vollen Blob, keine Deltas — Delta-Kompression gibt es erst beim Packen. Eine Ein-Zeichen-Änderung an einer 5-KB-Notiz erzeugt einen kompletten neuen komprimierten Blob (Ableitung aus derselben Quelle).
  • gc.auto = 6700 lose Objekte, gc.autoPackLimit = 50 Packs, gc.pruneExpire = 2.weeks.ago, gc.autoDetach = true (https://git-scm.com/docs/git-config, https://git-scm.com/docs/git-gc).
  • Sobald Housekeeping ausgelöst wird, laufen laut Doku alle Housekeeping-Aufgaben mit (rerere, worktrees, reflog).

Ableitung für DioNotes: 6700 ÷ ~3 Objekte je Commit ≈ ~2.200 Commits bis zum ersten automatischen Repack. Bei „ein Commit pro Speichern" mit Autosave ist das eine Frage von Wochen, nicht von Jahren. Der Repack läuft dann im Hintergrund, schreibt große .pack-Dateien und löscht tausende lose Dateien auf einen Schlag — mitten in einem Ordner, den womöglich Dropbox oder Syncthing beobachtet. Für den Sync-Client sieht das aus wie ein Änderungssturm (Ableitung, mittlere Konfidenz). .git/ gehörte damit zwingend aus dem Sync ausgenommen.

Die Rust-Seite (belegt, crates.io/docs.rs):

Crate Version Downloads gesamt C-Toolchain nötig?
git2 (libgit2-Bindings) 0.21.0 ~110 Mio. Ja — libgit2-sys kompiliert ~180k Zeilen C via cc (kein CMake)
gix / gitoxide 0.87.1 ~44 Mio. Nein — Feature-Doku wörtlich: „No C toolchain is involved."

gix beschreibt sich als „pure Rust implementation of Git", testet Windows in CI („failures do prevent releases") und deckt commit, log, status, diff, reset ab. Der Haken ist der Reifegrad: Der Haupt-Einstiegs- punkt gix steht laut eigenem Stability-Tier-Schema noch auf „Initial Development", entsprechend API-Churn bei jedem Minor. Unter Windows nutzt libgit2 WinHTTP, OpenSSL ist für lokale Repo-Operationen nicht nötig (Ableitung aus der Windows-Verlinkung in build.rs).

Bewertung: Git ist der einzige Weg, der Historie und Diff und Branching liefert — und der einzige, der eine schwergewichtige Dependency plus unbegrenztes Wachstum plus periodisch teures Repacking mitbringt. Für „Undo über Sitzungen" ist es mit Abstand die teuerste Antwort. Bemerkenswert ist, was die untersuchten Editoren daraus machen: Zettlr empfiehlt Git nicht (2.4), Logseq benutzt es nur lesend und setzt voraus, dass der Nutzer es selbst eingerichtet hat (2.3). Niemand richtet dem Nutzer ein Repo ein.

4.4 Kostenvergleich

Option Zusätzliche Dateien Wachstum Aufräumen Dependency Bricht das Versprechen?
Nichts tun (heute) 0 — — keine nein
Eine Sicherungskopie (4.1) 1 pro gespeicherter Notiz keins entfällt keine nein
Zeitstempel-Ordner (4.2) N pro Notiz, ausgedünnt beschränkt Ausdünn-Regel, ~1 Funktion keine nein
Git im Workspace (4.3) .git/, ≥3 Objekte pro Commit unbeschränkt git gc, teuer gix oder git2 nein
Append-Log (Abschnitt 3) 1 Log pro Notiz ≈ 70× Rohtext ungekürzt, ≈ 1,2–1,5× gemerged von den Bibliotheken nicht vorgesehen (Automerge: Dokument neu anlegen; Yjs: Tombstones bleiben) CRDT-/Log-Bibliothek ja — die .md wird zur Kopie

5. Die eigentliche Frage: Schließt temp+rename eine spätere Historie aus?

Nein. Die Begründung in vier Schritten.

5.1 Das Schreibverfahren ist für Historie günstig, nicht hinderlich

Wie in Abschnitt 1 hergeleitet: Historie braucht Auslöser und Vorfassung. Beim temp+rename-Verfahren ist die Vorfassung im Moment vor dem Umbenennen noch vollständig auf der Platte. Der Einbau einer Historie wäre eine Einfügung von wenigen Zeilen an einer Stelle, an der man ohnehin schon steht:

schreibe_notiz(pfad, inhalt):
    prüfe Konflikt (aus #20)
    ── hier ließe sich später die Vorfassung wegsichern ──
    schreibe inhalt nach pfad.tmp
    sync
    benenne pfad.tmp nach pfad um

Das Gegenteil gilt für den naiven Weg: fs::write(pfad, inhalt) zerstört die Vorfassung, bevor irgendjemand sie sichern könnte. Wer dort Historie nachrüstet, muss die Datei zusätzlich vorher lesen. Das ist immer noch billig — aber es ist der teurere der beiden Wege, und er ist zugleich der, den #19 aus Gründen der Absturzsicherheit ohnehin ausschließen will.

Damit ist der Einwand in seiner konkreten Form widerlegt: Die Entscheidung, die #19 zu treffen hat, macht Historie nicht teurer. Sie macht sie billiger.

5.2 Was Historie tatsächlich teuer machen würde

Damit das Ergebnis belastbar ist, hier die Gegenprobe — die Entscheidungen, die den Einwand berechtigt machen würden. Keine davon ist die Wahl zwischen rename und write:

Falle Warum sie teuer wird Kosten sie zu vermeiden
Mehrere Schreibstellen. Autosave schreibt hier, „Notiz verlassen" dort, „Fenster schließen" an dritter Stelle. Historie muss an jeder Stelle nachgezogen werden, und jede vergessene Stelle ist ein Loch in der Historie, das man erst durch einen Bug bemerkt. Eine Funktion, die schreibt. Alle Auslöser aus #18 rufen dieselbe. Kostet heute nichts.
Kein Notiz-Begriff, nur Pfade. Historie, die am Pfad hängt, reißt beim Umbenennen ab (#23). Obsidian hat genau dieses Problem, dokumentiert („snapshots are stored with the absolute path to the note"). Entweder akzeptieren (Obsidian tut es) oder später eine stabile ID einführen. Beides nachrüstbar; die Kette bräche dann nur für Umbenennungen vor der Einführung.
Umbenennen als Löschen+Neuanlegen implementiert. Zerreißt jede Historie und erzeugt Waisen im Ablageort. Umbenennen als Umbenennen implementieren. Ist in #23 ohnehin die naheliegende Wahl.
Der Log-Ansatz aus dem Vorschlag. Siehe Abschnitt 3 — er ist die einzige der hier betrachteten Optionen, die schwer umkehrbar ist. Ihn nicht bauen.

Nur die erste Zeile ist eine echte Auflage an #19 — und sie kostet heute exakt nichts, weil es aktuell noch überhaupt keine Schreibstelle im Code gibt (verifiziert: keine Fundstelle für fs::/write/File:: unter src/; der Editor nimmt Eingaben entgegen und verwirft sie, so beschrieben in #16).

5.3 Die Asymmetrie läuft andersherum

Der Vorschlag aus der Beratungsrunde begründet sich damit, dass die Schreib-Entscheidung schwer umkehrbar sei. Prüft man beide Richtungen, ergibt sich das Gegenteil:

Richtung Aufwand
„Datei ist die Wahrheit" → „Datei plus Sicherungen" Ergänzung. Eine Funktion erweitern, ein Ordner, eine Aufräumregel. Bestehende Notizen funktionieren unverändert weiter, die Historie beginnt eben ab Einführung. Kein Datenmigrations-Schritt, kein Bruch für den Nutzer.
„Log ist die Wahrheit" → „Datei ist die Wahrheit" Migration. Jede Notiz aus dem Log materialisieren, die Logs entfernen oder verwaisen lassen, die gesamte gesammelte Historie wegwerfen oder in ein neues Format überführen — und den Nutzern erklären, warum ihre Historie weg ist.

Wer sich die Option offenhalten will, hält sie also gerade dadurch offen, dass er den Log nicht baut. Der Log ist die Einbahnstraße, nicht das schlichte Schreiben.

Obsidian ist der empirische Beleg: Das Werkzeug wurde 2020 veröffentlicht, hat von Anfang an lose .md-Dateien geschrieben, und File recovery kam später als Core-Plugin obendrauf — vollständig außerhalb des Vault, ohne das Dateiformat oder das Schreibverfahren anzufassen (belegt durch den Aufbau des Plugins, s. 2.1; das genaue Erscheinungsdatum des Plugins ist nicht belegt).

5.4 Belastbarkeit dieser Antwort

Die Antwort steht auf zwei Beinen unterschiedlicher Härte:

  • Hart (belegt): Obsidian betreibt eine produktive Historie für lose .md-Dateien als aufgesetzte Schicht, die vom Schreibverfahren des Kerns entkoppelt ist und außerhalb des Datenordners lebt. Ein Werkzeug mit derselben Ausgangslage wie DioNotes hat das Problem also nachweislich nachträglich gelöst.
  • Weich (Argumentation, nicht durch Quelle belegt): Dass genau eine Schreibstelle die einzige load-bearing Auflage ist, ist eine Schlussfolgerung aus der Zerlegung in Abschnitt 1, kein Zitat. Sie ist aber schwer zu bestreiten: Eine Schicht über N Aufrufstellen einzuziehen kostet N Änderungen, über einer Aufrufstelle eine.

Fazit für #19: Der Einwand ist entkräftet. Die Schreib-Entscheidung darf allein nach den Kriterien aus #19 fallen — Absturzsicherheit, Sichtbarkeit der temporären Datei, Dateirechte, Aufräumen. Historie legt ihr genau eine Auflage auf, und die ist heute gratis: eine einzige Funktion, durch die alles Schreiben läuft.


6. Randbefunde für die Nachbartickets

Bei der Recherche fielen Belege an, die #22 nicht beantworten, aber #19 und #20 direkt betreffen. Sie stehen hier, damit sie nicht verlorengehen — nicht als Entscheidung.

Für #19 (Schreibverfahren):

  • Weder Rust noch Microsoft dokumentieren rename als atomar. Das Wort „atomic" kommt weder in https://doc.rust-lang.org/std/fs/fn.rename.html noch in der MoveFileExW-Doku vor. Rust garantiert nur „replacing the original file if to already exists" und nennt MoveFileExW mit Fallback auf SetFileInformationByHandle. Man verlässt sich auf undokumentiertes NTFS-Verhalten (belegt durch Abwesenheit).
  • Microsoft empfiehlt für genau diesen Anwendungsfall ReplaceFile, nicht MoveFileEx. Aus https://learn.microsoft.com/en-us/windows/win32/fileio/deprecation-of-txf, Abschnitt „Applications updating a single file with 'document-like' data": „A common approach is to write the document to a new file, then replace the original file with the new one. One method to do this is with the ReplaceFile API."
  • Temp+rename verliert Metadaten. Die ReplaceFileW-Remarks führen als Vorteil auf, dass die Funktion creation time, short file name, object identifier, DACLs, security resource attributes, encryption, compression und named streams der Originaldatei erhält — was per Kontrast belegt, dass ein nacktes temp+rename genau diese verliert. Die File ID ändert sich sogar bei ReplaceFile. Kein untersuchtes Rust-Crate (tempfile, atomicwrites, atomic-write-file) nutzt ReplaceFile; atomic-write-file dokumentiert die Verluste immerhin explizit („No support for preserving timestamps, ACLs, extended attributes…"). → Praktische Folge für #19: Ein „erstellt am" gehört ins Frontmatter der .md, nicht ins Dateisystem.
  • Das realistischste Fehlerbild ist ERROR_SHARING_VIOLATION, wenn Defender oder ein Sync-Client die Zieldatei ohne FILE_SHARE_DELETE offen hält (https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew). tempfile::NamedTempFile::persist gibt die temporäre Datei im PersistError zurück — genau für den Retry-Fall gebaut. Ebenfalls belegt: persist synchronisiert weder Inhalt noch Verzeichnis.
  • Die temporäre Datei muss im selben Verzeichnis liegen — fs::rename scheitert über Volume-Grenzen („This will not work if the new name is on a different mount point"), und Syncthing warnt bei seinem eigenen Versions-Ordner mit derselben Begründung.
  • fs-err löst das Problem nicht (es ist nur ein Drop-in-Ersatz für std::fs mit besseren Fehlermeldungen), ist aber die naheliegende Ergänzung, um überhaupt zu sehen, welche Datei die Sharing-Violation geworfen hat.

Für #20 (Zeitstempel-Konflikt):

  • Zettlrs mtime-Regel ist bewusst modtime <= ourModtime → ignorieren, nicht Gleichheitsprüfung, mit wörtlicher Begründung im Code: „to prevent sluggish cloud synchronization services (e.g. OneDrive and Box) from having text appear to 'jump'". Danach folgt ein Inhaltsvergleich, weil Cloud-Dienste Dateien anfassen, ohne den Inhalt zu ändern. Ein reiner mtime-Vergleich fängt das nicht ab.
  • Logseq vergleicht ebenfalls den Inhalt vor dem Schreiben und schreibt bei Abweichung gar nicht. Beide untersuchten Open-Source-Editoren lösen den Konflikt über Inhalt, nicht über Zeitstempel allein.
  • Beide brauchen einen Self-Echo-Filter, damit der eigene Schreibvorgang nicht als externe Änderung zurückkommt (Zettlr: _ignoreChanges-Liste; Logseq: Inhaltsvergleich). Das ist erst relevant, falls je ein Filewatcher dazukommt — aber es erklärt, warum ein Watcher nicht „nur ein Watcher" ist.
  • Dropbox nennt als dokumentierte Ursache Nr. 3 für Conflicted Copies wörtlich „applications with an auto-save feature" (https://help.dropbox.com/organize/conflicted-copy). Das ist ein Argument für #18, Autosave zu entprellen statt pro Tastendruck zu schreiben.
  • Syncthings Konfliktnamen sind <name>.sync-conflict-<date>-<time>-<modifiedBy>.<ext>, und diese Dateien werden „treated as normal files after they are created, so they are propagated between devices" — sie tauchen also im Notiz-Baum auf, wenn nichts sie ausfiltert.

7. Belegt vs. Vermutung — Übersicht

Aussage Status
Obsidian: Snapshots außerhalb des Vault, %APPDATA%\Obsidian\, Volltext, 5 Min / 7 Tage Default Belegt (help.obsidian.md)
Obsidian: Snapshots syncen nicht, absolute Pfade, brechen nach Vault-Umzug Belegt (help.obsidian.md)
Obsidian: Snapshots liegen konkret in IndexedDB/LevelDB Vermutung — starkes Indiz über die Lockdown-Mode-Einschränkung, kein wörtlicher Beleg
Obsidian: Serialisierungsformat der Snapshots Nicht belegbar — Closed Source, nirgends dokumentiert
Obsidian: Schreibverfahren des Kerns (atomar oder nicht) Nicht belegbar — keine offizielle Aussage
Obsidian: Vault.process ist „atomar" nur prozessintern, nicht auf FS-Ebene Lesart — die Doku macht die Unterscheidung nicht explizit
Joplin: SQLite statt .md, revisions-Tabelle, diff-match-patch, 10 Min / 90 Tage Belegt (joplinapp.org + Quellcode)
Joplin: Rebase der ältesten überlebenden Revision beim Aufräumen Belegt (Quellcode-Kommentar + Implementierung)
Joplin: konkrete Größe der Revisions-Tabelle in der Praxis Nicht belegbar — keine offizielle Zahl
Logseq: logseq/bak/<datei>/<ISO>.md, nur bei Konflikt/Schreibfehler/externer Änderung Belegt (Quellcode 0.10.9)
Logseq: „reserve the latest 6 version files", nur beim Schreiben in denselben Ordner Belegt (Quellcode)
Logseq: bak-Ordner werden GB-groß Nicht belegbar — belegt ist unbegrenztes Ordner-Wachstum, keine Primärquelle nennt eine Größe
Logseq: nicht-atomares writeFileSync, chokidar-Watcher, Diff-Modal bei Konflikt Belegt (Quellcode)
Logseq: Undo ist reines In-Memory-Atom, überlebt Neustart nicht Belegt (Quellcode + Doku)
Zettlr: keine eingebaute Historie, Doku verweist auf Cloud-Backup Belegt (docs.zettlr.com)
Zettlr: fs.writeFile, nicht atomar; mtime-Filter „nur neuer, nicht ungleich"; Konflikt-Dialog Belegt (Quellcode master @ 86c6c56)
Zettlr: kein offizielles Git-Plugin, kein Plugin-System Belegt (404 + Maintainer-Aussage in Discussion #3108)
Syncthing: .stversions, Zeitstempel-Namen, vier Modi, staggered-Buckets Belegt (docs.syncthing.net)
Git: gc.auto = 6700, gc.autoPackLimit = 50, ≥3 lose Objekte pro Commit Belegt (git-scm.com)
Git: „~2.200 Commits bis zum ersten Repack" Ableitung aus den belegten Defaults
gix braucht keinen C-Compiler, git2 schon Belegt (docs.rs Feature-Doku, libgit2-sys/build.rs)
Ausgedünnte Zeitstempel-Historie ≈ 50–100 Fassungen pro Notiz nach einem Jahr Abschätzung, nicht belegt
„Historie ist eine Schicht über dem Schreiben" Belegt am Beispiel Obsidian, im Übrigen Argumentation
„Eine Schreibstelle ist die einzige echte Auflage" Argumentation, nicht durch Quelle belegt — aber aus der Zerlegung in Abschnitt 1 zwingend
Zwei-Wahrheiten-Problem des Append-Logs Argumentation aus den Projekt-Rahmenbedingungen (kein Filewatcher, externer Sync erwartet)
CRDT-Dokument gemerged ≈ 1,2–1,5× Rohtext; Summe der Einzel-Updates ≈ 70× Rohtext (B4-Trace) Belegt (dmonad/crdt-benchmarks, Repo des Yjs-Autors)
Automerge: „Discarding the history is not currently possible without replacing the document" Belegt (Maintainer-Zitat, Issue #799)
Automerge: shallow clone / history truncation seit 2023 angekündigt, bis heute nicht implementiert Belegt (Blogpost + Issue-Verlauf + API-Oberfläche)
Yjs: Tombstones sind konstruktionsbedingt nicht sammelbar, Update-Merge räumt nicht auf Belegt (README, Item.js, docs.yjs.dev)
Automerge-Cookbook: „The savings are not all that great" (Gegenargument) Belegt — gilt für Kleppmanns Messfall (großes Dokument, wenige Actor-IDs)
Ob die Ökonomie beim Notiz-Profil (viele kleine Dokumente, viele Sitzungen) kippt Nicht belegt — die 70×-Zahl legt es nahe, ist aber am B4-Trace gemessen
Obsidian Sync: „Files on-demand"-Platzhalter werden als gelöscht interpretiert Belegt (help.obsidian.md)
Apple TN3150: Existenzprüfung/Verzeichnis-Enumeration kann Materialisierung und Watchdog-Crash auslösen Belegt — Übertragung auf OneDrive Files On-Demand unter Windows ist Vermutung
Dass die bloße Verdopplung der Dateizahl in Sync-Ordnern schadet Nicht belegt — die 300.000er-Limits sprechen eher dagegen

Quellenliste

Obsidian

Joplin

Logseq (alle Codebelege gegen Tag 0.10.9)

Zettlr (Codebelege gegen master @ 86c6c56)

Append-Log / CRDT (Automerge, Yjs)

Sidecar-Dateien in Sync-Ordnern

Syncthing

Git und Rust-Anbindung

Windows-Schreibverfahren (für #19, hier nur als Randbefund)