Files
Melo/docs/reviews/2026-08-27-sync-ausbau/state/phase_14_judge_ruling.md
T
Hermes (Server)andClaude Sonnet 5 852c17cd48 Review-Panel: Sync-Ausbau-Spec (Verdikt 6/10, nachschärfen)
Adversariales 5-Reviewer-Panel (Opus) mit 2 Debattenrunden,
Vollständigkeits-Audit, konsolidierter Faktenprüfung (~70 Belege,
keine Halluzination) und Richterurteil.

Ergebnis: SPEC NACHSCHÄRFEN DANN FREIGEBEN. Kein P0 im Geltungsbereich.
17 Aktionspunkte, wichtigste: POST /favorites (Voll-Ersatz) streichen
statt Regel, SSE und beidseitigen Playlist-Merge herausnehmen,
irreversiblen Bestands-Löschpfad benennen (Backlog-P0, Rückfrage an
Dustin offen).

Bemerkenswert: Der vom Panel selbst empfohlene Basis-Snapshot wurde
von der Verifikation als gefährlicher entlarvt als das Problem, das
er lösen sollte — abgelehnt.

Reviewer-Rohtexte bleiben lokal (.gitignore), nur Bericht + Urteil
im Repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcDiyJdVRqh1TtJk5JiabX
2026-08-27 01:35:55 +02:00

52 KiB
Raw Blame History

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/<sid> und über loese_navidrome (:190-202) navidrome/music/<sid>. 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:120SortableSongList (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<SubsonicSong>) (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 (08192 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 /favoritesserver (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-1302handle_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/<sid> und navidrome/music/<sid> (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 /<id>, POST /<id>/songs, DELETE /<id>/songs/<sid>, PUT /<id>/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 08192 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.