# Phase 14 — Urteil des Obersten Richters Gegenstand: `docs/superpowers/specs/2026-08-27-sync-ausbau-design.md` (Design, noch nicht implementiert). Grundlage: `phase_10_11_verification.md`, `phase_8_audit.md`, beide Debatten-Zusammenfassungen, die fünf Phase-7-Schlussurteile, plus eigener Code-Blick (read-only, grep/sed/read auf `lib/`, `test/`, `/home/dustin/scripts/melo_cloud.py`). Maßstab: Auftraggeber ist ein Hobby-Entwickler, drei Nutzer (Dustin, Baka, Tinker), Projekt-Prinzip „minimale, robuste Lösungen". Keine Enterprise-Sync-Architektur. „Feature streichen oder verschieben" ist eine zulässige und hier mehrfach die richtige Antwort. --- ## 0. Verifikation zuerst — was die Faktenprüfung entscheidet Die konsolidierte Verifikation schlägt jede widersprechende Panel-Behauptung. Ich habe stichprobenartig gegengeprüft und **keine ihrer Korrekturen widerlegen können**. Es gilt daher: **0.1 Kein P0 im Geltungsbereich dieser Spec. [VERIFIZIERT]** Das einzige verbliebene Panel-P0 (F1, `parseFavoriten` + Superset-Invariante) steht auf totem Code: `MeloCloudService.favoriten()` (`lib/services/melo_cloud_service.dart:258-265`) hat **null Aufrufer** in `lib/` und `test/` — eigener Gegen-Grep bestätigt. Die Live-DB hat **0 Favoriten**. Der Befund kann heute nichts verlieren; er wird erst durch die Spec scharf. Nach der Regel des Context Briefs (P0 = Bestandsfehler mit Datenverlust *oder* durch die Spec neu geschaffener Datenverlust) ist das **P1**. Die Panel-Aussage „ein P0 hält" ist nicht haltbar. **0.2 Die Lösch-Kette ist Glied für Glied bestätigt. [VERIFIZIERT]** `markMissing` (`lib/library/database.dart:237-245`) → `planeSync serverLoeschen` (`lib/services/sync_service.dart:68-76`) → Lösch-Bremse greift bei 325 Titeln erst ab 109 (`:112-113`) → `_meldeLoeschungen` (`:210`, `:239-251`) → `handle_delete` (`melo_cloud.py:374-394`) → `_entferne_datei_wenn_verwaist` (`:356-372`) löscht `registry/` **und** über `loese_navidrome` (`:190-202`) `navidrome/music/`. Für die **Datei** existiert kein Grabstein. Der Reparaturweg ist verifiziert kaputt: der überlebende `registry.sha256` erzwingt den Dedup-Zweig (`:265-267`), `shutil.move` steht nur im Neu-Zweig (`:289`), `os.remove(tmp)` verwirft die Bytes (`:312`), `/download` antwortet dauerhaft 404 (`:1400-1408`). Alle drei denkbaren Bruchstellen sind ausgeschlossen (Fremdnutzer-Schutz greift nie — Live-DB `Baka|325|0`; `users/` ist leer; beide Pfade auf `/dev/vda3`, also echte Hardlinks). **Bestandsfehler, nicht von dieser Spec verursacht** — Kalibrierung: `SUM(deleted) = 0`, die Kette wurde noch nie ausgelöst. **0.3 Die Haupt-Empfehlung des Panels schafft selbst einen neuen Löschpfad. [VERIFIZIERT] [PLAN-RISIKO, neu geschaffen]** Das ist der wichtigste Einzelbefund dieses Reviews und er richtet sich gegen das Panel, nicht gegen die Spec. Das Panel konvergiert in Runde 2 auf den Basis-Snapshot mit `neu = (lokal ∪ server) − (Basis \ lokal) − (Basis \ server)`. Ich habe nachgerechnet: Liefert `GET /favorites` fälschlich `∅` — und genau das tut `parseFavoriten` heute bei jedem `{"status":"error"}` im 200er-Körper (`melo_cloud_service.dart:117-124`; sechs Handler desselben Servers antworten so: `melo_cloud.py:418, 491, 545, 766, 1021, 1118`; der Router verdrahtet für `GET /favorites` hart 200: `:1292-1293`) —, dann wird `Basis \ server = Basis` und die Formel kollabiert zu `neu = lokal − Basis`: **der gesamte bestätigte Bestand wird gelöscht, lokal und über den Delta-Push auch auf dem Server und damit auf allen Geräten.** Der Basis-Snapshot ist damit die **einzige Maßnahme im gesamten Bericht, die einen neuen Datenverlustpfad einführt.** DA und Risk haben das je einmal am Rand notiert; beide Phase-6-Zusammenfassungen führen es nicht als Finding. Es ist tragend für mein Urteil in Abschnitt 1. **0.4 Was die Verifikation dem Panel abspricht. [VERIFIZIERT]** - „Auswahl-Modus in ‚Meine Musik' existiert so nicht" (DA) — **widerlegt**: `my_music_screen.dart:120` → `SortableSongList` (`lib/shared/sortable_song_list.dart:46-54, 194-196`) mit vollem Auswahl-Modus. Bleibt ein reines Scoping-Thema (die Aktion erschiene in fünf Ansichten). - „Pro-Song-Lade-Loop bringt weder Fortschritt noch Doppel-Lauf-Schutz mit" (Feasibility) — **widerlegt**: `DownloadService.lade(List)` (`lib/services/download_service.dart:72-111`) ist öffentlich und bringt Doppel-Lauf-Schutz (`:73`), Verbindungsprüfung (`:77-84`) und Fortschritt (`:85-104`) mit. **Die Spec hat hier recht, das Panel nicht.** - Worker-Pool-Erschöpfung (Risk K1) — vom Panel selbst korrekt zurückgezogen (`ThreadingMixIn` ohne Pool, `melo_cloud.py:1449-1453`). - „alle 33 `handle_*`" (Feasibility) — es sind 32. Ohne Folge. **0.5 Was die Verifikation dem Panel *hinzufügt*. [VERIFIZIERT]** `melo_cloud.py:505` löscht `user_playlist_songs` für **jede** `playlist_id` ohne `user`-Bedingung. Das ist nicht nur ein falsches Erfolgssignal, sondern echte Fremddaten-Löschung. Heute folgenlos (0 Playlisten), aber die Spec befördert genau diesen Endpunkt zum Regelpfad. **0.6 Severity-Dämpfung angewandt.** Ein reines [PLAN-RISIKO], das eine sorgfältige Implementierung vermeiden kann, ist höchstens P1. Ein [BESTANDSFEHLER], den diese Spec nicht verursacht, ist Backlog-Punkt, kein Blocker. Nach diesem Maßstab bleibt von der Panel-Liste: **0 × P0, 10 × P1, 8 × P2, 4 × P3.** Die Dringlichkeitsrhetorik des Panels ist durch den Live-Stand entkräftet: ein Nutzer mit Songs, 0 gelöschte, 0 Favoriten, 0 Playlisten. --- ## 1. Entscheidung über die verbliebenen Meinungsverschiedenheiten ### 1.1 Merge-Mechanismus: Union vs. Basis-Snapshot vs. Drei-Wege-Merge **Beide Lager haben in ihrer Diagnose recht und in ihrer Therapie unrecht. Ich entscheide für eine dritte Option, die keines der beiden vorgeschlagen hat.** Was feststeht: - **Union kann „entfernt" nicht ausdrücken. [VERIFIZIERT]** Der Zyklus des Sync-Spezialisten hält, und er ist schärfer als die Spec zugibt: A ent-favorisiert **online**, Server ∅; B (das seither nicht synchronisiert hat, also praktisch immer) synchronisiert, `Union({X}, ∅) = {X}`, POSTet `{X}`; A holt X beim nächsten Lauf zurück. Die Spec-Zusage in Z. 108-112 („Ent-Favorisierungen propagieren über den Sofort-Push (online)") ist damit **falsch**, nicht nur unvollständig. Sie muss weg. - **Der Basis-Snapshot heilt das, führt aber 0.3 ein.** Dazu braucht er kontogebundenen Zustand, der beim Abmelden mitgelöscht werden muss — `BakaAuth.abmelden` (`lib/services/baka_auth.dart:114-120`) räumt heute nichts ab, und ein Token-Refresh gibt es auch nicht. Das ist neue Persistenz mit einer neuen Aufräumpflicht für drei Nutzer mit null Favoriten. **Was das Panel übersehen hat (mein eigener Befund, siehe auch Abschnitt 3):** Die eigentliche Gefahrenquelle ist gar nicht die Union — es ist der **Full-Replace-`POST /favorites`**. Solange die Sync-Phase den kompletten Server-Stand ersetzt, hängt die Datensicherheit an einer *Regel* (GET-vor-POST), und jede Lücke in dieser Regel (`parseFavoriten`!) wird sofort zum Datenverlust. Streicht man den Full-Replace, verschwindet die ganze Fehlerklasse **strukturell**. **URTEIL — additiver Delta-Abgleich, kein Voll-Ersatz, kein Basis-Snapshot:** | | schreibt Server | schreibt lokal | neuer Zustand | neuer Löschpfad | |---|---|---|---|---| | heute | `POST /favorites` (Voll-Ersatz) | — | — | **ja, der Bug** | | Spec (Union + Voll-Ersatz) | `POST /favorites` | ja | — | ja, sobald GET `∅` täuscht | | Panel (Basis-Snapshot) | Delta-Push | ja | Basis je Datentyp, kontogebunden | **ja (0.3)** | | **Urteil (additiv)** | `POST /favorites/toggle` `set:true`, nur für `lokal \ server` | nur additiv | **keiner** | **keiner** | Der Endpunkt existiert und ist deterministisch — eigene Prüfung: `melo_cloud.py:1298-1302` (Route, `set` aus dem JSON-Körper) → `handle_favorites_toggle` (`:644-688`), `set_state is True/False` erzwingt den Zielzustand. Im Client fehlt nur die Methode (kein Treffer für `toggle` in `melo_cloud_service.dart`). Warum das die richtige Antwort für dieses Projekt ist: 1. **Ziel 1 wird vollständig erreicht** („kein Gerät überschreibt mehr die Server-Favoriten") — und zwar durch Konstruktion statt durch eine Regel. Es gibt danach keinen Codepfad mehr, der den Server-Stand ersetzen kann. 2. **Kein neuer Zustand.** Kein Basis-Set, keine Aufräumpflicht beim Abmelden, keine Mengenalgebra. Weniger Teile als die Spec heute hat, nicht mehr. 3. **Der Fehlerpfad ist selbstheilend.** Täuscht der GET eine leere Menge vor, pusht das Gerät seine lokalen Favoriten additiv hoch — harmlos und idempotent. Beim Basis-Snapshot wäre derselbe Fehler eine geräteübergreifende Massenlöschung. 4. **Der spätere Ausbau ist nicht verbaut.** Wenn sich in der Praxis zeigt, dass Ent-Favorisieren wirklich stört, ist der Basis-Snapshot ein *Aufsatz* auf denselben Schreibweg (`set:false` statt `set:true`) — kein Umbau. 5. **Preis, ehrlich benannt:** Ent-Favorisieren bleibt geräteübergreifend unzuverlässig. Das ist dieselbe Einschränkung, die die Spec ohnehin schon akzeptiert — nur muss sie **richtig** beschrieben werden (nicht „offline", sondern „auch online, sobald ein zweites Gerät den alten Stand hält"). **Damit ist die Auftraggeber-Entscheidung „Union-Merge" nicht überstimmt, sondern präzisiert:** die Vereinigungs-*Semantik* bleibt genau wie entschieden; nur der *Schreibweg* wechselt vom Voll-Ersatz auf additive Pushes. Die Panel-Empfehlung Basis-Snapshot wird **abgelehnt** — mit der Begründung aus 0.3. Sie kommt als benannter Backlog-Punkt ins Dokument, nicht als Bestandteil. ### 1.2 SSE streichen — ja **Das Panel steht 3:2 für Behalten (mit acht Härtungs-Bausteinen). Ich entscheide gegen die Mehrheit, aber mit einem Argument, das niemand widerlegt hat.** Nicht „SSE ist schlecht", sondern: **die eigene Vorbedingung von SSE ist in dieser Spec nicht erfüllt.** - Der beworbene Haupt-Nutzen („neuer Song erscheint in Sekunden auf dem anderen Gerät") braucht das `song_upload`-Event. Das feuert `melo_cloud.py` nicht (`_emit_event` nur bei `:393 song_delete`, `:410 song_update`, `:687 song_favorite` — **[VERIFIZIERT]**), und die Spec lagert es ausdrücklich aus (Z. 211-215, 276-278). **Am Auslieferungstag kann SSE genau das nicht, wofür es gebaut wird.** - Die drei vorhandenen Kanäle haben heute keinen Adressaten: Live-DB `Baka|325`, 0 gelöschte, 0 Favoriten, 0 Playlisten. **[VERIFIZIERT]** - Dem stehen acht eigene, je einzeln belegte Fehlerklassen gegenüber (eigener `http.Client` wegen `melo_cloud_service.dart:82-83`; Heartbeat-Watchdog gegen den halboffenen Socket nach WLAN↔LTE; 401-Dauerabbruch, weil `baka_auth.dart` kein Refresh hat; Selbst-Echo-Unterdrückung, weil `melo_realtime.py:56-70` an alle Queues des Nutzers ohne Absenderkennung broadcastet; Timer-Abbau bei `paused`; Debounce; Backoff; Nachhol-Anstoß). Kein anderes Ziel der Spec trägt annähernd so viel Fehleroberfläche. - Dieselbe Spec lehnt Delta-Protokoll, Hintergrund-Sync und Chunked-Upload mit YAGNI ab. SSE ist die einzige Ausnahme von der eigenen Regel. - Der ehrliche Preis ist klein und vom Panel selbst festgestellt: Es gibt **keinen** billigen Poll-Ersatz (`automatisch()` läuft nur aus `initState` und `resumed`, `main.dart:175`, `:203`, kein Timer — **[VERIFIZIERT]**). Der Tausch lautet: Änderungen anderer Geräte erscheinen beim nächsten App-Start oder Zurückkehren — **genau wie heute**. Für drei private Nutzer ist das der richtige Preis für acht entfallende Fehlerklassen. **URTEIL: SSE (Ziel 6, Feature 6, `echtzeit_sync.dart`) wird aus dieser Spec herausgenommen und als eigene, spätere Stufe geführt — Vorbedingung: das `song_upload`-Event steht.** Das ist keine Verwerfung, sondern Reihenfolge. Fällt mit weg: der Parameter `erzwinge` (wäre ohnehin wirkungslos — die Drossel sitzt in `automatisch()` `:166-170`, nicht in `synchronisiere()` `:174-180`, **[VERIFIZIERT]**). ### 1.3 Playlist-Sync drin lassen oder herausschneiden — reduzieren Risk formuliert es korrekt: entweder vier Semantik-Entscheidungen treffen oder herausschneiden; das Weiterlaufen im jetzigen Zustand nicht. Der Befundstand: - Namenszuordnung ist keine Funktion (gleichnamige Playlisten per Knopfdruck erzeugbar, `playlist_service.dart:75-109`). - Kein Rename-Endpunkt — **[VERIFIZIERT]**, vollständiger Routing-Block `melo_cloud.py:1251-1284` gelesen, kein `PUT`/`PATCH` auf die Playlist selbst. Die Spec behauptet in Z. 129-130 das Gegenteil. - `POST /playlists` nicht idempotent, kein Unique (`:488-500`). - `handle_playlist_delete` löscht `user_playlist_songs` ohne Owner-Prüfung (`:505`) — echter Server-Bug. - „Längerer Stand gewinnt" ist bei Gleichstand undefiniert; `updatedAtMs` wird app-seitig nur in `createPlaylist`/`deletePlaylist` gepflegt. - N+1-Requests: `handle_playlist_list` liefert nur `song_count`. - Live-Stand: **0 Playlisten auf dem Server.** Das ist kein Nachschärf-Fall, sondern ein Entwurfsfall. Es ist aber auch kein Grund, das Feature ganz zu streichen — der Nutzen (Playlisten überleben ein zurückgesetztes Handy) ist real und billig zu haben, **wenn man auf den beidseitigen Merge verzichtet.** **URTEIL: Playlist-Sync Stufe 1 = einseitige Sicherung.** Die App pusht lokale Playlisten-Änderungen online an den Server und merkt sich die vom Server vergebene cloudId. **Kein Rück-Merge.** Server-Playlisten werden nur dann lokal angelegt, wenn die lokale Playlisten-Tabelle **leer** ist (Neuinstallation / Wiederherstellung). Damit entfallen ersatzlos: Namenszuordnung, Reihenfolge-Schiedsrichter, Gleichstands-Regel, Tombstone-Semantik und die Abhängigkeit vom fehlenden Rename-Endpunkt — die Identität stammt **immer** aus dem eigenen `POST /playlists` und ist damit per Konstruktion stabil (`user_playlists.id` ist `INTEGER PRIMARY KEY AUTOINCREMENT`). Vorbedingung bleibt: `melo_cloud.py:505` (Owner-Prüfung) muss serverseitig geflickt sein, bevor die App diesen Endpunkt regelmäßig benutzt. Der beidseitige Playlist-Merge ist eine eigene, spätere Spec. (Wer noch weiter reduzieren will: ganz herausschneiden ist ebenfalls vertretbar und kostet heute nichts.) --- ## 2. Prüfung der Konsens-Punkte (Konsens kann geschlossen falsch sein) | Konsens-Punkt | Urteil | |---|---| | „Union muss durch einen Basis-Snapshot ersetzt werden" (Runde 2, alle fünf) | **FALSCH in der Therapie.** Diagnose hält, Empfehlung nicht — siehe 0.3 und 1.1. Der Konsens entstand, weil die Debatte nur zwei Kandidaten kannte (Outbox vs. Basis-Snapshot) und beide *in Kombination mit dem Full-Replace-POST* dachte. | | „DAs Outbox löst nur ein Drittel" (Runde 1, mit Gegenbeispiel akzeptiert) | **Zu früh geschlossen.** Das Gegenbeispiel lautet wörtlich „Offline-Gerät B resurrects **via Union**" — es setzt voraus, dass die Union bleibt. Es widerlegt die Outbox nur in dieser Paarung. Für mein Urteil ist das nicht tragend (ich brauche keine Outbox), aber es zeigt den Denkfehler des Konsenses. | | „Die GET-vor-POST-Regel ist die beste Stelle der Spec" (Phase 3, alle fünf) | **FALSCH und vom Panel selbst korrigiert.** Sie ist eine Regel über ein nicht beobachtbares Prädikat. Richtig ist Flutters Umkehrung in Runde 2. Meine Konsequenz geht weiter: eine Regel, deren Verletzung Datenverlust bedeutet, gehört durch eine Konstruktion ersetzt, die den Verlust unmöglich macht (1.1). | | „Die Testliste trägt" (7× erwähnt, stets lobend) | **FALSCH.** Der Auditor hat recht: sie nennt ausschließlich neue Tests. Zwei Bestandstests stehen der Spec aktiv im Weg (`melo_cloud_service_test.dart:98-101`, `sync_service_test.dart:166-202`), und der Sofort-Push — der einzige Online-Löschkanal der Spec — hat keinen einzigen Eintrag. | | „Der Löschpfad ist Bestand, P1 für die Spec, P0 im Backlog" | **RICHTIG.** Die einzige Panel-Einstufung, die den Maßstab exakt trifft. Bestätigt. | | „Die Architektur wird von keinem Befund angegriffen" | **RICHTIG.** `sync_merge.dart` als reine Funktionsdatei ohne I/O ist der richtige Ort — auch für den additiven Abgleich. Der Dateischnitt bleibt (abzüglich `echtzeit_sync.dart`). | | „Die Nicht-Ziele sind diszipliniert und begründet" | **RICHTIG — mit einer Ausnahme, die das Panel benannt hat:** SSE bricht die eigene YAGNI-Regel. Nach 1.2 ist die Ausnahme beseitigt und der Konsens wird nachträglich vollständig richtig. | | „`flutter_local_notifications` ist der einzige harte Blocker" | **RICHTIG**, mit Feasibilitys Korrektur: `multiDexEnabled` ist bei `minSdk 24` (`FlutterExtension.kt:26`) gegenstandslos und fällt aus der Forderung. | | „Die Phasenreihenfolge stimmt" | **RICHTIG**, gegen `sync_service.dart:207-215` verifiziert. Aber Z. 225 („Zeitstempel am Ende") widerspricht Z. 250 („Snapshot VOR dem Listen") — beides ist erfüllbar, die Spec sagt es nur nicht. | --- ## 3. Eigener Lücken-Scan — was auch der Auditor nicht gesehen hat **L1 [P1] [EINZELQUELLE: Richter] [PLAN-RISIKO] — Der Full-Replace-POST, nicht die Union, ist die Wurzel.** Fünf Reviewer und der Auditor haben zwei Runden über die *Mengenformel* debattiert und dabei den *Schreibweg* als gegeben behandelt. Kein Beitrag prüft, was passiert, wenn man `POST /favorites` einfach nicht mehr benutzt. Ergebnis: Ziel 1 wird dann strukturell erreicht, die Abhängigkeit der Datensicherheit von `parseFavoriten` entfällt, und es entsteht kein neuer Zustand. Risk hat den Delta-Push als Bestandteil (c) seines Basis-Snapshot-Pakets vorgeschlagen — dass Bestandteil (c) **allein** genügt, hat niemand geprüft. Ausführlich in 1.1. **L2 [P2] [EINZELQUELLE: Richter] [BESTANDSFEHLER] — Es gibt zwei parallele Favoriten-Systeme in derselben Oberfläche, und die Spec kennt nur eines.** `lib/shared/server_favorite_button.dart:42` setzt Favoriten über `navidrome.setFavorite(navidromeId)` — also im **Navidrome**, nicht in der lokalen `Favorites`-Tabelle und nicht in der Melo-Cloud. `lib/player/now_playing_screen.dart:275-279` zeigt für lokale Songs `FavoriteButton` (lokal) und für Server-Titel `ServerFavoriteButton` (Navidrome) — **dasselbe Herz-Symbol, zwei getrennte Systeme.** Gegenprobe: `ServerFavoriteButton` / `server_favorite_button` kommt in **keiner** der 25 State-Dateien und nicht in der Spec vor (`grep -rln` → kein Treffer). Der dritte Kanal, `PlaylistService.syncFavoritesFromServer()` (`lib/library/playlist_service.dart:52-71`), wurde von Risk (Phase 3, `:303-308`) und DA (Phase 3, `:297-299`) als toter Code erkannt (`db.songExists(navidromeId)` gegen lokale UUIDs) — fiel aber aus allen fünf Schlussurteilen heraus. Folge: Ein Nutzer, der im Now-Playing-Screen ein Herz antippt, kann je nach Titelquelle in zwei völlig verschiedenen Systemen landen; die Spec verspricht Favoriten-Sync, deckt aber nur eines ab. Das ist eine **Geltungsbereichs-Frage, die in die Spec gehört**, nicht in den Kopf des Umsetzenden. **L3 [P2] [EINZELQUELLE: Richter] [PLAN-RISIKO] — Die Pull-Richtung hat keine Regel für unauflösbare Server-Favoriten.** Die Spec sagt „fehlende Favoriten lokal setzen" (Z. 99) und schweigt dazu, was bei einer cloudId passiert, die sich lokal auf keinen Song abbilden lässt (Download fehlgeschlagen, Song noch nicht geladen). `Favorites.songId` verweist auf `Songs.id` (`database.dart:102-107`); ein Favorit ohne Song wäre über `watchFavorites()` (innerJoin, `:371-375`) unsichtbar, würde aber über `favoriteSongIds()` (`:534-536`) ewig mitgeschleppt und wieder hochgepusht. Nebenbefund am selben Ort: `favoriteSongIds()` liefert auch Favoriten **getombsteter** Songs, weil `allSongs()` (`:216`) Grabsteine einschließt — ein lokal gelöschter Titel wird also weiter als Favorit gemeldet, während derselbe Lauf ihn am Server löscht. Unter dem additiven Abgleich harmlos, unter jedem Voll-Ersatz nicht. **L4 [P2] [EINZELQUELLE: Richter] — Der additive Delta-Push feuert N SSE-Events.** `handle_favorites_toggle` emittiert bei **jedem** Aufruf `song_favorite` (`melo_cloud.py:687`). Beim ersten Lauf nach der Umstellung sind das so viele Events wie lokale Favoriten. Ohne SSE in der App (Urteil 1.2) folgenlos — aber es gehört als Vorbedingung notiert, falls SSE später kommt: das Selbst-Echo-Fenster muss dann existieren, **bevor** der Delta-Push zum Regelpfad wird. **L5 [P3] [EINZELQUELLE: Richter] — `handle_favorites_get` liefert `favorited_at`, der Client wirft es weg.** `melo_cloud.py:620` gibt je Favorit `favorited_at` zurück; `parseFavoriten` liest nur `['id']`. Lokal existiert das Gegenstück als `Favorites.createdAtMs` (`database.dart:103`). Kein Handlungsbedarf für diese Spec (ohne Tombstones auf beiden Seiten trägt ein Zeitstempel keine Lösch-Entscheidung), aber es ist die Zutat, die ein späterer echter Merge bräuchte — und sie ist bereits da. Ein Satz im Backlog spart der Zukunft eine Server-Änderung. --- ## 4. Epistemik- und Typ-Labels (Sammelübersicht) | # | Befund | Epistemik | Typ | Severity | |---|---|---|---|---| | 1 | `parseFavoriten` verwandelt Fehlerantworten in `[]`; `favoriten()` hat null Aufrufer | [VERIFIZIERT] | [PLAN-RISIKO] | P1 | | 2 | Union kann „entfernt" nicht ausdrücken; Spec-Zusage Z. 108-112 ist falsch | [VERIFIZIERT] | [PLAN-RISIKO] | P1 | | 3 | Basis-Snapshot schafft neuen Löschpfad (Panel-Empfehlung) | [VERIFIZIERT] | [PLAN-RISIKO, neu geschaffen] | P1 — **abgelehnt** | | 4 | Full-Replace-`POST /favorites` ist die eigentliche Wurzel | [EINZELQUELLE: Richter] | [PLAN-RISIKO] | P1 | | 5 | Irreversibler Löschpfad bis in die Navidrome-Bibliothek | [VERIFIZIERT] [KONSENS] | [BESTANDSFEHLER] | P1 Spec / **P0 Backlog** | | 6 | Playlist-Identität instabil (4 offene Entscheidungen) | [VERIFIZIERT] [KONSENS] | [PLAN-RISIKO] | P1 | | 7 | `melo_cloud.py:505` löscht fremde `user_playlist_songs` ohne Owner-Prüfung | [VERIFIZIERT] | [BESTANDSFEHLER] | P1 Server-Backlog, **Vorbedingung** | | 8 | Kein Playlist-Rename-Endpunkt (Spec behauptet das Gegenteil) | [VERIFIZIERT] | [PLAN-RISIKO] | P1 | | 9 | Fünf „bestehende Schutzmechanismen", die es nicht gibt | [VERIFIZIERT] | [BESTANDSFEHLER] + [PLAN-RISIKO] | P1 | | 10 | Gradle-Desugaring für `flutter_local_notifications` | [VERIFIZIERT] [KONSENS] | [PLAN-RISIKO] | P1 (harter Blocker) | | 11 | SSE-Hauptnutzen hängt am ausgelagerten `song_upload`-Event | [VERIFIZIERT] | [PLAN-RISIKO] | P1 | | 12 | Bestandstest `melo_cloud_service_test.dart:98-101` zementiert den Bug | [VERIFIZIERT] (Verifikation) | [BESTANDSFEHLER im Test] | P1 | | 13 | Bestandstest `sync_service_test.dart:166-202` + Catch-All-Mock `:196` | [VERIFIZIERT] (Auditor) | [BESTANDSFEHLER im Test] | P1 | | 14 | Sofort-Push ohne einen einzigen Testeintrag | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P1 | | 15 | Zwei parallele Favoriten-Systeme (`ServerFavoriteButton`) | [EINZELQUELLE: Richter] | [BESTANDSFEHLER] | P2 | | 16 | Pull-Richtung ohne Regel für unauflösbare cloudIds | [EINZELQUELLE: Richter] | [PLAN-RISIKO] | P2 | | 17 | Auswahl-Upload nicht abbrechbar | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 | | 18 | 30er-Rückfrage strukturell wirkungslos; kein Platz-Check | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 | | 19 | Doppelspeicherung Download × Abspiel-Cache | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 | | 20 | Zeitstempel × Auswahl-Upload × 24-h-Bericht offen | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 | | 21 | Bericht: `letzterLauf == null` und „erfolgreich" undefiniert | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 | | 22 | Offline-Modus von keinem neuen Netz-Verbraucher beachtet | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 | | 23 | Navidrome-Playlist-Import nicht idempotent („🌐"-Duplikate) | [VERIFIZIERT] | [BESTANDSFEHLER] | P2 | | 24 | `_LadeKnopf` ist privat und in einer anderen Datei | [VERIFIZIERT] | [PLAN-RISIKO] | P2 | | 25 | Schema-Divergenz `ist_korrupt` / `uq_user_song` | [VERIFIZIERT] | [BESTANDSFEHLER] | P2 Backlog | | 26 | `server_titel_screen._geladen`-Einmal-Snapshot | [VERIFIZIERT] | [BESTANDSFEHLER] | P2 | | 27 | Widerspruch Z. 225 ↔ Z. 250 (Zeitstempel) | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P3 | | 28 | „2-GB-Abspiel-Cache" ist ein Standardwert (0–8192 MB) | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P3 | | 29 | Kein Migrations-Downgrade-Pfad (Schema 10 → 11) | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P3 | | 30 | `favorited_at` / `createdAtMs` ungenutzt | [EINZELQUELLE: Richter] | [PLAN-RISIKO] | P3 | | — | „Auswahl-Modus existiert nicht" (DA) | [BESTRITTEN → widerlegt] | — | entfällt | | — | „Pro-Song-Loop ohne Fortschritt/Schutz" (Feasibility) | [BESTRITTEN → widerlegt] | — | entfällt | | — | Worker-Pool-Erschöpfung (Risk K1) | [BESTRITTEN → zurückgezogen] | — | entfällt | --- ## 5. Finaler Score und Verdikt # Score: **6 / 10** Über dem Panel-Mittel (5,2), weil die Verifikation zwei tragende Säulen der Panel-Kritik entfernt hat: es gibt **kein P0 im Geltungsbereich**, und die zentrale Ersatz-Empfehlung des Panels ist selbst gefährlicher als das, was sie ersetzen soll (0.3). Die beiden „ÜBERARBEITEN"-Stimmen begründen sich fast vollständig aus einer Therapie, die ich ablehne. Nicht höher als 6, weil das Dokument drei belegte Substanzmängel hat: eine **falsche Garantie** (Z. 108-112 — Ent-Favorisierungen propagieren nicht), **fünf zu optimistische Bestandsbehauptungen** (Rename-Endpunkt, `_LadeKnopf`, „verzögert", `erzwinge`, „vollständig" in Z. 17) und einen **Umfang, den es nicht rechtfertigen kann** (SSE ohne seine eigene Vorbedingung, Playlist-Sync mit vier offenen Semantik-Entscheidungen, eine Dependency, die den einzigen harten Build-Blocker der ganzen Planung erzwingt). # Verdikt: **SPEC NACHSCHÄRFEN DANN FREIGEBEN** Begründung gegen „ÜBERARBEITEN": Jede von mir geforderte Änderung ist entweder (a) die Korrektur eines faktisch falschen Satzes, (b) eine **Reduktion** des Umfangs — Abschnitte streichen oder verschieben ist keine Neuschrift, oder (c) **ein** Mechanismus-Tausch, der das Dokument *kleiner* macht (additiver Delta-Push statt Voll-Ersatz = weniger bewegliche Teile als heute). Rahmen, Zerlegung, Nicht-Ziele, Architektur (`sync_merge.dart` als reine Funktionsdatei), Phasenreihenfolge und der Testansatz bleiben unangetastet und wurden von keinem Reviewer angegriffen. Es gibt nichts wegzuwerfen. Begründung gegen „FREIGEBEN": Die falsche Lösch-Garantie und die fünf Bestandsbehauptungen würden den Umsetzenden nachweislich in die Irre führen; zwei grüne Bestandstests stehen der Umsetzung aktiv im Weg; und eine Frage kann kein Reviewer entscheiden (siehe A6). **Bedingung:** Die nachgeschärfte Fassung wird **einmal kurz gegengelesen**, bevor der Implementierungsplan entsteht — die Punkte A1, A5 und A7 ändern, *was* gebaut wird, nicht nur *wie* es beschrieben ist. --- ## 6. AKTIONSLISTE — konkrete Änderungen an der Spec Priorisiert. Jede Zeile ist so formuliert, dass sie direkt ins Dokument kann. --- ### A1 · P1 · [EINZELQUELLE: Richter] [PLAN-RISIKO] · §Feature 1, Punkt „Vereinigung beim Sync" (Z. 95-101) + §Risiken (Z. 252-254) **Full-Replace-POST ersatzlos streichen. Der Abgleich schreibt nur noch additiv.** > Die Sync-Phase benutzt **nie** `POST /favorites` (Voll-Ersatz). Sie schreibt > ausschließlich additiv: > 1. `GET /favorites` → `server` (Menge von cloudIds). > 2. Für jede cloudId in `lokal \ server`: `POST /favorites/toggle` mit > `{"song_id": …, "set": true}` (existiert und ist deterministisch: > `melo_cloud.py:1298-1302` → `handle_favorites_toggle:644-688`; die > Client-Methode ist neu). > 3. Für jede cloudId in `server \ lokal`: lokal Favorit setzen, sofern der Song > lokal auflösbar ist. > 4. **In keiner Richtung wird etwas entfernt.** > > `MeloCloudService.setzeFavoriten` (`melo_cloud_service.dart:268-280`) wird nicht > mehr benutzt und entfällt. > > **Wirkung:** Der Datenverlust-Bug aus Ziel 1 ist danach **strukturell** > unmöglich, nicht nur durch eine Regel verhindert — es existiert kein Codepfad > mehr, der den Server-Stand ersetzen kann. Deckel: höchstens 200 Pushes je Lauf, > der Rest im nächsten Lauf (die Pushes sind idempotent, ein Teilausfall heilt > sich beim nächsten Lauf selbst). Streichen in §Risiken: „`POST /favorites` bleibt technisch ein Voll-Ersatz — die Sicherheit liegt allein in der GET-vor-POST-Regel." Ersetzen durch: „Die Sicherheit liegt nicht mehr in einer Regel, sondern darin, dass kein Voll-Ersatz-Aufruf mehr existiert." --- ### A2 · P1 · [VERIFIZIERT] [PLAN-RISIKO] · §Feature 1 „Sicherheitsregel" (Z. 102-105) + §Tests **`parseFavoriten` härten — und den grünen Bestandstest mitändern.** > Ein GET gilt nur als erfolgreich, wenn HTTP 200 **und** kein `error`-Schlüssel > **und** `favorites` als Liste vorhanden ist. Ein fehlender `favorites`-Schlüssel > ist ein **Fehler, keine leere Menge**; `parseFavoriten` wirft dann > `CloudException`, genau wie `parseListe` (`melo_cloud_service.dart:99-100`) und > `parseUpload` (`:111-112`) es bereits tun. Hintergrund: der Router verdrahtet > für `GET /favorites` hart 200 (`melo_cloud.py:1292-1293`), und sechs Handler > desselben Servers geben Fehler im 200er-Körper zurück (`:418, 491, 545, 766, > 1021, 1118`). > > **Bestandsänderung, Teil des Arbeitspakets:** > `test/services/melo_cloud_service_test.dart:98-101` prüft heute ausdrücklich > das Gegenteil (`expect(parseFavoriten(jsonEncode({'status':'ok'})), isEmpty)`) > und wird mit umgeschrieben. Das ist kein Regressionsfehler. > > Zwei Pflichttests: „200 mit Fehlerkörper → Phase übersprungen, kein Push" und > „200 mit `favorites: []` → Phase läuft normal durch" (der zweite verhindert, > dass der erste durch ein zu scharfes „wirf bei leer" trivial erfüllt wird). --- ### A3 · P1 · [VERIFIZIERT] [PLAN-RISIKO] · §Feature 1, letzter Punkt (Z. 108-112) **Die falsche Lösch-Garantie streichen und durch die richtige Einschränkung ersetzen.** Zu streichen: „Ent-Favorisierungen propagieren über den Sofort-Push (online) … Offline entfernte Herzen kommen beim nächsten Sync zurück". Das ist falsch — es betrifft auch **online** entfernte Herzen. > **Bekannte Einschränkung (bewusst, dokumentiert):** Der Abgleich ist rein > additiv. Ein Ent-Favorisieren wirkt sofort lokal und — online — auch auf dem > Server, ist aber **nicht geräteübergreifend garantiert**: Hält ein zweites > Gerät den Favoriten noch, bringt dessen nächster Sync ihn auf allen Geräten > zurück. Das gilt für offline **und für online** entfernte Herzen. Bewusster > Tausch, wie in v2: kein Datenverlust ist wichtiger als verlässliche > Lösch-Propagation. > Falls das in der Praxis stört: Folgeschritt „Basis-Snapshot" (Backlog). Er > lässt sich später **ohne Umbau** ergänzen, weil der Schreibweg dann schon der > Delta-Push ist — aus `set:true` wird zusätzlich `set:false`. --- ### A4 · P1 · [VERIFIZIERT] [PLAN-RISIKO] · §Risiken, neuer Unterabschnitt „Verworfene Alternativen" **Den Basis-Snapshot ausdrücklich ablehnen und begründen (Gegen-Empfehlung zum Panel).** > **Verworfen: Drei-Wege-Merge gegen einen persistierten Basis-Snapshot** > (`neu = (lokal ∪ server) − (Basis \ lokal) − (Basis \ server)`). Er löst zwar > die Lösch-Propagation, führt aber den **einzigen neuen Datenverlustpfad** der > ganzen Planung ein: Liefert `GET /favorites` fälschlich eine leere Liste, wird > `Basis \ server = Basis`, die Formel kollabiert zu `lokal − Basis` und löscht > den gesamten bestätigten Bestand — lokal **und** über den Delta-Push auf dem > Server und damit auf allen Geräten. Zusätzlich braucht er kontogebundenen > Zustand mit Aufräumpflicht beim Abmelden; `BakaAuth.abmelden` > (`baka_auth.dart:114-120`) räumt heute nichts ab und es gibt kein > Token-Refresh. Für drei Nutzer, 0 Favoriten und 0 Playlisten auf dem Server ist > der additive Abgleich der bessere Tausch. Der Basis-Snapshot bleibt als > Folgeschritt im Backlog. --- ### A5 · P1 · [VERIFIZIERT] [KONSENS] [PLAN-RISIKO] · §Ziel 4, §Feature 2 (Z. 114-140), §Nicht-Ziele, §Tests, §Offene Abhängigkeiten **Playlist-Sync auf einseitige Sicherung reduzieren. Der beidseitige Merge wird eigene Spec.** > **Playlist-Sync Stufe 1 = einseitige Sicherung.** Die App meldet lokale > Playlisten-Änderungen (anlegen / Song hinzufügen / Song entfernen / > Reihenfolge) online fire-and-forget an den Server und persistiert die vom > Server vergebene cloudId (Drift-Migration `Playlists.cloudId TEXT NULL` bleibt). > Es findet **kein Rück-Merge** statt: Server-Playlisten werden nur dann lokal > angelegt, wenn die lokale Playlisten-Tabelle **leer** ist (Neuinstallation / > Wiederherstellung). > > Damit entfallen ersatzlos: Namenszuordnung, Reihenfolge-Schiedsrichter > („längerer Stand gewinnt"), Gleichstands-Regel, Tombstone-Semantik und die > Abhängigkeit vom fehlenden Rename-Endpunkt. Die Identität stammt **immer** aus > dem eigenen `POST /playlists` und ist stabil (`user_playlists.id` ist > `INTEGER PRIMARY KEY AUTOINCREMENT`). > > **Nicht abgedeckt (dokumentiert):** Umbenennungen propagieren nicht. > Playlisten-Änderungen auf einem zweiten Gerät erscheinen auf dem ersten nicht. > Der beidseitige Playlist-Merge ist eine eigene, spätere Spec. > > **Vorbedingung (Server-Auftrag, blockierend):** `melo_cloud.py:505` löscht > `user_playlist_songs` für jede `playlist_id` **ohne `user`-Bedingung** — echte > Fremddaten-Löschung. Muss geflickt sein, bevor die App diesen Endpunkt > regelmäßig benutzt. *Ebenfalls vertretbar und heute kostenlos (0 Playlisten auf dem Server): Feature 2 ganz herausschneiden. Entscheidung liegt bei Dustin; das Weiterlaufen im jetzigen Zustand (vier offene Semantik-Entscheidungen) ist es nicht.* --- ### A6 · P1 für die Spec / **P0 im Backlog** · [VERIFIZIERT] [KONSENS] [BESTANDSFEHLER] · §Kontext (Z. 15-17) + §Risiken **Den irreversiblen Löschpfad benennen — und die eine Frage stellen, die kein Reviewer entscheiden kann.** §Kontext, Korrektur des Wortes „vollständig": > „… der Weg Handy→Server→Navidrome-Bibliothek existiert also bereits — **und er > läuft auch rückwärts**: verschwindet die lokale Datei, meldet der Sync die > Löschung, und der Server entfernt die Audiodatei aus Registry **und** > Navidrome-Bibliothek." §Risiken, neuer Absatz: > **Irreversibler Löschpfad (Bestand, nicht von dieser Spec verursacht — > Backlog-P0).** `markMissing` (`database.dart:237-245`) tombstoned jeden Titel, > dessen Datei der Scan nicht findet — auch unbeabsichtigt (SD-Karte nicht > eingehängt, Berechtigung entzogen, Dateimanager). `planeSync` > (`sync_service.dart:68-76`) macht daraus eine Server-Löschung; die Lösch-Bremse > (`:112-113`) greift bei 325 Titeln erst ab 109 — **1 bis 108 Löschungen laufen > ungebremst**. Der Server löscht `registry/` und `navidrome/music/` > (`melo_cloud.py:356-372`, `:190-202`); für die Datei gibt es keinen Grabstein. > Erneutes Hochladen repariert es **nicht**: der überlebende `registry.sha256` > erzwingt den Dedup-Zweig (`:265-267`), `shutil.move` steht nur im Neu-Zweig > (`:289`), `os.remove(tmp)` verwirft die Bytes (`:312`), `/download` antwortet > dauerhaft 404 (`:1400-1408`). Bisher nie ausgelöst (Live-DB: `SUM(deleted)=0`). > > **Rückfrage an Dustin — kein Reviewer kann das entscheiden: Ist das gewollt?** > Bei „ja" ist es eine dokumentierte Eigenschaft. Bei „nein" gehört ein > Server-Auftrag (Dedup-Zweig stellt die Datei wieder her, wenn > `registry_pfad(sid)` leer ist) **vor** Feature 3 — denn Feature 3 gibt mehr > Titeln eine cloudId und vergrößert damit die Angriffsfläche. --- ### A7 · P1 · [VERIFIZIERT] [PLAN-RISIKO] · §Ziel 6 + §Feature 6 (Z. 193-215) + §Architektur-Tabelle + §Tests + §Offene Abhängigkeiten **SSE aus dieser Spec herausnehmen, als eigene spätere Stufe führen.** Ziel 6 und Feature 6 entfallen; `echtzeit_sync.dart` fällt aus der Architektur-Tabelle; die SSE-Tests fallen aus der Testliste. Neuer Eintrag unter §Nicht-Ziele: > **Kein Echtzeit-Sync (SSE) in dieser Stufe.** Der beworbene Haupt-Nutzen > („neuer Song erscheint in Sekunden auf dem anderen Gerät") hängt am > `song_upload`-Event, das `melo_cloud.py` heute nicht feuert (`_emit_event` nur > bei `:393`, `:410`, `:687`) und das diese Spec ausdrücklich auslagert — am > Auslieferungstag könnte SSE also genau das nicht, wofür es gebaut wird. Die > drei vorhandenen Kanäle (`song_delete`, `song_update`, `song_favorite`) haben > heute keinen Adressaten (Live-Stand: ein Nutzer mit Songs, 0 gelöschte, > 0 Favoriten, 0 Playlisten). Dem stehen acht eigene Fehlerklassen gegenüber: > eigener `http.Client` (der geteilte aus `melo_cloud_service.dart:82-83` würde > beim `close()` laufende Sync-Requests mitreißen), Heartbeat-Watchdog gegen den > halboffenen Socket nach WLAN↔LTE, dauerhafter 401-Ausstieg (`baka_auth.dart` > hat kein Refresh), Selbst-Echo-Unterdrückung (`melo_realtime.py:56-70` > broadcastet an alle Queues des Nutzers ohne Absenderkennung), Timer-Abbau bei > `paused`, Debounce, Backoff, Nachhol-Anstoß. > **Ehrlicher Preis:** Änderungen anderer Geräte erscheinen beim nächsten > App-Start oder Zurückkehren — genau wie heute. Es gibt keinen billigeren > Poll-Ersatz: `automatisch()` läuft nur aus `initState` und `resumed` > (`main.dart:175`, `:203`), ein Timer existiert nicht. > SSE wird eigene Stufe, sobald das `song_upload`-Event steht; die acht > Bausteine gehören dann in **jene** Spec, nicht in den Plan. Ebenfalls streichen: der Parameter `erzwinge` (Z. 201-202). Er wäre wirkungslos — die 15-Minuten-Drossel sitzt in `automatisch()` (`sync_service.dart:166-170`), nicht in `synchronisiere()` (`:174-180`). --- ### A8 · P1 · [VERIFIZIERT] [KONSENS] [PLAN-RISIKO] · §Ziel 5 + §Feature 5 (Z. 172-191) + §Architektur-Tabelle **Notification halbieren: Bericht sofort, persistente Notification als eigene Stufe mit Beweis-Build.** > **Stufe A (keine neue Dependency): Sync-Bericht „Was ist neu"** als In-App-Dialog. > **Stufe B (eigenes Arbeitspaket): persistente Fortschritts-Notification.** Sie > erzwingt `flutter_local_notifications` und damit > `isCoreLibraryDesugaringEnabled = true` plus > `coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")` in > `android/app/build.gradle.kts` — die Datei hat heute überhaupt keinen > `dependencies { }`-Block, der Gradle-Heap steht auf 2 GB > (`gradle.properties:1`, dokumentierte OOM-Quelle), AGP ist 9.0.1 > (`settings.gradle.kts:22`) und android-37 ist ein Nachbau von 36. > **Erstes Abnahmekriterium der Stufe B ist ein grüner > `flutter build apk --target-platform android-arm64` nach dem `pub add` — vor > jeder Zeile Notification-Logik.** > `multiDexEnabled` entfällt aus der Forderung: bei `minSdkVersion = 24` > (`FlutterExtension.kt:26`) ist es gegenstandslos. > Mitzuentscheiden: Channel mit `Importance.low`, `onlyAlertOnce: true`, und ein > `cancel()` beim Init gegen die nach App-Kill stehengebliebene > Fortschritts-Notification. --- ### A9 · P1 · [VERIFIZIERT] [BESTANDSFEHLER] + [PLAN-RISIKO] · §Fehlerfälle (Z. 227-240) **Den Abschnitt von Zusagen auf zu bauende Arbeit umstellen. Fünf „bestehende Schutzmechanismen" gibt es nicht.** > Die folgenden Schutzmechanismen **existieren nicht** und sind Teil dieser > Arbeit: > - **Phasen-Isolation:** `synchronisiere()` hat heute **ein einziges** try/catch > um alle Phasen (`sync_service.dart:188-228`). Jede neue Phase bekommt ihr > eigenes try/catch mit definiertem Teil-Erfolg. > - **„verzögert" gibt es nicht:** `if (_laeuft) return;` (`:175`) verwirft den > Anstoß wortlos. Wer einen Nachlauf will, braucht ein `_nachziehen`-Flag, das > im `finally` genau **einen** weiteren Lauf auslöst. > - **Der Upload überlebt keinen Timeout:** `_ladeHoch` fängt nur > `CloudException` (`:313`); der 120-s-Timeout > (`melo_cloud_service.dart:180`) wirft `TimeoutException` und reißt den ganzen > Lauf ab. Timeouts sind wie Einzelfehler zu behandeln. > - **Die Drossel sitzt in `automatisch()`** (`:166-170`), nicht in > `synchronisiere()` (`:174-180`) — siehe A7, `erzwinge` entfällt. > - **Der Löschweg zum Server** wird in den Fehlerfällen nirgends beschrieben — > siehe A6. --- ### A10 · P1 · [VERIFIZIERT] (Verifikation + Auditor) [BESTANDSFEHLER im Test] · §Tests (Z. 256-272) **Testliste um den Bestand erweitern. Sie nennt heute ausschließlich neue Tests.** > **Anzupassender Bestand:** > - `test/services/melo_cloud_service_test.dart:98-101` — zementiert das > `[]`-Verhalten, wird mit A2 umgeschrieben. > - `test/services/sync_service_test.dart:166-202` („Favoriten werden mit ihren > Server-IDs gemeldet") prüft heute genau das Full-Replace-Verhalten, das > Ziel 1 beseitigt. Sein MockClient beantwortet **jeden** Pfad außer `/list` > mit `{'status':'ok'}` (`:196`) — unter der neuen Regel liefert > `GET /favorites` dort 200 ohne `favorites`-Schlüssel und die Phase wird > übersprungen. Der Test wird auf den additiven Delta-Push umgeschrieben und > bekommt eine **explizite** `/favorites`-Antwort. > **Der Catch-All-Mock darf nicht aufgeweicht werden, um die neue > Sicherheitsregel zu umgehen** — das ist der schnellste Weg zu Grün und der > falsche. > - Dasselbe gilt abgeschwächt für die übrigen sieben Tests derselben Datei. > > **Neu, heute nicht in der Liste:** `PlaylistService` — der Sofort-Push ist der > einzige Online-Kanal der Spec und hat keinen einzigen Testeintrag. --- ### A11 · P2 · [VERIFIZIERT] [PLAN-RISIKO] · §Feature 2 (Z. 129-132), §Feature 4 (Z. 159-170) **Drei „existiert bereits"-Behauptungen korrigieren — und zwei ausdrücklich bestätigen.** §Feature 2: > Endpunkte: `GET/POST/DELETE /playlists`, `GET /`, `POST //songs`, > `DELETE //songs/`, `PUT //positions` (`melo_cloud.py:1251-1284`). > **Einen Rename-Endpunkt gibt es nicht** — kein `PUT`/`PATCH` auf die Playlist > selbst; `handle_rename:749` betrifft Songs. §Feature 4: > `ladeEinzelnenTitel(song)` ruft den bestehenden **öffentlichen** > `DownloadService.lade([song])` (`download_service.dart:72-111`) auf — der > bringt Doppel-Lauf-Schutz (`:73`), Verbindungsprüfung (`:77-84`) und > Fortschritts-Buchführung (`:85-104`) mit. **Nicht** `_ladeEinen` (`:113-139`) > direkt verdrahten. > `_LadeKnopf` ist eine **private** Klasse in > `lib/downloads/downloads_screen.dart:419-429`, kein wiederverwendbares Widget — > als Muster kopieren oder vorher extrahieren. *Zur Klarstellung im Dokument nicht nötig, aber für den Umsetzenden relevant: Die Spec-Behauptungen „der Pro-Song-Lade-Loop existiert bereits" und „der Auswahl-Modus in ‚Meine Musik' existiert" sind **beide richtig** — das Panel lag hier falsch (`sortable_song_list.dart:46-54, 194-196`).* --- ### A12 · P2 · [EINZELQUELLE: Richter] [BESTANDSFEHLER] · §Feature 1, neuer Satz „Geltungsbereich" **Die beiden parallelen Favoriten-Systeme benennen.** > **Geltungsbereich:** Der Favoriten-Abgleich betrifft ausschließlich die lokale > `Favorites`-Tabelle. Der Stern in der Server-Titel-Ansicht > (`ServerFavoriteButton`, `lib/shared/server_favorite_button.dart:42`, benutzt in > `now_playing_screen.dart:279`) ist ein **anderes** System — Navidrome-Starring > über `navidrome.setFavorite(navidromeId)` — und wird von dieser Spec nicht > angefasst. Beide erscheinen im Now-Playing-Screen als dasselbe Herz-Symbol; > dass sie getrennt bleiben, ist eine Entscheidung, keine Auslassung. > `PlaylistService.syncFavoritesFromServer()` (`playlist_service.dart:52-71`) ist > ein dritter Kanal und heute wirkungslos (`db.songExists(navidromeId)` gegen > lokale UUIDs) — er wird **nicht** Teil des Abgleichs. --- ### A13 · P2 · [EINZELQUELLE: Richter] [PLAN-RISIKO] · §Feature 1, Pull-Richtung **Regel für unauflösbare Server-Favoriten festschreiben.** > Server-Favoriten, deren cloudId sich lokal auf keinen Song abbilden lässt > (Download fehlgeschlagen, Titel noch nicht geladen), werden **übersprungen, > nicht gelöscht**: `Favorites.songId` verweist auf `Songs.id` > (`database.dart:102-107`); ein Favorit ohne Song wäre über `watchFavorites()` > (innerJoin, `:371-375`) unsichtbar, würde aber über `favoriteSongIds()` > (`:534-536`) weiter mitgeschleppt und wieder hochgepusht. Der additive > Abgleich schreibt sie deshalb weder lokal, noch entfernt er sie serverseitig. > Hinweis am selben Ort: `favoriteSongIds()` liefert auch Favoriten > **getombsteter** Songs (`allSongs()` schließt Grabsteine ein, `:216`). --- ### A14 · P2 · [VERIFIZIERT] (Auditor) [PLAN-RISIKO] · §Feature 3 (Z. 142-157) + §Feature 4 (Z. 159-170) + §Fehlerfälle **Fünf Entscheidungen treffen, die die Spec heute dem Umsetzenden überlässt.** > - **Abbrechen:** Der Auswahl-Upload bekommt `SyncService.abbrechen()` nach dem > Muster von `DownloadService` (`:46`, `:66-68`, `:96`) — ein Flag, das die > Schleife zwischen zwei Songs prüft. `SyncService` hat heute **keinerlei** > Abbruchmöglichkeit (0 Treffer für `abbrech|cancel` in 376 Zeilen); ohne das > ist ein versehentlich markierter 60-Titel-Upload nur durch App-Kill zu > stoppen — und die Spec bewirbt den Album-Download in Z. 19 ausdrücklich mit > „Fortschritt + Abbrechen". > - **Zeitstempel:** `ladeAusgewaehlteHoch` schreibt `_letzterLauf` (`:215-217`) > **nicht** — es ist kein Sync. Sonst unterdrückt ein manueller Upload 15 > Minuten Auto-Sync (`sollAutoSync`, `:120-121`) und verschiebt die 24-h-Uhr > des Berichts; wer die App täglich zum Hochladen öffnet, sähe den Bericht nie. > - **Kein Platz-Check, keine Schwelle beim Einzel-Song-Offline:** Die > 30er-Rückfrage (`download_service.dart:13`, `:15`, `:17-21`, Aufrufer nur > `downloads_screen.dart:460` und `:496`) ist bei einem Einzeltitel per > Konstruktion wirkungslos, und einen Check auf freien Speicher gibt es in der > ganzen App nicht. Bewusst akzeptiert: der Knopf lädt genau einen Titel. > - **Doppelspeicherung:** Vor dem Netz-Download prüft `ladeEinzelnenTitel`, ob > die Datei bereits im Abspiel-Cache liegt — sonst entsteht genau die > Doppelspeicherung, die `audio_handler.dart:326-331` in der Gegenrichtung > ausdrücklich verhindert („doppelter Platz und doppeltes Datenvolumen"). > Falls zu teuer: als bekannte Einschränkung dokumentieren, nicht schweigen. > - **Offline-Modus:** Der Schalter (`lib/services/offline_mode.dart`, > `settings_screen.dart:218-231`) wirkt heute nur auf die Wiedergabe > (`main.dart:106-110`). Entscheidung festhalten: Auswahl-Upload und > Einzel-Song-Offline **respektieren ihn nicht**, weil beides ausdrückliche > Nutzeraktionen sind. --- ### A15 · P2 · [VERIFIZIERT] (Auditor) [PLAN-RISIKO] · §Feature 5, Sync-Bericht (Z. 187-191) **Erstfall und „erfolgreich" definieren.** > `letzterLauf == null` (Neuinstallation, nach Abmelden, nach App-Daten-Löschen) > gilt **nicht** als fällig — sonst begrüßt der Bericht ein frisch installiertes > Gerät mit „Willkommen zurück! 325 neue Songs". Achtung: `sollAutoSync` > (`sync_service.dart:120-121`) behandelt `null` genau umgekehrt und ist hier > **kein** Vorbild. > „Erfolgreich" heißt: alle Phasen ohne geschluckten Fehler. Dafür führt > `synchronisiere()` ein Flag, das jede Phase bei einem Fehlschlag löscht; der > Bericht-Zeitstempel wird nur bei gesetztem Flag geschrieben. Heute schreibt > `:215-217` denselben Zeitstempel, egal ob Phasen ausgefallen sind > (`_meldeLoeschungen:243-252` schluckt Fehler). --- ### A16 · P3 · [VERIFIZIERT] (Auditor) · Kleinigkeiten, je ein Satz > - **§Sync-Phasen Z. 225 ↔ §Risiken Z. 250:** „Der Zeitstempel wird **vor** dem > Listen genommen und **nach** allen Phasen geschrieben." (Heute: > `_letzterLauf = DateTime.now()` in `:215`, nach allen Phasen.) > - **§Kontext Z. 19-20:** „automatischer Abspiel-Cache, Standard 2 GB, > einstellbar 0–8192 MB" (`app_settings.dart:28`, `:31`). > - **§Nicht-Ziele:** „Ein Rollback auf eine ältere App-Version ist nach der > Schema-Migration nicht vorgesehen — `database.dart:154-192` kennt nur > `onCreate` und `onUpgrade`." > - **§Offene Abhängigkeiten:** Schema-Divergenz `ist_korrupt` / `uq_user_song` > (nur in der Live-DB, nicht im `CREATE TABLE` des Skripts) als Backlog-Punkt — > trifft nur wiederhergestellte oder Test-Instanzen. > - **Backlog-Notiz:** `handle_favorites_get` liefert bereits `favorited_at` > (`melo_cloud.py:620`), lokal existiert `Favorites.createdAtMs` — die Zutat > für einen späteren echten Merge ist beidseitig vorhanden. --- ### A17 · P1 · [KONSENS] · §Neuer Abschnitt „Reihenfolge" (vor §Tests) **Auslieferungsreihenfolge festschreiben — sie fehlt heute ganz.** > 1. **Favoriten-Fix allein** (additiver Delta-Push, `parseFavoriten`, > Testanpassung). Wartet auf **nichts**: kein Server-Auftrag, keine neue > Dependency, keine Gradle-Änderung, kein neues UI-Muster. Einzige Stufe mit > belegtem Datenverlust-Bezug. > 2. Auswahl-Upload + Einzel-Song-Offline (UI, keine neue Dependency). > 3. Sync-Bericht (Stufe A, kein Plugin). > 4. Playlist-Sicherung Stufe 1 — **nach** dem Server-Fix `melo_cloud.py:505`. > 5. Notification Stufe B — beginnt mit dem grünen Beweis-Build. > 6. SSE — eigene Spec, nach dem `song_upload`-Event. --- ### §Offene Abhängigkeiten — geschlossene Liste (ersetzt Z. 274-281) | # | Auftrag | Art | Blockiert | |---|---|---|---| | 1 | `melo_cloud.py:505` — Owner-Prüfung in `handle_playlist_delete` | **Voraussetzung** | A5 / Feature 2 | | 2 | Datei-Wiederherstellung im Dedup-Zweig von `upload()` | **Voraussetzung, falls Antwort auf A6 „nein"** | Feature 3 | | 3 | `song_upload`-SSE-Event | Voraussetzung für die spätere SSE-Stufe | nichts in dieser Spec | | 4 | Playlist-Rename-Endpunkt (`PUT`/`PATCH`) | optional | nichts (A5 braucht ihn nicht mehr) | | 5 | Playlist-Tombstones serverseitig | optional / Backlog | nichts | --- ## 7. Meta-Beobachtung **Zur Spec-Qualität.** Das Dokument ist auffallend ehrlich in seinen Nicht-Zielen und faktisch überwiegend korrekt — rund zwanzig Bestandsbehauptungen haben fünf unabhängigen Prüfungen standgehalten, und zwei angebliche Fehler des Panels waren in Wahrheit Fehler des Panels. Seine zwei echten Schwächen sind typisch für Specs, die von jemandem geschrieben werden, der den Code gut kennt: Es **verwechselt „ich weiß, wie das geht" mit „das ist schon da"** (fünf zu optimistische Bestandssätze), und es **formuliert Sicherheit als Regel statt als Konstruktion** („nie ohne erfolgreiches GET POSTen") — Regeln haben Lücken, Konstruktionen nicht. Der größte Einzelmangel ist aber der Umfang: sechs Ziele, von denen genau eines auf nichts wartet und einen belegten Bug behebt, während die anderen fünf zusammen vier Server-Aufträge, eine Build-Umbauaktion und acht Fehlerklassen mitbringen. Das ist kein Denkfehler, das ist fehlende Reihenfolge — und sie ist mit einem Abschnitt behoben. **Zum Panel-Prozess.** Der Prozess war stark, wo er üblicherweise schwach ist, und schwach an einer Stelle, die der Prozess nicht vorsieht. *Stark:* Die Beleglage ist außergewöhnlich — ~70 geprüfte Datei:Zeile-Belege, **keine einzige Halluzination**, keine Fehlzuordnung. Mehrere Reviewer haben eigene P0 aktiv falsifiziert (DA hat seine eigene Rettungshypothese widerlegt, Risk hat sein eigenes P0 konzediert, Feasibility hat drei eigene Befunde fallen lassen). Das ist das Gegenteil von Konsens-Drift. *Schwach:* **Der Prozess prüft die Spec adversarial, aber die eigene Empfehlung nur konsensual.** Sobald Runde 2 auf den Basis-Snapshot konvergiert war, hat niemand ihn mehr mit derselben Härte angegriffen wie zuvor die Union — obwohl DA und Risk den entscheidenden Einwand (0.3) je einmal notiert hatten. Beide Phase-6-Zusammenfassungen führen ihn nicht als Finding. Ein Panel, das eine Therapie empfiehlt, braucht eine Runde, in der die *Therapie* der Review-Gegenstand ist. Erst die separate Verifikation hat das aufgedeckt — sie ist der wertvollste Bestandteil dieses Verfahrens gewesen. *Zweiter blinder Fleck:* Der Suchraum war zu klein. Zwei Runden über „Outbox oder Basis-Snapshot" haben den Schreibweg (`POST /favorites` als Voll-Ersatz) als gegeben behandelt. Die billigste Lösung lag außerhalb der Debattenachse — sie bestand darin, einen Aufruf zu **streichen**, nicht einen Mechanismus zu bauen. Das ist die wiederkehrende Blindstelle von Review-Panels: Sie sind darauf trainiert, Lücken zu finden, und finden deshalb Dinge, die man **hinzufügen** muss. *Dritter Punkt:* Die Testschuld wurde erst vom Vollständigkeits-Auditor entdeckt. Fünf Reviewer haben `sync_service.dart` zeilengenau seziert und `test/` nur einmal betreten. Für ein Projekt mit TDD-Pflichtworkflow ist das die bemerkenswerteste Lücke des ganzen Durchlaufs — und die mit den unmittelbarsten Folgen, weil der schnellste Weg zu Grün darin bestünde, die neue Sicherheitsregel im Mock wieder aufzuweichen. --- *Alle eigenen Prüfungen read-only (grep/sed/read auf `lib/`, `test/`, `android/`, `melo_cloud.py`). Keine git-Operationen, keine `.env`, keine Server-Prozesse. Geschrieben wurde ausschließlich diese Datei.*