From cb13adce3337d5a419c5991ee747465f8157af69 Mon Sep 17 00:00:00 2001 From: MDietermann Date: Mon, 10 Aug 2026 13:46:03 +0200 Subject: [PATCH] docs: add ADR-0003 on save format, location and migration --- .../adr/0003-save-format-ort-und-migration.md | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 docs/adr/0003-save-format-ort-und-migration.md diff --git a/docs/adr/0003-save-format-ort-und-migration.md b/docs/adr/0003-save-format-ort-und-migration.md new file mode 100644 index 0000000..59425cf --- /dev/null +++ b/docs/adr/0003-save-format-ort-und-migration.md @@ -0,0 +1,304 @@ +# ADR-0003: Save-Format, Speicherort und Migration + +- **Status:** Angenommen +- **Datum:** 2026-08-10 +- **Betrifft:** Persistenz des Meta-Fortschritts (Workshop, Cards, Loadout, Meta-Währung, Wellen-Rekord) +- **Baut auf:** ADR-0002 (nur Quellen werden persistiert; Ablageregel `Resource` vs. `Component`), + ADR-0001 (Balancing-Werte sind in die Binary gebacken) + +## Kontext + +ADR-0002 hat die schwierige Hälfte bereits entschieden: Ein Save enthält **niemals einen +berechneten Wert**, nur Quellen. Damit ist die Datenmenge klein, flach und formelunabhängig — +jede Formeländerung wirkt rückwirkend, ohne Migration. Offen bleiben Format, Ort, Migrationsregel +und das Verhalten bei einem kaputten Save. + +Der Spannungspunkt ist der Ort: Er muss plattformüblich sein (Windows, Linux, macOS heute; Mobile +und Web später), ohne dass der Spielkern eine Plattform-API sieht. + +**Was Bevy 0.19 mitbringt** (an der Quelle geprüft, nicht aus dem Gedächtnis — Belege unten): +Bevy 0.19 hat mit `bevy_settings` ein eingebautes Framework für persistente Nutzerdaten. Es liefert +genau die vier Dinge, die man sonst selbst bauen müsste: + +1. **Pfadauflösung für vier Plattformen** über `bevy_platform::dirs::preferences_dir()`. +2. **Atomares Schreiben** — erst `.toml.new`, dann `fs::rename`. +3. **Feld-Toleranz beim Laden** — Reflect gleicht Feldnamen ab; fehlende Felder behalten ihren + Wert, unbekannte Schlüssel werden ignoriert. Das ist `#[serde(default)]`-Verhalten ohne Attribut. +4. **Web-Pfad** über `localStorage` statt Dateisystem, hinter derselben API. + +Das Framework hat drei Lücken, die diese ADR schließen muss: es gibt **keinen Rückkanal** an die +App, ob Laden oder Schreiben geklappt hat; auf **Android und iOS** persistiert es nichts; und in +`store_fs.rs` fehlt nach einem fehlgeschlagenen `fs::write` ein `return`. + +**Randbedingung aus dem Spielablauf:** Cards sind im laufenden Run über den Pause-Button nur +einsehbar, nicht wechselbar; das Loadout steht vor dem Run fest, und Pause friert die gesamte +Simulation ein. Damit ändert sich **während eines Runs keine einzige persistierte Quelle**. Das +Schreibfenster liegt vollständig zwischen den Runs — es ist keine Politik, sondern eine Folge. + +## Entscheidung + +### 1. Format und Ort: `bevy_settings` mit eigener Fortschrittsdatei + +Der Meta-Fortschritt lebt als `Resource` mit `#[derive(SettingsGroup)]` und +`#[settings_group(file = "progress")]` — also in **`progress.toml`**, getrennt von der späteren +`settings.toml` für Grafik und Audio. Fortschritt und Optionen haben verschiedene Lebensdauern und +verschiedene Löschwege; wer seine Grafikoptionen zurücksetzt, darf nicht seinen Turm verlieren. + +Der App-Name ist die Reverse-Domain der Repo-URL: **`de.hive-brain.mdietermann.infinitower`**. +Daraus ergeben sich die Orte: + +| Plattform | Verzeichnis | Quelle | +|---|---|---| +| Windows | `%LOCALAPPDATA%\\progress.toml` (`FOLDERID_LocalAppData`) | `dirs/windows.rs` | +| Linux | `$XDG_CONFIG_HOME//progress.toml`, sonst `~/.config/...` | `dirs/linux.rs` | +| macOS | `~/Library/Preferences//progress.toml` | `dirs/macos.rs` | +| Web | `localStorage` (kein Dateisystem) | `store_wasm.rs` | +| Android, iOS | **nichts** — `preferences_dir()` liefert `None` | `dirs/mod.rs` | + +**Das Format ist damit TOML, und zwar nicht frei gewählt, sondern der Preis des Frameworks.** Die +Abwägung fällt trotzdem für Klartext aus: Ein Spieler kann `progress.toml` mit jedem Editor +aufmachen und sich Workshop-Level schenken. Es gibt kein Leaderboard, keinen Server, keine +Ökonomie und genau einen Spieler — der Schaden ist null, und der Nutzen beim Debuggen eines +Lernprojekts ist real. Verschleierung (Base64, Prüfsumme, Binärformat) hält keinen auf, der es +ernst meint, und kostet genau die Lesbarkeit, die hier den Wert ausmacht. Schummeln wird +ausdrücklich akzeptiert, nicht übersehen. + +### 2. Der Sentinel: `version` als erstes Feld + +Das erste Feld ist `version: u32` mit `Default = 0`. Beim Schreiben steht dort **immer** der +aktuelle Wert (heute `1`), nie `0`. Weil das Framework der App nicht mitteilt, ob geladen wurde, +ist dieses Feld der Rückkanal — er liegt in den Daten statt in der API. + +Nach dem Laden verzweigt genau ein System dreifach: + +| Gelesen | Bedeutung | Verhalten | +|---|---|---| +| `0` | Es wurde **nichts** geladen: erster Start **oder** unlesbare Datei | Meldung an den Spieler, **kein Schreiben** bis zur ersten bewussten Aktion | +| `== CURRENT` | Normalfall | Normal weiter | +| `0 < v < CURRENT` | Älterer Stand mit Bedeutungsänderung | Migrationsschritt läuft **vor** dem ersten Schreiben | +| `> CURRENT` | Save aus einer neueren Programmversion | Alle Schreibvorgänge für die Sitzung gesperrt, Meldung an den Spieler | + +`0` ist bewusst zweideutig: Der Unterschied zwischen „erster Start" und „konnte nicht lesen" ist +plattformübergreifend nicht feststellbar, weil eine Existenzprüfung auf Dateiebene im Web keine +Entsprechung hat. Statt die Zweideutigkeit zu verstecken, wird sie ausgesprochen — siehe Punkt 4. + +### 3. Regel: niemals schreiben, bevor bestätigt gelesen wurde + +**Solange nicht feststeht, dass ein vorhandener Stand gelesen wurde, schreibt das Programm nicht.** +Diese Regel ist wichtiger als die Wahl des Frameworks und gilt auch, wenn Punkt 1 später fällt. + +Der Grund ist nicht Robustheit, sondern Respekt vor investierter Zeit: Ein Save, der still durch +Defaults überschrieben wird, entwertet rückwirkend jede Stunde, die darin steckt — und der Spieler +erfährt es nie. Ein Programm, das stattdessen einen Satz sagt und wartet, kann beendet werden, +bevor Schaden entsteht, und die Datei liegt noch da. + +### 4. Migration: Feldnamen sind der Vertrag, `version` ist die Notausfahrt + +| Änderung | Was passiert | Nötig | +|---|---|---| +| Feld kommt hinzu | Schlüssel fehlt in der Datei → `Default` bleibt stehen | nichts | +| Feld fällt weg | Schlüssel ist unbekannt → wird ignoriert, verschwindet beim nächsten Schreiben | nichts | +| Feld ändert seine **Bedeutung** | Reflect wendet den alten Wert widerspruchslos **falsch** an | **Verbot, siehe unten** | + +Für die ersten beiden Fälle reicht die Feld-Toleranz des Frameworks; ein Versionsfeld wäre dafür +überflüssig. Der dritte Fall ist der, den `#[serde(default)]`-artige Toleranz prinzipbedingt nicht +abfangen kann — der Schlüssel passt ja. + +**Verbindlich: Eine Bedeutungsänderung erzwingt einen neuen Schlüsselnamen, nie eine +Neuinterpretation des alten.** Umbenennen heißt: alter Schlüssel wird ignoriert, neuer Schlüssel +startet auf `Default`. Der Spieler verliert **einen Wert**, nicht seinen Spielstand. Nur wenn genau +dieser eine Wert nicht verloren gehen darf, wird `version` erhöht und ein expliziter +Migrationsschritt geschrieben, der vor dem ersten Schreiben läuft. + +Daraus folgt eine zweite verbindliche Regel: **Die Ablage ist namensbasiert, nie positionsbasiert.** +Workshop-Upgrades und Cards stehen als je ein benanntes Feld in der Datei, nicht als Liste mit +Indizes. Eine positionsbasierte Liste macht das Einfügen eines Upgrades in der Mitte zu einer +stillen Bedeutungsänderung aller nachfolgenden Werte — genau der Fall, den das Verbot verhindern +soll, nur unsichtbar. + +`version` wird also von Tag eins mitgeschrieben, obwohl es im Normalbetrieb nichts tut. Der Grund +ist, dass man es nicht nachrüsten kann: Eine Datei ohne Versionsfeld ist von `version = 0` nicht zu +unterscheiden. + +### 5. Was persistiert wird + +| Feld | Typ | Herkunft | +|---|---|---| +| `version` | `u32` | Sentinel, siehe Punkt 2 | +| `workshop_levels` | Struct mit **einem benannten Feld je Upgrade** | Workshop-Käufe zwischen Runs | +| `card_levels` | Struct mit **einem benannten Feld je Card**, `0` = nicht besessen | fasst „besessen" und „Stufe" in einem Feld zusammen | +| `equipped_cards` | Liste von Card-Bezeichnern (Namen, keine Indizes) | Loadout, steht vor dem Run fest | +| `card_slots` | `u8` | freigeschaltete Slot-Zahl | +| *Meta-Währung* | ganzzahlig | **Name offen** — `CONTEXT.md` hat für diese Ressource noch keinen Begriff; der Feldname folgt dem Glossar, sobald er feststeht | +| `highest_wave` | `u32` | höchste je erreichte Welle | + +**Kein Zeitstempel.** Ohne Offline-Progress liest ihn niemand, und ein Feld ohne Leser ist die +Miniaturausgabe genau des Fehlers, den ADR-0002 verbietet — gespeicherter Zustand, den niemand +braucht. + +**Kein laufender Run.** Keine In-Run-Upgrades, kein Wellenstand, keine Coins, kein `EffectiveStats`. + +**Kein `Balance`.** `Balance` ist eine `Resource`, wird aber nie persistiert: Ihre Werte sind laut +ADR-0001 in die Binary gebacken, und ein Save darf nicht auf Werte zeigen, die es im aktuellen +Build nicht mehr gibt. + +### 6. Verhältnis zur Ablageregel aus ADR-0002 + +Die Regel hält, aber sie ist **einseitig** und wird hier präzisiert: + +> Persistiert ⊆ `Resource`. Nicht jede `Resource` ist persistiert. + +`Balance` ist der Gegenbeleg (Punkt 5). Die Markierung ist deshalb nicht „ist eine `Resource`", +sondern das `SettingsGroup`-Derive — am Typ ablesbar, nicht aus der Ablage zu erraten. + +**Es gibt genau eine Ausnahme, und sie ist benannt:** `highest_wave` und die im Run verdiente +Meta-Währung entstehen *innerhalb* eines Runs, müssen ihn aber überdauern. Sie sind trotzdem keine +Run-Daten, sondern Meta-Werte, die aus Run-Daten abgeleitet werden. Verbindlich: **Dieser Übergang +findet genau einmal statt, am Run-Ende, in einem einzigen System.** Kein anderes System schreibt +aus Run-Zustand in eine persistierte `Resource`. Alles Übrige folgt ADR-0002 unverändert. + +Die Randbedingung „Cards im Run nur einsehbar" entzieht ADR-0002 Punkt 10 nebenbei ihren Fall 1 +(Card-Wechsel im laufenden Run). Der Abgleich in `sync_tower_abilities` bleibt richtig; er hat nur +einen Auslöser weniger. Das ist eine Beobachtung, keine Änderung an ADR-0002. + +### 7. Wann geschrieben wird + +- Nach **jeder** Änderung einer persistierten `Resource`: `SaveSettingsDeferred` (1 s Debounce). +- **Zusätzlich** beim Beenden: `SaveSettingsSync::IfChanged`. Beides zusammen, nicht eines von + beiden — die Bevy-Doku weist ausdrücklich darauf hin, dass ein Exit-Event auf manchen Plattformen + (etwa Cmd-Q auf macOS) nicht abgefangen werden kann. +- **Nicht periodisch, nicht bei Pause.** Während eines Runs ändert sich keine persistierte Quelle + (Punkt 0 im Kontext), und Pause friert ohnehin alles ein. Es gibt schlicht nichts zu speichern. + +Damit fällt in der Praxis **ein** Schreibvorgang pro Run an (Run-Ende) plus je einer pro Kauf oder +Loadout-Änderung zwischen den Runs. + +**Atomares Schreiben ist nötig und kostet hier nichts**, weil das Framework es bereits tut: Bei +einer Datei unter 2 KB ist der Preis eine zusätzliche `write`- und eine `rename`-Operation, und der +Schutz gegen einen Absturz mitten im Schreiben ist strukturell statt hoffnungsbasiert. + +### 8. Die vier Fehlerfälle + +| Fall | Was technisch passiert | Was der Spieler sieht | +|---|---|---| +| **Beschädigte Datei** | `decode_toml_file` loggt `Error parsing settings file` und gibt `None`; alle Resources bleiben auf `Default`, also `version == 0` | „Kein Fortschritt geladen. Es wird ein neuer Spielstand angelegt, sobald du fortfährst." — und bis dahin wird **nicht** geschrieben (Punkt 3) | +| **Halb geschriebene Datei nach Absturz** | Kann `progress.toml` nicht erreichen: geschrieben wird `progress.toml.new`, erst `fs::rename` macht es gültig. Ein Absturz hinterlässt den alten Stand plus eine verwaiste `.new` | nichts — der alte Stand lädt normal | +| **Save aus neuerer Programmversion** | Unbekannte Schlüssel werden ignoriert, bekannte angewandt; `version > CURRENT` fängt das ab | „Dieser Spielstand stammt aus einer neueren Version." Alle Schreibvorgänge sind für die Sitzung gesperrt, die Datei bleibt unangetastet | +| **Fehlende Schreibrechte** | `fs::write` bzw. `fs::rename` schlagen fehl, werden geloggt, und die App erfährt es nicht | **Nichts** — und das ist die ehrliche Lücke dieser Entscheidung. Es gibt in 0.19 keinen Rückkanal. Deshalb zeigt der Slice ausdrücklich **keine** „Gespeichert"-Bestätigung: eine Erfolgsmeldung, die das Programm nicht belegen kann, ist schlimmer als gar keine | + +Der Fall, der bestehenden Fortschritt aktiv zerstören kann, ist keiner dieser vier, sondern eine +Verkettung: In `store_fs.rs` fehlt nach einem fehlgeschlagenen `fs::write` ein `return`, sodass +`fs::rename` die möglicherweise abgeschnittene Temp-Datei trotzdem über den intakten Save schiebt. +Auslöser wäre eine volle Platte oder ein Quota. Der Schutz ist einseitig: Parst die abgeschnittene +Datei nicht mehr, fängt sie Fall 1 ab und es wird nichts überschrieben. **Ob eine abgeschnittene +TOML-Datei immer ungültig ist, wurde nicht geprüft und ist unwahrscheinlich** — eine Datei, die +nach einer vollständigen Zeile endet, parst tadellos und hat den Rest still auf `Default`. Genau +deshalb steht dieser Punkt unten als Revisions-Auslöser und nicht als gelöstes Problem. + +## Belegtiefe + +Jede tragende Behauptung mit ihrer Quelle. Alle Dateipfade beziehen sich auf das Bevy-Repo am Tag +**`v0.19.0`**. + +| Behauptung | Beleg | Status | +|---|---|---| +| `bevy_settings` existiert im Engine-Repo, erreichbar als `bevy::settings` hinter dem **nicht-default** Feature `bevy_settings` | `crates/bevy_settings/src/lib.rs`; docs.rs `bevy/0.19.0/bevy/settings/`; Feature-Liste docs.rs `crate/bevy/0.19.0/features` | belegt | +| Attribut `#[settings_group(file = "")]`, Default `settings`, ergibt `.toml` | `lib.rs`, Doc-Kommentar am Trait `SettingsGroup`; verwendet im Test `#[settings_group(file = "audio")]` | belegt | +| `preferences_dir()` liefert auf Android und iOS `None` | `crates/bevy_platform/src/dirs/mod.rs`: `#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] pub fn preferences_dir() -> Option { None }`. `store_fs.rs` setzt daraufhin `base_path: None`; `save` und `load` sind dann No-Ops mit `warn!("Could not find user configuration directories")` | belegt | +| Pfade: Windows `FOLDERID_LocalAppData`, macOS `~/Library/Preferences`, Linux `$XDG_CONFIG_HOME` bzw. `~/.config` | `dirs/windows.rs` (`SHGetKnownFolderPath`), `dirs/macos.rs`, `dirs/linux.rs` | belegt | +| Atomares Schreiben über Temp-Datei plus `rename` | `store_fs.rs`: `let temp_path = base_path.join(format!("{filename}.toml.new"));` … `fs::rename(&temp_path, file_path)` | belegt | +| In `store_fs.rs` fehlt nach fehlgeschlagenem `fs::write` ein `return`, `fs::rename` läuft trotzdem | `store_fs.rs`, in `save` und `save_async`: `if let Err(e) = fs::write(&temp_path, …) { error!(…); }` — ohne `return`, direkt gefolgt vom `fs::rename` | belegt **durch Code-Lesen**; ein tatsächlicher Datenverlust wurde nicht beobachtet und der Upstream-Bugtracker nicht geprüft | +| Kein Rückkanal an die App über Lade- oder Schreiberfolg | `SettingsStore::{save, save_async, load}` sind `pub(crate)` und geben nichts zurück; `load_settings_file` gibt `()` zurück; das Modul exportiert kein Fehler-Event und keine Status-Resource | belegt | +| Fehlerbehandlung ist **`error!`** für Lese- und Parse-Fehler, **`warn!`** für fehlende Datei und fehlendes Verzeichnis | `store_fs.rs`: `error!("Error reading settings file: …")`, `error!("Error parsing settings file: …")`; `lib.rs`: `warn!("Filename {filename}.toml not found")` | belegt — **korrigiert** gegenüber meiner früheren, ungenauen Aussage „nur `warn!`" | +| Fehlende Felder behalten ihren Wert, unbekannte Schlüssel werden ignoriert | `lib.rs`, `load_properties`, Kommentar wörtlich: *„Deserialize matching field names, ignore ones that don't match."* — die Schleife läuft über die Reflect-Feldnamen und schlägt jeden in der TOML-Tabelle nach | belegt | +| Deferred-Save mit 1 s Debounce; `SaveSettingsSync::IfChanged`; Empfehlung, beides zu nutzen | `lib.rs`: `SaveSettingsDeferred(pub Duration)` mit `Duration::from_secs(1)` als `Default`; `SaveSettingsSync::IfChanged`; Doc-Kommentar am `SettingsPlugin` zum Cmd-Q-Fall auf macOS | belegt | +| Persistierte Typen brauchen `#[reflect(Resource, SettingsGroup, Default)]` | `lib.rs` ruft `ty.data::().unwrap()`, wenn die Resource noch nicht existiert — ein fehlendes `ReflectDefault` ist damit ein Panic, kein stiller Fehler | belegt | +| Ob eine abgeschnittene TOML-Datei zuverlässig ungültig ist | — | **nicht geprüft, wird nicht angenommen** | +| Ob `bevy_settings` in einer späteren Bevy-Version einen Rückkanal bekommt | — | **nicht geprüft** | + +## Verworfene Alternativen + +- **Eigenes Save-Modul mit `ron` und `serde`** (beide laut ADR-0001 bereits im `Cargo.lock`). Der + Rückkanal wäre ein echter Gewinn — man hielte `io::Result` in der Hand. Der Preis ist alles + andere: eine eigene `preferences_dir()`-Entsprechung für drei Betriebssysteme (Bevys Version ist + nach `dirs-sys` modelliert und braucht auf Windows `unsafe` um `SHGetKnownFolderPath`), ein + eigener `localStorage`-Pfad für Web, eigenes Temp-and-Rename und eine eigene Feld-Toleranz — für + sechs Felder Nutzdaten. Dazu bliebe es dauerhaft eigene Wartungslast, während der `store_fs`-Bug + stromaufwärts gefixt wird und beim nächsten Bevy-Bump gratis ankommt. +- **`dirs` (6.0.0) oder `directories` als Dependency plus eigenes Schreiben.** Löst nur den Ort, + nicht Web, nicht Atomarität, nicht die Feld-Toleranz — und `bevy_platform::dirs` ist ohnehin nach + demselben Vorbild gebaut und liegt bereits im Baum. +- **Alles in `settings.toml`, ohne eigene Datei.** Spart ein Attribut und koppelt Fortschritt an + Grafikoptionen. „Einstellungen zurücksetzen" würde den Spielstand mitnehmen. +- **Verschlüsselung oder Prüfsumme gegen Schummeln.** Kauft nichts (Einzelspieler, kein + Vergleichswert verlässt das Gerät) und kostet die Lesbarkeit, die beim Debuggen den Wert ausmacht. +- **Positionsbasierte Listen für Workshop-Level und Cards.** Kompakter in der Datei, aber ein + eingefügtes Upgrade verschiebt still die Bedeutung aller folgenden Werte — die Fehlerklasse, die + Punkt 4 verbietet, nur ohne Symptom. +- **Speichern bei jeder Änderung ohne Debounce.** Unnötig: Zwischen den Runs ändert sich wenig, im + Run nichts, und `SaveSettingsDeferred` fasst Klick-Serien im Workshop ohnehin zusammen. + +## Konsequenzen + +**Positiv** + +- Keine deklarierte Dependency; nur ein Cargo-Feature (siehe unten). +- Der Spielkern sieht keine Plattform-API — nur ein `#[derive]` und eine `Resource`. Die + Plattformfallunterscheidung liegt vollständig in `bevy_settings`. +- Web funktioniert ohne eigenen Code, weil `localStorage` hinter derselben API liegt. +- Atomares Schreiben, Feld-Toleranz und Pfadauflösung sind fremdgewartet. +- Der Save ist im Editor lesbar — ein Fehlerbild lässt sich in Sekunden verifizieren. + +**Negativ** + +- **Android und iOS persistieren nichts** und melden es nur als `warn!`. Der Mobile-Port erbt ein + stumm kaputtes Save/Load und braucht eigene Arbeit. `CONTEXT.md` nennt Mobile als nächsten Schritt + nach dem Slice. +- Ein fehlgeschlagener Schreibvorgang ist für die App unsichtbar. Deshalb gibt es keine + „Gespeichert"-Anzeige — der Slice verzichtet lieber auf Rückmeldung als auf Wahrheit. +- `version == 0` kann „erster Start" oder „kaputte Datei" heißen. Die Meldung an den Spieler muss + beides abdecken und darf keines von beidem behaupten. +- Ein *Settings*-Framework trägt Save-Daten. Das passt, solange der Fortschritt aus flachen + Skalaren besteht (ADR-0002 Punkt 5). Sobald ein Save Entities oder Weltzustand enthielte, wäre es + das falsche Werkzeug — und ADR-0002 müsste zuerst fallen. +- Die persistierten Typen sind an `Reflect` gebunden; ein vergessenes `#[reflect(… Default)]` ist + ein Panic beim Start statt eines Compile-Fehlers. + +**Neue Dependencies** + +Keine deklarierte Dependency. Nötig ist das **nicht-default Cargo-Feature `bevy_settings`**: + +```toml +bevy = { version = "0.19.0", features = ["bevy_settings"] } +``` + +Transitiv kommt dadurch genau **eine** Kiste neu in den `Cargo.lock`: die Fassade **`toml` +(^1.1.0)**. Ihr Unterbau liegt bereits dort (`toml_edit` 0.25.13, `toml_parser` 1.1.3, +`toml_datetime` 1.1.1), ebenso `serde`, `thiserror`, `web-sys` und `windows-sys`. Der Zuwachs an +Compile-Zeit ist damit auf eine dünne Fassadenkiste begrenzt — das ist der Grund, warum diese +Entscheidung nicht gegen die Dependency-Zurückhaltung aus ADR-0001 verstößt. + +**Folgeaufgaben** (nicht Teil dieser ADR) + +- Der Feldname für die Meta-Währung folgt, sobald `CONTEXT.md` den Begriff festlegt. +- Die Reihenfolge der Plugins: `SettingsPlugin` lädt beim Hinzufügen, also vor allem, was die + geladenen Werte lesen will. + +## Revisions-Auslöser + +Diese Entscheidung wird neu getroffen, sobald **eines** der folgenden Ereignisse eintritt: + +1. **Truncated-Promote beobachtet:** Ein Log-Eintrag `Error saving settings file` erscheint, **oder** + eine Datei `progress.toml.new` bleibt im Einstellungsverzeichnis liegen. Beides ist der direkte + Beleg, dass der fehlende `return` in `store_fs.rs` gegriffen hat. Dann wird das Schreiben + übernommen — Eigenbau nur für den Schreibpfad, Laden bleibt beim Framework. +2. **Mobile im Scope:** Sobald ein Android- oder iOS-Build gebaut werden soll. `preferences_dir()` + liefert dort `None`, und der Fortschritt verschwindet lautlos. Kein Grenzwert, ein Build reicht. +3. **Ein Save muss mehr als flache Skalare enthalten** — Entities, Weltzustand, ein laufender Run. + Dann ist nicht dieses Format falsch, sondern zuerst ADR-0002 Punkt 5 neu zu bewerten. +4. **Ein Vergleichswert verlässt das Gerät** (Leaderboard, geteilter Wellen-Rekord, Cloud-Save). + Ab diesem Moment hat Schummeln ein Opfer, und die Klartext-Entscheidung aus Punkt 1 ist neu zu + treffen — nicht vorher. +5. **Eine zweite Bedeutungsänderung innerhalb eines Slice** erzwingt eine `version`-Erhöhung. Einmal + ist eine Notausfahrt; zweimal heißt, dass die Feldnamen den Vertrag nicht mehr tragen, und dann + wird ein echtes Migrationsschema fällig statt einer Umbenennungsregel.