docs: ticket 04 (fehlerstrategie) aufloesen, vorgaben an ticket 11

This commit is contained in:
2026-08-12 09:45:55 +02:00
parent d2d5ae6d8c
commit 145b82bc08
3 changed files with 117 additions and 1 deletions

View File

@@ -1,7 +1,7 @@
# 04 — Fehlerstrategie für kaputte Mods
Type: grilling
Status: open
Status: resolved
Blocked by: 03
## Question
@@ -32,3 +32,110 @@ Offen bleibt damit: kaputte TOML-Syntax (die Datei ist unlesbar, die Einheit „
fehlende Pflichtfelder, unaufgelöste Referenzen aus Phase 3 — und wohin die Warnungen gehen, wenn der
Bildschirm ratatui gehört. Ein Ladelauf produziert leicht ein Dutzend, und `println!` ist nach dem
TUI-Start keine Option mehr.
## Answer
### Zwei Klassen, zwei Einheiten
**Syntax ist ein Datei-Fehler, alles Semantische ein Eintrags-Fehler.**
Das ist nicht gratis: `toml::from_str::<ModFile>()` mit `items: Vec<Item>` scheitert *komplett*, wenn
ein einziger Eintrag ein kaputtes Feld hat — das Versprechen aus Ticket 03 wäre damit genau dann
gebrochen, wenn 200 Gegenstände in einer Datei stehen, also im Normalfall. Deshalb wird zweistufig
deserialisiert: erst `Vec<Spanned<toml::Value>>` je Registry-Tabelle, dann jeder Eintrag einzeln in
seinen Typ. Etwa zehn Zeilen, und ein falsches Feld trifft nur noch seinen Eintrag.
Bei kaputter TOML-Syntax fällt die ganze Datei — da kommt der Parser nicht weiter, das ist keine
Entscheidung, sondern eine Tatsache.
### Harte Fehler — genau drei
`depends` nicht installiert · `depends`-Reihenfolge verletzt · `core` nicht gefunden. Alle drei:
Meldung auf stderr, `exit(1)`. Das TUI startet gar nicht erst, damit ist stderr genau der richtige Ort.
`core` ist darüber hinaus **kein Sonderfall**: Ein kaputter Eintrag darin ist eine Warnung wie überall.
Ein Sonderfall würde die Privilegierung wieder einbauen, die Ticket 03 gerade abgeschafft hat. Die
Absicherung gehört nicht in die Laufzeit, sondern in einen Test — siehe unten. Dass `core` fehlend
hart ist, widerspricht dem nicht: Ohne `core` gibt es kein Attribut, keinen Slot und keinen
Gegenstand, das ist nicht Privileg, sondern die Abwesenheit sämtlichen Inhalts.
Ein nicht schreibbares Log ist ausdrücklich **kein** harter Fehler. Der Schreibfehler wird verschluckt;
ein Spiel, das wegen seiner Logdatei nicht startet, wäre die Karikatur guter Fehlerbehandlung.
### Warnungen
Während des Ladens in einem `Vec<String>` gesammelt, am Ende nach
`data_dir()/rustkeep/logs/load-20260812-112305.log` geschrieben. Die letzten zehn bleiben liegen,
ältere werden beim Schreiben gelöscht — streng auf das Muster `load-*.log` im eigenen `logs/`-Ordner
beschränkt. Ein sauberer Lauf legt keine Datei an.
Auf dem Startbildschirm steht **eine Zeile** mit Anzahl und Dateiname. Kein Report-Screen: Das wäre
ein Screen mit Scrolling und Navigation, den Ticket 11 mitschleppen müsste, für Inhalt, den ein
Texteditor besser anzeigt.
Keine Schweregrade. Jeder Fall aus 03 und aus der Deserialisierung ist „Warnung, verworfen" — eine
zweite Stufe wäre ein Feld, das überall denselben Wert trägt, plus die Frage, was `INFO` auslöst.
### Meldungsformat
```
meinmod/items.toml:42: core:iron_sword — unknown field `base_pric`
```
Die **Mod-ID steht vorne**: Bei zwölf installierten Mods ist sie die einzige Angabe, mit der ein
Spieler weiß, wen er anschreiben muss.
`toml::de::Error` liefert `span() -> Option<Range<usize>>` und `message()`, aber keine Zeile — die ist
`input[..span.start].lines().count()`, eine Zeile Code. Keine Spalte: `message()` nennt das Feld
bereits im Klartext, die Spalte wäre doppelt gemoppelt und bräuchte eine zweite Rechnung. Ein Eintrag
ohne lesbare `id` kann sich nicht benennen und heißt nach seiner Position: `items[3]`.
### Rust-Seite
Handgeschriebenes `enum LoadError` mit drei Varianten und eigenem `impl Display`.
`thiserror` wäre ein Proc-Macro-Dependency (zieht `syn` und `quote` in den Build) für ungefähr zehn
gesparte Zeilen `match` — und in einem Ticket, das ausdrücklich Rust lernen soll, ist das Schreiben
dieses `match` der Zweck. `anyhow` ist für Anwendungen gedacht, denen der konkrete Fehlertyp egal ist;
hier ist er es nicht, es sind genau drei benannte Fälle. `impl std::error::Error` bleibt weg, solange
niemand `Box<dyn Error>` braucht.
```rust
fn load_mods() -> Result<(Registries, Vec<String>), LoadError>
```
Warnungen reisen als zweiter Rückgabewert. Als Feld in `Registries` würden sie Diagnose an die
Spieldaten hängen, die danach das ganze Spiel lang mitfahren und in jedem Save-Gedanken auftauchen;
als `&mut Vec<String>`-Parameter wäre es das C-Muster. Ein Tupel aus zwei Dingen braucht keinen
`LoadReport`-Typ — den gäbe es für genau eine Aufrufstelle.
Die `Result`-Kette endet **vor** `main`: `eprintln!("{e}")` plus `exit(1)`. `main() -> Result<…>`
druckt über `Debug` und setzt dem Spieler `LoadError { .. }` samt „Error: " vor.
### Panic-Grenze
Die Grenze ist die **Datenherkunft**, nicht die Schwere:
- Alles, was aus einer TOML kommt, ist `Result`.
- Alles, was aus dem eigenen Code kommt, darf panicken — Index in eine selbst gebaute `Vec`,
`unreachable!` im `match` über ein eigenes Enum. Ein Panic dort ist ein Engine-Bug und soll laut
sein; ihn in ein `Result` zu verpacken hieße, den Aufrufer nach etwas zu fragen, das er nicht
beantworten kann.
`unwrap()` auf Modder-Daten ist ausnahmslos falsch — genau das tut `items.rs::parse()` heute.
### Check
Ein `#[test]`, der `mods/core/` lädt und auf **null Warnungen** prüft. Damit fällt ein kaputtes
Kernspiel beim `cargo test` auf statt beim Spieler, und die Ladelogik bleibt frei von Sonderregeln.
Das ist die eine runnable Prüfung, die dieses Ticket hinterlässt.
### Auflagen an andere Tickets
- **11 UI-Gerüst und Screen-Modell**: Der Startbildschirm trägt eine Zeile mit Warnungsanzahl und
Logdateiname. Eine Zeile, kein Screen.
### Folgen für bestehenden Code
`items.rs::parse()` verliert sein `panic!`, `main()` bekommt den stderr-plus-exit-Pfad, die
`logs/`-Verwaltung kommt neu dazu.

View File

@@ -24,3 +24,11 @@ Entscheidungen darüber stehen noch aus:
- **Welche Screens gibt es zum Start**, und welche Aktionen kennt `Game::apply(Action)`?
Ergebnis: Screen-Enum, Stack-Modell, Action-Liste und die Regel, was `game/` nicht importieren darf.
## Vorgaben aus anderen Tickets
- **02**: Charakterbogen und Charakterbau sind Registry-Schleifen, keine gemauerten Layouts —
Begründung in [Ticket 02](02-typ-registry.md).
- **04**: Der Startbildschirm trägt eine Zeile mit Warnungsanzahl und Logdateiname
(„12 Ladewarnungen — `logs/load-20260812-112305.log`"). **Eine Zeile, kein Screen** — der Inhalt
gehört in einen Texteditor, nicht in ein TUI mit Scrolling und Navigation.

View File

@@ -62,6 +62,7 @@ auch über Ticket 13.
- [01 — Effekt-Vokabular](issues/01-effekt-vokabular.md) ([#2](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/2)) — **Zwei Begriffe statt einem**: Modifikator (rechnet ein Attribut um, solange er existiert) und Aktion (ändert Zustand einmalig), getrennte Maschinen und getrennte TOML-Arrays. Fester Attributkatalog ohne Namensraum; `max_health` ist Attribut, der Lebensstand ist Zustand. Sechs Aktionen, `damage` eigenständig wegen der Minderungskette. Stapeln `(Basis + Σ flach) × (1 + Σ prozentual)`. Eine Uhr: der Zug. Sieben feste Schadenstypen, ein Minderungsmechanismus (`resistance`), `armor` nur Kurzschreibweise. Auren wohnen beim Träger. Bedingungsvokabular bewusst an Ticket 08 delegiert. Begriffe im [Glossar](../../CONTEXT.md).
- [02 — Typ-Registry statt Enums](issues/02-typ-registry.md) ([#3](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/3)) — **Es gibt keinen Item-Typ mehr**: `enum ItemType` vermischte Slot, Verhalten und Kategorie und entfällt ersatzlos zugunsten der Felder `slot`, `stackable`, `consumed_on_use`. `ItemWeapon` und `ItemArmor` fallen weg, ein Gegenstand ist einheitlich. **Fähigkeit** kommt als dritter Begriff neben Modifikator und Aktion dazu (Angebot statt Wirkung). Fünf moddbare Registries; Slots und sogar Charakteroption-Kategorien sind Inhalt, mit `pick = N`. Startausrüstung ist `on_create` als gewöhnliche Aktionsliste. `value` → `base_price`, `shop_price` → `price_modifier`.
- [03 — Mod-Ladepipeline und Manifest](issues/03-mod-ladepipeline.md) ([#4](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/4)) — Zwei Suchpfade: `mods/` neben der Binary (enthält `core/`) und `data_dir()/rustkeep/mods` über die Crate `directories`; bei gleicher ID gewinnt das Profil, mit Warnzeile. **`core` liegt als echter Mod-Ordner auf der Platte, `include_str!` entfällt.** `manifest.toml` mit vier Feldern (`id`, `name`, `version`, `depends`) — TOML statt YAML, weil `serde_yaml` und der Fork `serde_yml` beide archiviert sind. Ladereihenfolge aus einer selbstheilenden Spielerliste `load_order.toml`; `depends` sortiert nicht, sondern prüft, und eine Verletzung ist harter Startfehler. Jede `*.toml` im Mod wird eingelesen, jede darf jede Registry-Tabelle enthalten. Präfix wird automatisch vorangestellt, Überschreiben über `overrides = "core:iron_sword"` am Eintrag — der landet **nur** unter der überschriebenen ID. Drei Phasen (Manifeste → Einlesen → Auflösen), dadurch keine Vorwärtsreferenzen; Fehlereinheit ist der Eintrag. Kein Nachladen zur Laufzeit.
- [04 — Fehlerstrategie für kaputte Mods](issues/04-fehlerstrategie.md) ([#5](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/5)) — **Syntax ist ein Datei-Fehler, alles Semantische ein Eintrags-Fehler**; damit 03s Versprechen hält, wird zweistufig deserialisiert (`Vec<Spanned<toml::Value>>`, dann Eintrag für Eintrag), sonst reißt ein Tippfehler die ganze `items.toml` mit. Genau drei harte Fehler: `depends` fehlend, `depends` verletzt, `core` nicht gefunden — stderr und `exit(1)`. `core` ist sonst kein Sonderfall; abgesichert wird es durch einen `#[test]`, der `mods/core/` mit null Warnungen lädt. Warnungen sammeln sich in `Vec<String>` und landen in `logs/load-<zeitstempel>.log` (letzte zehn), plus eine Zeile auf dem Startbildschirm — kein Report-Screen. Meldung `meinmod/items.toml:42: core:iron_sword — <message>`, Mod-ID vorne, keine Spalte. Rust: handgeschriebenes `enum LoadError` mit `impl Display` statt `thiserror`/`anyhow`, `load_mods() -> Result<(Registries, Vec<String>), LoadError>`, Kette endet vor `main` mit `eprintln!` und `exit(1)`. **Panic-Grenze ist die Datenherkunft**: TOML ist `Result`, eigener Code darf panicken.
- [05 — Research: Script-Engine](issues/05-research-script-engine.md) ([#6](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/6)) — Empfehlung Rhai wegen Fehlermeldungen mit Zeile und Spalte, billiger Typ-Brücke, AST-Hot-Reload und Build ohne `cc`. Gemessen: mlua liest unter `StdLib::ALL_SAFE` per `io.open` echte Dateien — „safe" heißt dort speichersicher, nicht fähigkeitsbeschränkt. Lua gewinnt bei der Modder-Vertrautheit; WASM scheidet für 1.0 aus. **Kernfolge: reden Scripts nur über das Effekt-Vokabular, ist die Engine-Wahl umkehrbar.** Entscheidung offen in Ticket 13.
- [06 — Research: ratatui-App-Architektur](issues/06-research-ratatui-architektur.md) ([#7](https://gitlab.hive-brain.de/MDietermann/rustkeep/issues/7)) — Screen-Stack über zentralem `Game`-State statt Elm oder Component-Trait; `game/` bleibt ratatui-frei; Popups per `Clear` auf Sub-`Rect` statt Widget-Stack; blockierendes `event::read()` ohne Tick; Dungeon-Grid ist Eigenbau, es gibt kein Widget dafür; ratatui 0.30.2 mit MSRV 1.88; `KeyEventKind::Press`-Filter unter Windows Pflicht. Voller Bericht in [research/06](research/06-ratatui-architektur.md).