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
52 KiB
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: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<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
(
ThreadingMixInohne 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:
- 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.
- Kein neuer Zustand. Kein Basis-Set, keine Aufräumpflicht beim Abmelden, keine Mengenalgebra. Weniger Teile als die Spec heute hat, nicht mehr.
- 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.
- 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:falsestattset:true) — kein Umbau. - 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 feuertmelo_cloud.pynicht (_emit_eventnur 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.Clientwegenmelo_cloud_service.dart:82-83; Heartbeat-Watchdog gegen den halboffenen Socket nach WLAN↔LTE; 401-Dauerabbruch, weilbaka_auth.dartkein Refresh hat; Selbst-Echo-Unterdrückung, weilmelo_realtime.py:56-70an alle Queues des Nutzers ohne Absenderkennung broadcastet; Timer-Abbau beipaused; 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 ausinitStateundresumed,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-1284gelesen, keinPUT/PATCHauf die Playlist selbst. Die Spec behauptet in Z. 129-130 das Gegenteil. POST /playlistsnicht idempotent, kein Unique (:488-500).handle_playlist_deletelöschtuser_playlist_songsohne Owner-Prüfung (:505) — echter Server-Bug.- „Längerer Stand gewinnt" ist bei Gleichstand undefiniert;
updatedAtMswird app-seitig nur increatePlaylist/deletePlaylistgepflegt. - N+1-Requests:
handle_playlist_listliefert nursong_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:
GET /favorites→server(Menge von cloudIds).- Für jede cloudId in
lokal \ server:POST /favorites/togglemit{"song_id": …, "set": true}(existiert und ist deterministisch:melo_cloud.py:1298-1302→handle_favorites_toggle:644-688; die Client-Methode ist neu).- Für jede cloudId in
server \ lokal: lokal Favorit setzen, sofern der Song lokal auflösbar ist.- 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 undfavoritesals Liste vorhanden ist. Ein fehlenderfavorites-Schlüssel ist ein Fehler, keine leere Menge;parseFavoritenwirft dannCloudException, genau wieparseListe(melo_cloud_service.dart:99-100) undparseUpload(:111-112) es bereits tun. Hintergrund: der Router verdrahtet fürGET /favoriteshart 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-101prü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:truewird zusätzlichset: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: LiefertGET /favoritesfälschlich eine leere Liste, wirdBasis \ server = Basis, die Formel kollabiert zulokal − Basisund 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 NULLbleibt). 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 /playlistsund ist stabil (user_playlists.idistINTEGER 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:505löschtuser_playlist_songsfür jedeplaylist_idohneuser-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öschtregistry/<sid>undnavidrome/music/<sid>(melo_cloud.py:356-372,:190-202); für die Datei gibt es keinen Grabstein. Erneutes Hochladen repariert es nicht: der überlebenderegistry.sha256erzwingt den Dedup-Zweig (:265-267),shutil.movesteht nur im Neu-Zweig (:289),os.remove(tmp)verwirft die Bytes (:312),/downloadantwortet 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, dasmelo_cloud.pyheute nicht feuert (_emit_eventnur 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: eigenerhttp.Client(der geteilte ausmelo_cloud_service.dart:82-83würde beimclose()laufende Sync-Requests mitreißen), Heartbeat-Watchdog gegen den halboffenen Socket nach WLAN↔LTE, dauerhafter 401-Ausstieg (baka_auth.darthat kein Refresh), Selbst-Echo-Unterdrückung (melo_realtime.py:56-70broadcastet an alle Queues des Nutzers ohne Absenderkennung), Timer-Abbau beipaused, 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 ausinitStateundresumed(main.dart:175,:203), ein Timer existiert nicht. SSE wird eigene Stufe, sobald dassong_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_notificationsund damitisCoreLibraryDesugaringEnabled = truepluscoreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")inandroid/app/build.gradle.kts— die Datei hat heute überhaupt keinendependencies { }-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ünerflutter build apk --target-platform android-arm64nach dempub add— vor jeder Zeile Notification-Logik.multiDexEnabledentfällt aus der Forderung: beiminSdkVersion = 24(FlutterExtension.kt:26) ist es gegenstandslos. Mitzuentscheiden: Channel mitImportance.low,onlyAlertOnce: true, und eincancel()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 imfinallygenau einen weiteren Lauf auslöst.- Der Upload überlebt keinen Timeout:
_ladeHochfängt nurCloudException(:313); der 120-s-Timeout (melo_cloud_service.dart:180) wirftTimeoutExceptionund reißt den ganzen Lauf ab. Timeouts sind wie Einzelfehler zu behandeln.- Die Drossel sitzt in
automatisch()(:166-170), nicht insynchronisiere()(:174-180) — siehe A7,erzwingeentfä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/listmit{'status':'ok'}(:196) — unter der neuen Regel liefertGET /favoritesdort 200 ohnefavorites-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 — keinPUT/PATCHauf die Playlist selbst;handle_rename:749betrifft Songs.
§Feature 4:
ladeEinzelnenTitel(song)ruft den bestehenden öffentlichenDownloadService.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._LadeKnopfist eine private Klasse inlib/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 innow_playing_screen.dart:279) ist ein anderes System — Navidrome-Starring übernavidrome.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.songIdverweist aufSongs.id(database.dart:102-107); ein Favorit ohne Song wäre überwatchFavorites()(innerJoin,:371-375) unsichtbar, würde aber überfavoriteSongIds()(: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 vonDownloadService(:46,:66-68,:96) — ein Flag, das die Schleife zwischen zwei Songs prüft.SyncServicehat heute keinerlei Abbruchmöglichkeit (0 Treffer fürabbrech|cancelin 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:
ladeAusgewaehlteHochschreibt_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 nurdownloads_screen.dart:460und: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, dieaudio_handler.dart:326-331in 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) behandeltnullgenau umgekehrt und ist hier kein Vorbild. „Erfolgreich" heißt: alle Phasen ohne geschluckten Fehler. Dafür führtsynchronisiere()ein Flag, das jede Phase bei einem Fehlschlag löscht; der Bericht-Zeitstempel wird nur bei gesetztem Flag geschrieben. Heute schreibt:215-217denselben Zeitstempel, egal ob Phasen ausgefallen sind (_meldeLoeschungen:243-252schluckt 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-192kennt nuronCreateundonUpgrade."- §Offene Abhängigkeiten: Schema-Divergenz
ist_korrupt/uq_user_song(nur in der Live-DB, nicht imCREATE TABLEdes Skripts) als Backlog-Punkt — trifft nur wiederhergestellte oder Test-Instanzen.- Backlog-Notiz:
handle_favorites_getliefert bereitsfavorited_at(melo_cloud.py:620), lokal existiertFavorites.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.
- 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.- Auswahl-Upload + Einzel-Song-Offline (UI, keine neue Dependency).
- Sync-Bericht (Stufe A, kein Plugin).
- Playlist-Sicherung Stufe 1 — nach dem Server-Fix
melo_cloud.py:505.- Notification Stufe B — beginnt mit dem grünen Beweis-Build.
- 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.