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.