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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3a376faf50
commit
852c17cd48
@@ -49,3 +49,7 @@ app.*.map.json
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Review-Panel-Zwischenstände (Reviewer-Rohtexte, ~500 KB) — nur der
|
||||
# Abschlussbericht und das Richter-Urteil gehören ins Repo
|
||||
docs/reviews/*/state/
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# Context Brief — Review der Sync-Ausbau-Spec
|
||||
|
||||
**Review-Gegenstand:** `/home/dustin/mello-dev/app/docs/superpowers/specs/2026-08-27-sync-ausbau-design.md`
|
||||
|
||||
**Review-Modus:** Exhaustive (reines Design-Dokument). ABER: Die Spec macht
|
||||
viele FAKTISCHE Behauptungen über existierenden Code — solche Behauptungen
|
||||
bitte präzise gegen den echten Code prüfen (grep/read, read-only).
|
||||
|
||||
**Codebase-Zustand:** Branch `fix/p0-vollwertigkeit` in
|
||||
`/home/dustin/mello-dev/app` (kein Worktree-Sonderfall, Branch aktuell mit
|
||||
origin). Server-Code: `/home/dustin/scripts/melo_cloud.py` (read-only
|
||||
ansehen erlaubt, NIE ändern).
|
||||
|
||||
## Projekt
|
||||
|
||||
Melo: Flutter-Musik-App (Android), schwarz/rot, 5 Tabs. Backend:
|
||||
Navidrome (Subsonic, read-only) + Melo-Cloud (`melo_cloud.py`,
|
||||
cloud.baka-net.de, Bearer-JWT via BakaAuth) + yt-Proxy. 602 Tests grün.
|
||||
Nutzer: Dustin (Admin), Baka, Tinker — kleine private Nutzerbasis,
|
||||
~330 Songs auf dem Server. Projekt-Prinzip „Ponytail“: minimale, robuste
|
||||
Lösungen; YAGNI wird ernst genommen.
|
||||
|
||||
## Bereits vom Auftraggeber ENTSCHIEDEN (im Brainstorming, mit Begründung)
|
||||
|
||||
Reviewer dürfen diese Entscheidungen kritisieren, sollen aber wissen, dass
|
||||
sie bewusst und informiert getroffen wurden:
|
||||
1. Cross-Platform (Mac/Win) = eigenes späteres Projekt, NICHT diese Spec.
|
||||
2. Upload-UX: Auswahl-Modus-Erweiterung; Auto-Upload bleibt zusätzlich.
|
||||
3. Playlist-Konflikte: Union-Merge (kein LWW, kein read-only).
|
||||
4. Kein Delta-Protokoll, kein Hintergrund-Sync, kein Chunked-Upload (YAGNI).
|
||||
5. Favoriten-Merge-Fix ist gesetzt (Datenverlust-Bug).
|
||||
6. Umfang: Einzel-Song-Offline + Playlist-Sync + Notification/Bericht + SSE.
|
||||
|
||||
## Verifizierte Schlüsselfakten aus der Bestandsaufnahme (2026-08-27)
|
||||
|
||||
App-Sync (lib/services/sync_service.dart, melo_cloud_service.dart):
|
||||
- Upload existiert: Multipart POST /upload, 50 MB Limit, 120s Timeout;
|
||||
Server-Dedup sha256 + fpcalc-Fingerprint (Jaccard>0.7).
|
||||
- planeSync() lädt ALLE Songs ohne cloudId hoch; Tombstones beidseitig;
|
||||
Lösch-Bremse nur für serverLoeschen (>10 && >1/3).
|
||||
- _gleicheFavoritenAb() POSTet lokale Favoriten als KOMPLETT-ERSATZ
|
||||
(der Datenverlust-Bug); MeloCloudService.favoriten() (GET) existiert,
|
||||
wird nie aufgerufen.
|
||||
- Auto-Sync bei Start/Resume, 15-Min-Drossel (sollAutoSync); Fortschritt
|
||||
als ChangeNotifier-Felder, UI nur in settings_screen.dart.
|
||||
- Kein SSE in der App, kein Playlist-Sync in der App.
|
||||
|
||||
Server (melo_cloud.py):
|
||||
- Endpunkte: list/upload/download/stream(Range)/delete/update/rename/
|
||||
playlists(CRUD+songs+positions)/favorites(GET/POST/toggle mit set)/
|
||||
history/sync(since)/subscribe(SSE, Heartbeat 30s).
|
||||
- SSE-Events: song_delete, song_update, song_favorite werden emittiert;
|
||||
UPLOAD feuert KEIN Event; playlist_update dokumentiert aber nie emittiert.
|
||||
- Playlists/Favoriten: harte DELETEs, KEINE Tombstones.
|
||||
- handle_favorites_sync = Full-Replace. handle_list liefert immer ALLES
|
||||
inkl. Grabsteine (kein ETag/Since).
|
||||
- Uploads landen in /var/lib/melo-cloud/registry/ und werden AKTIV nach
|
||||
/home/dustin/navidrome/music gehardlinkt + Navidrome-Scan getriggert.
|
||||
- Achtung bekanntes Server-Risiko: handle_list selektiert Spalte
|
||||
ist_korrupt, die im CREATE-TABLE-Schema fehlt (nur Live-DB hat sie).
|
||||
- Uploads/Downloads liegen serverseitig komplett im RAM.
|
||||
|
||||
Offline (download_service.dart u. a.):
|
||||
- Alben/Künstler-Download in App-Speicher (Application-Support/
|
||||
melo_downloads), Drift-Tabelle Downloads (navidromeId), Fortschritt+
|
||||
Abbrechen; Rückfrage ab 30 Titeln. Abspiel-Cache 2 GB LRU getrennt davon.
|
||||
|
||||
Melo v2 (Referenz, /home/dustin/mello-dev/referenz/melo-app-code.md):
|
||||
- Hatte: bidirektionalen Favoriten-Merge (Union) + deterministischen
|
||||
set-Push beim Toggle; persistente Sync-Notification (i/n); Sync-Bericht
|
||||
nach >24h; Konflikt-Dialog. Bekannte v2-Lektionen: Sync-Zeitstempel =
|
||||
Snapshot VOR listSongs (Tombstone-Race MED-1); neue Songs sofort MIT
|
||||
cloudId in DB (Doppel-Download MED-2); Dateinamen-Kollisionszähler.
|
||||
|
||||
Plattform:
|
||||
- flutter_local_notifications ist NICHT in pubspec.yaml (wäre neu).
|
||||
- http-Paket vorhanden (client.send für SSE-Streaming möglich).
|
||||
- POST_NOTIFICATIONS wird seit kurzem beim App-Start angefragt.
|
||||
- Drift-DB mit Migrationen vorhanden (database.dart, schemaVersion).
|
||||
|
||||
## Regeln für Reviewer
|
||||
|
||||
- READ-ONLY: nichts schreiben außer der eigenen State-Datei, keine
|
||||
git-Operationen, keine Server-Prozesse anfassen, keine .env lesen.
|
||||
- Jede Behauptung über existierenden Code mit Datei:Zeile belegen
|
||||
(grep/read). Behauptungen ohne Beleg als [UNVERIFIED] taggen.
|
||||
- Findings-Severity: P0 (blockiert die Spec / Datenverlust), P1 (muss vor
|
||||
Implementierung geklärt werden), P2 (sollte), P3 (nice to have).
|
||||
- Für jedes P0/P1: falsifizierbaren Check nennen (welcher grep/read würde
|
||||
es widerlegen?), wenn möglich als verification_command (read-only).
|
||||
@@ -0,0 +1,130 @@
|
||||
# Review-Panel-Bericht — Sync-Ausbau-Spec
|
||||
|
||||
**Gegenstand:** `docs/superpowers/specs/2026-08-27-sync-ausbau-design.md`
|
||||
**Datum:** 2026-08-27
|
||||
**Panel:** 5 Reviewer (Feasibility, Risk, Devil's Advocate, Flutter/Android,
|
||||
Sync-/Verteilte-Systeme) + Vollständigkeits-Auditor + konsolidierter
|
||||
Verifizierer + Oberster Richter. Alle Modell: Opus.
|
||||
**Ablauf:** Phase 3 (unabhängig) → 4 (Reflexion) → 5 (Debatte, 2 Runden) → 7
|
||||
(blinde Schlussurteile) → 8 (Audit) → 10+11 (Zitat- + Severity-Prüfung) → 14
|
||||
(Urteil).
|
||||
**Review-Modus:** Exhaustive (Design-Dokument), aber alle Bestandsbehauptungen
|
||||
gegen echten Code geprüft.
|
||||
|
||||
**Verdikt: SPEC NACHSCHÄRFEN DANN FREIGEBEN · Score 6/10 · Konfidenz: Hoch**
|
||||
|
||||
Panel-Scores: 6 / 5 / 5 / 5 / 5 (Mittel 5,2). Der Richter liegt darüber, weil
|
||||
die Faktenprüfung zwei tragende Säulen der Panel-Kritik entfernt hat.
|
||||
|
||||
---
|
||||
|
||||
## Kurzfassung
|
||||
|
||||
Die Spec ist **im Kern richtig und faktisch überwiegend korrekt** — rund zwanzig
|
||||
Behauptungen über den Bestandscode haben fünf unabhängigen Prüfungen
|
||||
standgehalten, und in zwei Fällen lag das Panel falsch, nicht die Spec. Es gibt
|
||||
**kein P0 im Geltungsbereich**: Der Datenverlust-Bug, den die Spec beheben will,
|
||||
kann heute nichts verlieren (die Live-DB hat 0 Favoriten, und
|
||||
`MeloCloudService.favoriten()` hat null Aufrufer).
|
||||
|
||||
Drei belegte Substanzmängel verhindern die direkte Freigabe:
|
||||
1. Eine **falsche Garantie** über Lösch-Propagation.
|
||||
2. **Fünf zu optimistische Bestandsbehauptungen** („existiert bereits", wo es
|
||||
das nicht tut).
|
||||
3. **Zu großer Umfang** für einen Wurf: sechs Ziele, von denen genau eines auf
|
||||
nichts wartet und einen echten Bug behebt.
|
||||
|
||||
**Der wichtigste Einzelbefund richtet sich gegen das Panel, nicht gegen die
|
||||
Spec:** Das Panel konvergierte in Debattenrunde 2 auf einen „Basis-Snapshot" als
|
||||
Merge-Mechanismus. Die separate Verifikation hat nachgerechnet — liefert
|
||||
`GET /favorites` fälschlich eine leere Menge (was `parseFavoriten` heute bei
|
||||
jedem Fehler im 200er-Körper tut, und sechs Handler desselben Servers antworten
|
||||
so), kollabiert die Formel und **löscht den gesamten Bestand auf allen Geräten**.
|
||||
Der empfohlene Ersatz wäre gefährlicher gewesen als das Problem. Er ist
|
||||
abgelehnt.
|
||||
|
||||
---
|
||||
|
||||
## Umfang und Grenzen
|
||||
|
||||
Geprüft wurde ein Design-Dokument, kein laufender Code. Nicht bewertbar:
|
||||
Laufzeitverhalten, echte Gerätetests, Server-Last unter echten Bedingungen.
|
||||
Alle Code-Aussagen sind read-only verifiziert (~70 Datei:Zeile-Belege).
|
||||
|
||||
Epistemik-Labels: [VERIFIZIERT] [KONSENS] [EINZELQUELLE] [UNVERIFIZIERT]
|
||||
Typ-Labels: [BESTANDSFEHLER] (existiert heute) [PLAN-RISIKO] (entsteht erst
|
||||
durch Umsetzung)
|
||||
|
||||
---
|
||||
|
||||
## Konsens-Punkte (vom Richter bestätigt)
|
||||
|
||||
- Die **Lösch-Kette ist Glied für Glied bestätigt** [VERIFIZIERT]
|
||||
[BESTANDSFEHLER]: Verschwindet eine Datei lokal (SD-Karte nicht eingehängt,
|
||||
Dateimanager, Verschieben), markiert der Scan sie als gelöscht, der Sync meldet
|
||||
das dem Server, und der Server löscht die Audiodatei aus **beiden** Orten
|
||||
(Registry + Navidrome-Ordner). Für die Datei gibt es keinen Grabstein, der
|
||||
Reparaturweg ist verifiziert kaputt. Die Lösch-Bremse greift bei 325 Titeln
|
||||
erst ab 109 gleichzeitigen Löschungen. **Nicht von dieser Spec verursacht** —
|
||||
bisher nie ausgelöst (`SUM(deleted)=0`), aber ein echter Bestandsfehler.
|
||||
- Die **Merge-Semantik der Playlisten ist unterspezifiziert** [KONSENS]: vier
|
||||
offene Entscheidungen (Identität, Reihenfolge bei Gleichstand,
|
||||
Tombstone-Verhalten, Namens-Zuordnung als Nicht-Funktion).
|
||||
- **`flutter_local_notifications` erzwingt Gradle-Umbau** (Desugaring) —
|
||||
der einzige harte Build-Blocker der Planung.
|
||||
|
||||
## Wo das Panel falsch lag (Verifikation korrigiert)
|
||||
|
||||
- „Auswahl-Modus existiert so nicht" — **widerlegt**, er existiert
|
||||
(`sortable_song_list.dart`).
|
||||
- „Pro-Song-Lade-Loop bringt keinen Fortschritt/Doppel-Lauf-Schutz mit" —
|
||||
**widerlegt**, `DownloadService.lade()` bringt beides mit. **Die Spec hatte
|
||||
recht.**
|
||||
- „Navidrome-Playlist-Import erzeugt Merge-Explosion" — **widerlegt**, die
|
||||
importierten Playlisten sind leer (ID-Räume treffen nie).
|
||||
- „SSE erschöpft den Worker-Pool" — **vom Panel selbst zurückgezogen**
|
||||
(ThreadingMixIn ohne Pool).
|
||||
|
||||
---
|
||||
|
||||
## Aktionsliste (17 Punkte, priorisiert)
|
||||
|
||||
Vollständig mit Formulierungsvorschlägen in
|
||||
`state/phase_14_judge_ruling.md`, Abschnitt 6. Die wichtigsten:
|
||||
|
||||
| # | Sev | Änderung |
|
||||
|---|---|---|
|
||||
| A1 | P1 | **`POST /favorites` (Voll-Ersatz) ersatzlos streichen** — nur noch additive `toggle set:true` für `lokal \ server`. Macht den Datenverlust-Bug *strukturell* unmöglich statt per Regel. |
|
||||
| A2 | P1 | **`parseFavoriten` härten** (Fehler im 200er-Körper werfen, nicht `[]` liefern) + den grünen Bestandstest mitändern, der heute das Gegenteil festschreibt. |
|
||||
| A3 | P1 | **Falsche Lösch-Garantie streichen** — auch *online* entfernte Herzen propagieren nicht zuverlässig. |
|
||||
| A4 | P1 | **Basis-Snapshot ausdrücklich ablehnen** und begründen (Gegen-Empfehlung zum Panel). |
|
||||
| A5 | P1 | **Playlist-Sync auf einseitige Sicherung reduzieren** — löst alle vier offenen Semantik-Fragen ersatzlos auf. Beidseitiger Merge wird eigene Spec. |
|
||||
| A6 | P1 (Spec) / **P0 (Backlog)** | **Irreversiblen Löschpfad benennen** — plus die eine Frage, die kein Reviewer entscheiden kann (siehe unten). |
|
||||
| A7 | P1 | **SSE herausnehmen** — der Hauptnutzen hängt am `song_upload`-Event, das die Spec selbst auslagert; acht Fehlerklassen für heute null Adressaten. |
|
||||
| A8 | P1 | **Notification halbieren**: Sync-Bericht sofort, persistente Notification als eigene Stufe mit Beweis-Build. |
|
||||
| A9 | P1 | **Fehlerfälle von Zusagen auf zu bauende Arbeit umstellen** — fünf „bestehende Schutzmechanismen" gibt es nicht. |
|
||||
| A10 | P1 | **Testliste um Bestandsänderungen erweitern** — sie nennt heute nur neue Tests. |
|
||||
| A17 | P1 | **Auslieferungsreihenfolge festschreiben** — fehlt heute ganz. |
|
||||
| A11–A16 | P2/P3 | Bestandsbehauptungen korrigieren, offene Entscheidungen treffen, Kleinigkeiten. |
|
||||
|
||||
**Bedingung des Richters:** Die nachgeschärfte Fassung wird einmal kurz
|
||||
gegengelesen, bevor der Implementierungsplan entsteht — A1, A5 und A7 ändern,
|
||||
*was* gebaut wird, nicht nur *wie* es beschrieben ist.
|
||||
|
||||
---
|
||||
|
||||
## Meta-Beobachtung zum Verfahren
|
||||
|
||||
Bemerkenswert stark: ~70 geprüfte Belege, **keine einzige Halluzination**,
|
||||
keine Fehlzuordnung. Mehrere Reviewer haben eigene P0-Befunde aktiv falsifiziert
|
||||
— das Gegenteil von Konsens-Drift.
|
||||
|
||||
Bemerkenswert schwach, und lehrreich: **Das Panel prüfte die Spec adversarial,
|
||||
seine eigene Empfehlung aber nur konsensual.** Sobald Runde 2 auf den
|
||||
Basis-Snapshot konvergiert war, griff ihn niemand mehr mit derselben Härte an.
|
||||
Erst die separate Verifikation deckte auf, dass er selbst einen Löschpfad
|
||||
schafft. Zweitens war der Suchraum zu klein: zwei Runden „Outbox oder
|
||||
Basis-Snapshot" behandelten den Schreibweg als gegeben — die billigste Lösung
|
||||
bestand darin, einen Aufruf zu **streichen**, nicht einen Mechanismus zu bauen.
|
||||
Drittens: die Testschuld fand erst der Vollständigkeits-Auditor; fünf Reviewer
|
||||
sezierten `sync_service.dart` zeilengenau und betraten `test/` nur einmal.
|
||||
@@ -0,0 +1,864 @@
|
||||
# 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
|
||||
(`ThreadingMixIn` ohne Pool, `melo_cloud.py:1449-1453`).
|
||||
- „alle 33 `handle_*`" (Feasibility) — es sind 32. Ohne Folge.
|
||||
|
||||
**0.5 Was die Verifikation dem Panel *hinzufügt*. [VERIFIZIERT]**
|
||||
`melo_cloud.py:505` löscht `user_playlist_songs` für **jede** `playlist_id` ohne
|
||||
`user`-Bedingung. Das ist nicht nur ein falsches Erfolgssignal, sondern echte
|
||||
Fremddaten-Löschung. Heute folgenlos (0 Playlisten), aber die Spec befördert
|
||||
genau diesen Endpunkt zum Regelpfad.
|
||||
|
||||
**0.6 Severity-Dämpfung angewandt.**
|
||||
Ein reines [PLAN-RISIKO], das eine sorgfältige Implementierung vermeiden kann,
|
||||
ist höchstens P1. Ein [BESTANDSFEHLER], den diese Spec nicht verursacht, ist
|
||||
Backlog-Punkt, kein Blocker. Nach diesem Maßstab bleibt von der Panel-Liste:
|
||||
**0 × P0, 10 × P1, 8 × P2, 4 × P3.** Die Dringlichkeitsrhetorik des Panels ist
|
||||
durch den Live-Stand entkräftet: ein Nutzer mit Songs, 0 gelöschte, 0 Favoriten,
|
||||
0 Playlisten.
|
||||
|
||||
---
|
||||
|
||||
## 1. Entscheidung über die verbliebenen Meinungsverschiedenheiten
|
||||
|
||||
### 1.1 Merge-Mechanismus: Union vs. Basis-Snapshot vs. Drei-Wege-Merge
|
||||
|
||||
**Beide Lager haben in ihrer Diagnose recht und in ihrer Therapie unrecht. Ich
|
||||
entscheide für eine dritte Option, die keines der beiden vorgeschlagen hat.**
|
||||
|
||||
Was feststeht:
|
||||
- **Union kann „entfernt" nicht ausdrücken. [VERIFIZIERT]** Der Zyklus des
|
||||
Sync-Spezialisten hält, und er ist schärfer als die Spec zugibt: A ent-favorisiert
|
||||
**online**, Server ∅; B (das seither nicht synchronisiert hat, also praktisch
|
||||
immer) synchronisiert, `Union({X}, ∅) = {X}`, POSTet `{X}`; A holt X beim
|
||||
nächsten Lauf zurück. Die Spec-Zusage in Z. 108-112 („Ent-Favorisierungen
|
||||
propagieren über den Sofort-Push (online)") ist damit **falsch**, nicht nur
|
||||
unvollständig. Sie muss weg.
|
||||
- **Der Basis-Snapshot heilt das, führt aber 0.3 ein.** Dazu braucht er
|
||||
kontogebundenen Zustand, der beim Abmelden mitgelöscht werden muss —
|
||||
`BakaAuth.abmelden` (`lib/services/baka_auth.dart:114-120`) räumt heute nichts
|
||||
ab, und ein Token-Refresh gibt es auch nicht. Das ist neue Persistenz mit einer
|
||||
neuen Aufräumpflicht für drei Nutzer mit null Favoriten.
|
||||
|
||||
**Was das Panel übersehen hat (mein eigener Befund, siehe auch Abschnitt 3):**
|
||||
Die eigentliche Gefahrenquelle ist gar nicht die Union — es ist der
|
||||
**Full-Replace-`POST /favorites`**. Solange die Sync-Phase den kompletten
|
||||
Server-Stand ersetzt, hängt die Datensicherheit an einer *Regel*
|
||||
(GET-vor-POST), und jede Lücke in dieser Regel (`parseFavoriten`!) wird sofort
|
||||
zum Datenverlust. Streicht man den Full-Replace, verschwindet die ganze
|
||||
Fehlerklasse **strukturell**.
|
||||
|
||||
**URTEIL — additiver Delta-Abgleich, kein Voll-Ersatz, kein Basis-Snapshot:**
|
||||
|
||||
| | schreibt Server | schreibt lokal | neuer Zustand | neuer Löschpfad |
|
||||
|---|---|---|---|---|
|
||||
| heute | `POST /favorites` (Voll-Ersatz) | — | — | **ja, der Bug** |
|
||||
| Spec (Union + Voll-Ersatz) | `POST /favorites` | ja | — | ja, sobald GET `∅` täuscht |
|
||||
| Panel (Basis-Snapshot) | Delta-Push | ja | Basis je Datentyp, kontogebunden | **ja (0.3)** |
|
||||
| **Urteil (additiv)** | `POST /favorites/toggle` `set:true`, nur für `lokal \ server` | nur additiv | **keiner** | **keiner** |
|
||||
|
||||
Der Endpunkt existiert und ist deterministisch — eigene Prüfung:
|
||||
`melo_cloud.py:1298-1302` (Route, `set` aus dem JSON-Körper) →
|
||||
`handle_favorites_toggle` (`:644-688`), `set_state is True/False` erzwingt den
|
||||
Zielzustand. Im Client fehlt nur die Methode (kein Treffer für `toggle` in
|
||||
`melo_cloud_service.dart`).
|
||||
|
||||
Warum das die richtige Antwort für dieses Projekt ist:
|
||||
1. **Ziel 1 wird vollständig erreicht** („kein Gerät überschreibt mehr die
|
||||
Server-Favoriten") — und zwar durch Konstruktion statt durch eine Regel. Es
|
||||
gibt danach keinen Codepfad mehr, der den Server-Stand ersetzen kann.
|
||||
2. **Kein neuer Zustand.** Kein Basis-Set, keine Aufräumpflicht beim Abmelden,
|
||||
keine Mengenalgebra. Weniger Teile als die Spec heute hat, nicht mehr.
|
||||
3. **Der Fehlerpfad ist selbstheilend.** Täuscht der GET eine leere Menge vor,
|
||||
pusht das Gerät seine lokalen Favoriten additiv hoch — harmlos und
|
||||
idempotent. Beim Basis-Snapshot wäre derselbe Fehler eine geräteübergreifende
|
||||
Massenlöschung.
|
||||
4. **Der spätere Ausbau ist nicht verbaut.** Wenn sich in der Praxis zeigt, dass
|
||||
Ent-Favorisieren wirklich stört, ist der Basis-Snapshot ein *Aufsatz* auf
|
||||
denselben Schreibweg (`set:false` statt `set:true`) — kein Umbau.
|
||||
5. **Preis, ehrlich benannt:** Ent-Favorisieren bleibt geräteübergreifend
|
||||
unzuverlässig. Das ist dieselbe Einschränkung, die die Spec ohnehin schon
|
||||
akzeptiert — nur muss sie **richtig** beschrieben werden (nicht „offline",
|
||||
sondern „auch online, sobald ein zweites Gerät den alten Stand hält").
|
||||
|
||||
**Damit ist die Auftraggeber-Entscheidung „Union-Merge" nicht überstimmt,
|
||||
sondern präzisiert:** die Vereinigungs-*Semantik* bleibt genau wie entschieden;
|
||||
nur der *Schreibweg* wechselt vom Voll-Ersatz auf additive Pushes. Die
|
||||
Panel-Empfehlung Basis-Snapshot wird **abgelehnt** — mit der Begründung aus 0.3.
|
||||
Sie kommt als benannter Backlog-Punkt ins Dokument, nicht als Bestandteil.
|
||||
|
||||
### 1.2 SSE streichen — ja
|
||||
|
||||
**Das Panel steht 3:2 für Behalten (mit acht Härtungs-Bausteinen). Ich
|
||||
entscheide gegen die Mehrheit, aber mit einem Argument, das niemand widerlegt
|
||||
hat.** Nicht „SSE ist schlecht", sondern: **die eigene Vorbedingung von SSE ist
|
||||
in dieser Spec nicht erfüllt.**
|
||||
|
||||
- Der beworbene Haupt-Nutzen („neuer Song erscheint in Sekunden auf dem anderen
|
||||
Gerät") braucht das `song_upload`-Event. Das feuert `melo_cloud.py` nicht
|
||||
(`_emit_event` nur bei `:393 song_delete`, `:410 song_update`,
|
||||
`:687 song_favorite` — **[VERIFIZIERT]**), und die Spec lagert es
|
||||
ausdrücklich aus (Z. 211-215, 276-278). **Am Auslieferungstag kann SSE genau
|
||||
das nicht, wofür es gebaut wird.**
|
||||
- Die drei vorhandenen Kanäle haben heute keinen Adressaten: Live-DB `Baka|325`,
|
||||
0 gelöschte, 0 Favoriten, 0 Playlisten. **[VERIFIZIERT]**
|
||||
- Dem stehen acht eigene, je einzeln belegte Fehlerklassen gegenüber (eigener
|
||||
`http.Client` wegen `melo_cloud_service.dart:82-83`; Heartbeat-Watchdog gegen
|
||||
den halboffenen Socket nach WLAN↔LTE; 401-Dauerabbruch, weil `baka_auth.dart`
|
||||
kein Refresh hat; Selbst-Echo-Unterdrückung, weil `melo_realtime.py:56-70` an
|
||||
alle Queues des Nutzers ohne Absenderkennung broadcastet; Timer-Abbau bei
|
||||
`paused`; Debounce; Backoff; Nachhol-Anstoß). Kein anderes Ziel der Spec trägt
|
||||
annähernd so viel Fehleroberfläche.
|
||||
- Dieselbe Spec lehnt Delta-Protokoll, Hintergrund-Sync und Chunked-Upload mit
|
||||
YAGNI ab. SSE ist die einzige Ausnahme von der eigenen Regel.
|
||||
- Der ehrliche Preis ist klein und vom Panel selbst festgestellt: Es gibt
|
||||
**keinen** billigen Poll-Ersatz (`automatisch()` läuft nur aus `initState` und
|
||||
`resumed`, `main.dart:175`, `:203`, kein Timer — **[VERIFIZIERT]**). Der Tausch
|
||||
lautet: Änderungen anderer Geräte erscheinen beim nächsten App-Start oder
|
||||
Zurückkehren — **genau wie heute**. Für drei private Nutzer ist das der
|
||||
richtige Preis für acht entfallende Fehlerklassen.
|
||||
|
||||
**URTEIL: SSE (Ziel 6, Feature 6, `echtzeit_sync.dart`) wird aus dieser Spec
|
||||
herausgenommen und als eigene, spätere Stufe geführt — Vorbedingung: das
|
||||
`song_upload`-Event steht.** Das ist keine Verwerfung, sondern Reihenfolge. Fällt
|
||||
mit weg: der Parameter `erzwinge` (wäre ohnehin wirkungslos — die Drossel sitzt
|
||||
in `automatisch()` `:166-170`, nicht in `synchronisiere()` `:174-180`,
|
||||
**[VERIFIZIERT]**).
|
||||
|
||||
### 1.3 Playlist-Sync drin lassen oder herausschneiden — reduzieren
|
||||
|
||||
Risk formuliert es korrekt: entweder vier Semantik-Entscheidungen treffen oder
|
||||
herausschneiden; das Weiterlaufen im jetzigen Zustand nicht. Der Befundstand:
|
||||
- Namenszuordnung ist keine Funktion (gleichnamige Playlisten per Knopfdruck
|
||||
erzeugbar, `playlist_service.dart:75-109`).
|
||||
- Kein Rename-Endpunkt — **[VERIFIZIERT]**, vollständiger Routing-Block
|
||||
`melo_cloud.py:1251-1284` gelesen, kein `PUT`/`PATCH` auf die Playlist selbst.
|
||||
Die Spec behauptet in Z. 129-130 das Gegenteil.
|
||||
- `POST /playlists` nicht idempotent, kein Unique (`:488-500`).
|
||||
- `handle_playlist_delete` löscht `user_playlist_songs` ohne Owner-Prüfung
|
||||
(`:505`) — echter Server-Bug.
|
||||
- „Längerer Stand gewinnt" ist bei Gleichstand undefiniert; `updatedAtMs` wird
|
||||
app-seitig nur in `createPlaylist`/`deletePlaylist` gepflegt.
|
||||
- N+1-Requests: `handle_playlist_list` liefert nur `song_count`.
|
||||
- Live-Stand: **0 Playlisten auf dem Server.**
|
||||
|
||||
Das ist kein Nachschärf-Fall, sondern ein Entwurfsfall. Es ist aber auch kein
|
||||
Grund, das Feature ganz zu streichen — der Nutzen (Playlisten überleben ein
|
||||
zurückgesetztes Handy) ist real und billig zu haben, **wenn man auf den
|
||||
beidseitigen Merge verzichtet.**
|
||||
|
||||
**URTEIL: Playlist-Sync Stufe 1 = einseitige Sicherung.**
|
||||
Die App pusht lokale Playlisten-Änderungen online an den Server und merkt sich
|
||||
die vom Server vergebene cloudId. **Kein Rück-Merge.** Server-Playlisten werden
|
||||
nur dann lokal angelegt, wenn die lokale Playlisten-Tabelle **leer** ist
|
||||
(Neuinstallation / Wiederherstellung). Damit entfallen ersatzlos: Namenszuordnung,
|
||||
Reihenfolge-Schiedsrichter, Gleichstands-Regel, Tombstone-Semantik und die
|
||||
Abhängigkeit vom fehlenden Rename-Endpunkt — die Identität stammt **immer** aus
|
||||
dem eigenen `POST /playlists` und ist damit per Konstruktion stabil
|
||||
(`user_playlists.id` ist `INTEGER PRIMARY KEY AUTOINCREMENT`).
|
||||
Vorbedingung bleibt: `melo_cloud.py:505` (Owner-Prüfung) muss serverseitig
|
||||
geflickt sein, bevor die App diesen Endpunkt regelmäßig benutzt.
|
||||
Der beidseitige Playlist-Merge ist eine eigene, spätere Spec.
|
||||
(Wer noch weiter reduzieren will: ganz herausschneiden ist ebenfalls vertretbar
|
||||
und kostet heute nichts.)
|
||||
|
||||
---
|
||||
|
||||
## 2. Prüfung der Konsens-Punkte (Konsens kann geschlossen falsch sein)
|
||||
|
||||
| Konsens-Punkt | Urteil |
|
||||
|---|---|
|
||||
| „Union muss durch einen Basis-Snapshot ersetzt werden" (Runde 2, alle fünf) | **FALSCH in der Therapie.** Diagnose hält, Empfehlung nicht — siehe 0.3 und 1.1. Der Konsens entstand, weil die Debatte nur zwei Kandidaten kannte (Outbox vs. Basis-Snapshot) und beide *in Kombination mit dem Full-Replace-POST* dachte. |
|
||||
| „DAs Outbox löst nur ein Drittel" (Runde 1, mit Gegenbeispiel akzeptiert) | **Zu früh geschlossen.** Das Gegenbeispiel lautet wörtlich „Offline-Gerät B resurrects **via Union**" — es setzt voraus, dass die Union bleibt. Es widerlegt die Outbox nur in dieser Paarung. Für mein Urteil ist das nicht tragend (ich brauche keine Outbox), aber es zeigt den Denkfehler des Konsenses. |
|
||||
| „Die GET-vor-POST-Regel ist die beste Stelle der Spec" (Phase 3, alle fünf) | **FALSCH und vom Panel selbst korrigiert.** Sie ist eine Regel über ein nicht beobachtbares Prädikat. Richtig ist Flutters Umkehrung in Runde 2. Meine Konsequenz geht weiter: eine Regel, deren Verletzung Datenverlust bedeutet, gehört durch eine Konstruktion ersetzt, die den Verlust unmöglich macht (1.1). |
|
||||
| „Die Testliste trägt" (7× erwähnt, stets lobend) | **FALSCH.** Der Auditor hat recht: sie nennt ausschließlich neue Tests. Zwei Bestandstests stehen der Spec aktiv im Weg (`melo_cloud_service_test.dart:98-101`, `sync_service_test.dart:166-202`), und der Sofort-Push — der einzige Online-Löschkanal der Spec — hat keinen einzigen Eintrag. |
|
||||
| „Der Löschpfad ist Bestand, P1 für die Spec, P0 im Backlog" | **RICHTIG.** Die einzige Panel-Einstufung, die den Maßstab exakt trifft. Bestätigt. |
|
||||
| „Die Architektur wird von keinem Befund angegriffen" | **RICHTIG.** `sync_merge.dart` als reine Funktionsdatei ohne I/O ist der richtige Ort — auch für den additiven Abgleich. Der Dateischnitt bleibt (abzüglich `echtzeit_sync.dart`). |
|
||||
| „Die Nicht-Ziele sind diszipliniert und begründet" | **RICHTIG — mit einer Ausnahme, die das Panel benannt hat:** SSE bricht die eigene YAGNI-Regel. Nach 1.2 ist die Ausnahme beseitigt und der Konsens wird nachträglich vollständig richtig. |
|
||||
| „`flutter_local_notifications` ist der einzige harte Blocker" | **RICHTIG**, mit Feasibilitys Korrektur: `multiDexEnabled` ist bei `minSdk 24` (`FlutterExtension.kt:26`) gegenstandslos und fällt aus der Forderung. |
|
||||
| „Die Phasenreihenfolge stimmt" | **RICHTIG**, gegen `sync_service.dart:207-215` verifiziert. Aber Z. 225 („Zeitstempel am Ende") widerspricht Z. 250 („Snapshot VOR dem Listen") — beides ist erfüllbar, die Spec sagt es nur nicht. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Eigener Lücken-Scan — was auch der Auditor nicht gesehen hat
|
||||
|
||||
**L1 [P1] [EINZELQUELLE: Richter] [PLAN-RISIKO] — Der Full-Replace-POST, nicht
|
||||
die Union, ist die Wurzel.**
|
||||
Fünf Reviewer und der Auditor haben zwei Runden über die *Mengenformel* debattiert
|
||||
und dabei den *Schreibweg* als gegeben behandelt. Kein Beitrag prüft, was
|
||||
passiert, wenn man `POST /favorites` einfach nicht mehr benutzt. Ergebnis:
|
||||
Ziel 1 wird dann strukturell erreicht, die Abhängigkeit der Datensicherheit von
|
||||
`parseFavoriten` entfällt, und es entsteht kein neuer Zustand. Risk hat den
|
||||
Delta-Push als Bestandteil (c) seines Basis-Snapshot-Pakets vorgeschlagen — dass
|
||||
Bestandteil (c) **allein** genügt, hat niemand geprüft. Ausführlich in 1.1.
|
||||
|
||||
**L2 [P2] [EINZELQUELLE: Richter] [BESTANDSFEHLER] — Es gibt zwei parallele
|
||||
Favoriten-Systeme in derselben Oberfläche, und die Spec kennt nur eines.**
|
||||
`lib/shared/server_favorite_button.dart:42` setzt Favoriten über
|
||||
`navidrome.setFavorite(navidromeId)` — also im **Navidrome**, nicht in der
|
||||
lokalen `Favorites`-Tabelle und nicht in der Melo-Cloud.
|
||||
`lib/player/now_playing_screen.dart:275-279` zeigt für lokale Songs
|
||||
`FavoriteButton` (lokal) und für Server-Titel `ServerFavoriteButton`
|
||||
(Navidrome) — **dasselbe Herz-Symbol, zwei getrennte Systeme.**
|
||||
Gegenprobe: `ServerFavoriteButton` / `server_favorite_button` kommt in **keiner**
|
||||
der 25 State-Dateien und nicht in der Spec vor (`grep -rln` → kein Treffer).
|
||||
Der dritte Kanal, `PlaylistService.syncFavoritesFromServer()`
|
||||
(`lib/library/playlist_service.dart:52-71`), wurde von Risk (Phase 3, `:303-308`)
|
||||
und DA (Phase 3, `:297-299`) als toter Code erkannt (`db.songExists(navidromeId)`
|
||||
gegen lokale UUIDs) — fiel aber aus allen fünf Schlussurteilen heraus.
|
||||
Folge: Ein Nutzer, der im Now-Playing-Screen ein Herz antippt, kann je nach
|
||||
Titelquelle in zwei völlig verschiedenen Systemen landen; die Spec verspricht
|
||||
Favoriten-Sync, deckt aber nur eines ab. Das ist eine **Geltungsbereichs-Frage,
|
||||
die in die Spec gehört**, nicht in den Kopf des Umsetzenden.
|
||||
|
||||
**L3 [P2] [EINZELQUELLE: Richter] [PLAN-RISIKO] — Die Pull-Richtung hat keine
|
||||
Regel für unauflösbare Server-Favoriten.**
|
||||
Die Spec sagt „fehlende Favoriten lokal setzen" (Z. 99) und schweigt dazu, was
|
||||
bei einer cloudId passiert, die sich lokal auf keinen Song abbilden lässt
|
||||
(Download fehlgeschlagen, Song noch nicht geladen). `Favorites.songId` verweist
|
||||
auf `Songs.id` (`database.dart:102-107`); ein Favorit ohne Song wäre über
|
||||
`watchFavorites()` (innerJoin, `:371-375`) unsichtbar, würde aber über
|
||||
`favoriteSongIds()` (`:534-536`) ewig mitgeschleppt und wieder hochgepusht.
|
||||
Nebenbefund am selben Ort: `favoriteSongIds()` liefert auch Favoriten
|
||||
**getombsteter** Songs, weil `allSongs()` (`:216`) Grabsteine einschließt — ein
|
||||
lokal gelöschter Titel wird also weiter als Favorit gemeldet, während derselbe
|
||||
Lauf ihn am Server löscht. Unter dem additiven Abgleich harmlos, unter jedem
|
||||
Voll-Ersatz nicht.
|
||||
|
||||
**L4 [P2] [EINZELQUELLE: Richter] — Der additive Delta-Push feuert N SSE-Events.**
|
||||
`handle_favorites_toggle` emittiert bei **jedem** Aufruf `song_favorite`
|
||||
(`melo_cloud.py:687`). Beim ersten Lauf nach der Umstellung sind das so viele
|
||||
Events wie lokale Favoriten. Ohne SSE in der App (Urteil 1.2) folgenlos — aber es
|
||||
gehört als Vorbedingung notiert, falls SSE später kommt: das Selbst-Echo-Fenster
|
||||
muss dann existieren, **bevor** der Delta-Push zum Regelpfad wird.
|
||||
|
||||
**L5 [P3] [EINZELQUELLE: Richter] — `handle_favorites_get` liefert
|
||||
`favorited_at`, der Client wirft es weg.**
|
||||
`melo_cloud.py:620` gibt je Favorit `favorited_at` zurück; `parseFavoriten` liest
|
||||
nur `['id']`. Lokal existiert das Gegenstück als `Favorites.createdAtMs`
|
||||
(`database.dart:103`). Kein Handlungsbedarf für diese Spec (ohne Tombstones auf
|
||||
beiden Seiten trägt ein Zeitstempel keine Lösch-Entscheidung), aber es ist die
|
||||
Zutat, die ein späterer echter Merge bräuchte — und sie ist bereits da. Ein Satz
|
||||
im Backlog spart der Zukunft eine Server-Änderung.
|
||||
|
||||
---
|
||||
|
||||
## 4. Epistemik- und Typ-Labels (Sammelübersicht)
|
||||
|
||||
| # | Befund | Epistemik | Typ | Severity |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `parseFavoriten` verwandelt Fehlerantworten in `[]`; `favoriten()` hat null Aufrufer | [VERIFIZIERT] | [PLAN-RISIKO] | P1 |
|
||||
| 2 | Union kann „entfernt" nicht ausdrücken; Spec-Zusage Z. 108-112 ist falsch | [VERIFIZIERT] | [PLAN-RISIKO] | P1 |
|
||||
| 3 | Basis-Snapshot schafft neuen Löschpfad (Panel-Empfehlung) | [VERIFIZIERT] | [PLAN-RISIKO, neu geschaffen] | P1 — **abgelehnt** |
|
||||
| 4 | Full-Replace-`POST /favorites` ist die eigentliche Wurzel | [EINZELQUELLE: Richter] | [PLAN-RISIKO] | P1 |
|
||||
| 5 | Irreversibler Löschpfad bis in die Navidrome-Bibliothek | [VERIFIZIERT] [KONSENS] | [BESTANDSFEHLER] | P1 Spec / **P0 Backlog** |
|
||||
| 6 | Playlist-Identität instabil (4 offene Entscheidungen) | [VERIFIZIERT] [KONSENS] | [PLAN-RISIKO] | P1 |
|
||||
| 7 | `melo_cloud.py:505` löscht fremde `user_playlist_songs` ohne Owner-Prüfung | [VERIFIZIERT] | [BESTANDSFEHLER] | P1 Server-Backlog, **Vorbedingung** |
|
||||
| 8 | Kein Playlist-Rename-Endpunkt (Spec behauptet das Gegenteil) | [VERIFIZIERT] | [PLAN-RISIKO] | P1 |
|
||||
| 9 | Fünf „bestehende Schutzmechanismen", die es nicht gibt | [VERIFIZIERT] | [BESTANDSFEHLER] + [PLAN-RISIKO] | P1 |
|
||||
| 10 | Gradle-Desugaring für `flutter_local_notifications` | [VERIFIZIERT] [KONSENS] | [PLAN-RISIKO] | P1 (harter Blocker) |
|
||||
| 11 | SSE-Hauptnutzen hängt am ausgelagerten `song_upload`-Event | [VERIFIZIERT] | [PLAN-RISIKO] | P1 |
|
||||
| 12 | Bestandstest `melo_cloud_service_test.dart:98-101` zementiert den Bug | [VERIFIZIERT] (Verifikation) | [BESTANDSFEHLER im Test] | P1 |
|
||||
| 13 | Bestandstest `sync_service_test.dart:166-202` + Catch-All-Mock `:196` | [VERIFIZIERT] (Auditor) | [BESTANDSFEHLER im Test] | P1 |
|
||||
| 14 | Sofort-Push ohne einen einzigen Testeintrag | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P1 |
|
||||
| 15 | Zwei parallele Favoriten-Systeme (`ServerFavoriteButton`) | [EINZELQUELLE: Richter] | [BESTANDSFEHLER] | P2 |
|
||||
| 16 | Pull-Richtung ohne Regel für unauflösbare cloudIds | [EINZELQUELLE: Richter] | [PLAN-RISIKO] | P2 |
|
||||
| 17 | Auswahl-Upload nicht abbrechbar | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 |
|
||||
| 18 | 30er-Rückfrage strukturell wirkungslos; kein Platz-Check | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 |
|
||||
| 19 | Doppelspeicherung Download × Abspiel-Cache | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 |
|
||||
| 20 | Zeitstempel × Auswahl-Upload × 24-h-Bericht offen | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 |
|
||||
| 21 | Bericht: `letzterLauf == null` und „erfolgreich" undefiniert | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 |
|
||||
| 22 | Offline-Modus von keinem neuen Netz-Verbraucher beachtet | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P2 |
|
||||
| 23 | Navidrome-Playlist-Import nicht idempotent („🌐"-Duplikate) | [VERIFIZIERT] | [BESTANDSFEHLER] | P2 |
|
||||
| 24 | `_LadeKnopf` ist privat und in einer anderen Datei | [VERIFIZIERT] | [PLAN-RISIKO] | P2 |
|
||||
| 25 | Schema-Divergenz `ist_korrupt` / `uq_user_song` | [VERIFIZIERT] | [BESTANDSFEHLER] | P2 Backlog |
|
||||
| 26 | `server_titel_screen._geladen`-Einmal-Snapshot | [VERIFIZIERT] | [BESTANDSFEHLER] | P2 |
|
||||
| 27 | Widerspruch Z. 225 ↔ Z. 250 (Zeitstempel) | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P3 |
|
||||
| 28 | „2-GB-Abspiel-Cache" ist ein Standardwert (0–8192 MB) | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P3 |
|
||||
| 29 | Kein Migrations-Downgrade-Pfad (Schema 10 → 11) | [VERIFIZIERT] (Auditor) | [PLAN-RISIKO] | P3 |
|
||||
| 30 | `favorited_at` / `createdAtMs` ungenutzt | [EINZELQUELLE: Richter] | [PLAN-RISIKO] | P3 |
|
||||
| — | „Auswahl-Modus existiert nicht" (DA) | [BESTRITTEN → widerlegt] | — | entfällt |
|
||||
| — | „Pro-Song-Loop ohne Fortschritt/Schutz" (Feasibility) | [BESTRITTEN → widerlegt] | — | entfällt |
|
||||
| — | Worker-Pool-Erschöpfung (Risk K1) | [BESTRITTEN → zurückgezogen] | — | entfällt |
|
||||
|
||||
---
|
||||
|
||||
## 5. Finaler Score und Verdikt
|
||||
|
||||
# Score: **6 / 10**
|
||||
|
||||
Über dem Panel-Mittel (5,2), weil die Verifikation zwei tragende Säulen der
|
||||
Panel-Kritik entfernt hat: es gibt **kein P0 im Geltungsbereich**, und die
|
||||
zentrale Ersatz-Empfehlung des Panels ist selbst gefährlicher als das, was sie
|
||||
ersetzen soll (0.3). Die beiden „ÜBERARBEITEN"-Stimmen begründen sich fast
|
||||
vollständig aus einer Therapie, die ich ablehne.
|
||||
|
||||
Nicht höher als 6, weil das Dokument drei belegte Substanzmängel hat:
|
||||
eine **falsche Garantie** (Z. 108-112 — Ent-Favorisierungen propagieren nicht),
|
||||
**fünf zu optimistische Bestandsbehauptungen** (Rename-Endpunkt, `_LadeKnopf`,
|
||||
„verzögert", `erzwinge`, „vollständig" in Z. 17) und einen **Umfang, den es
|
||||
nicht rechtfertigen kann** (SSE ohne seine eigene Vorbedingung, Playlist-Sync mit
|
||||
vier offenen Semantik-Entscheidungen, eine Dependency, die den einzigen harten
|
||||
Build-Blocker der ganzen Planung erzwingt).
|
||||
|
||||
# Verdikt: **SPEC NACHSCHÄRFEN DANN FREIGEBEN**
|
||||
|
||||
Begründung gegen „ÜBERARBEITEN": Jede von mir geforderte Änderung ist entweder
|
||||
(a) die Korrektur eines faktisch falschen Satzes, (b) eine **Reduktion** des
|
||||
Umfangs — Abschnitte streichen oder verschieben ist keine Neuschrift, oder
|
||||
(c) **ein** Mechanismus-Tausch, der das Dokument *kleiner* macht (additiver
|
||||
Delta-Push statt Voll-Ersatz = weniger bewegliche Teile als heute). Rahmen,
|
||||
Zerlegung, Nicht-Ziele, Architektur (`sync_merge.dart` als reine Funktionsdatei),
|
||||
Phasenreihenfolge und der Testansatz bleiben unangetastet und wurden von keinem
|
||||
Reviewer angegriffen. Es gibt nichts wegzuwerfen.
|
||||
|
||||
Begründung gegen „FREIGEBEN": Die falsche Lösch-Garantie und die fünf
|
||||
Bestandsbehauptungen würden den Umsetzenden nachweislich in die Irre führen; zwei
|
||||
grüne Bestandstests stehen der Umsetzung aktiv im Weg; und eine Frage kann kein
|
||||
Reviewer entscheiden (siehe A6).
|
||||
|
||||
**Bedingung:** Die nachgeschärfte Fassung wird **einmal kurz gegengelesen**,
|
||||
bevor der Implementierungsplan entsteht — die Punkte A1, A5 und A7 ändern, *was*
|
||||
gebaut wird, nicht nur *wie* es beschrieben ist.
|
||||
|
||||
---
|
||||
|
||||
## 6. AKTIONSLISTE — konkrete Änderungen an der Spec
|
||||
|
||||
Priorisiert. Jede Zeile ist so formuliert, dass sie direkt ins Dokument kann.
|
||||
|
||||
---
|
||||
|
||||
### A1 · P1 · [EINZELQUELLE: Richter] [PLAN-RISIKO] · §Feature 1, Punkt „Vereinigung beim Sync" (Z. 95-101) + §Risiken (Z. 252-254)
|
||||
**Full-Replace-POST ersatzlos streichen. Der Abgleich schreibt nur noch additiv.**
|
||||
|
||||
> Die Sync-Phase benutzt **nie** `POST /favorites` (Voll-Ersatz). Sie schreibt
|
||||
> ausschließlich additiv:
|
||||
> 1. `GET /favorites` → `server` (Menge von cloudIds).
|
||||
> 2. Für jede cloudId in `lokal \ server`: `POST /favorites/toggle` mit
|
||||
> `{"song_id": …, "set": true}` (existiert und ist deterministisch:
|
||||
> `melo_cloud.py:1298-1302` → `handle_favorites_toggle:644-688`; die
|
||||
> Client-Methode ist neu).
|
||||
> 3. Für jede cloudId in `server \ lokal`: lokal Favorit setzen, sofern der Song
|
||||
> lokal auflösbar ist.
|
||||
> 4. **In keiner Richtung wird etwas entfernt.**
|
||||
>
|
||||
> `MeloCloudService.setzeFavoriten` (`melo_cloud_service.dart:268-280`) wird nicht
|
||||
> mehr benutzt und entfällt.
|
||||
>
|
||||
> **Wirkung:** Der Datenverlust-Bug aus Ziel 1 ist danach **strukturell**
|
||||
> unmöglich, nicht nur durch eine Regel verhindert — es existiert kein Codepfad
|
||||
> mehr, der den Server-Stand ersetzen kann. Deckel: höchstens 200 Pushes je Lauf,
|
||||
> der Rest im nächsten Lauf (die Pushes sind idempotent, ein Teilausfall heilt
|
||||
> sich beim nächsten Lauf selbst).
|
||||
|
||||
Streichen in §Risiken: „`POST /favorites` bleibt technisch ein Voll-Ersatz — die
|
||||
Sicherheit liegt allein in der GET-vor-POST-Regel." Ersetzen durch: „Die
|
||||
Sicherheit liegt nicht mehr in einer Regel, sondern darin, dass kein
|
||||
Voll-Ersatz-Aufruf mehr existiert."
|
||||
|
||||
---
|
||||
|
||||
### A2 · P1 · [VERIFIZIERT] [PLAN-RISIKO] · §Feature 1 „Sicherheitsregel" (Z. 102-105) + §Tests
|
||||
**`parseFavoriten` härten — und den grünen Bestandstest mitändern.**
|
||||
|
||||
> Ein GET gilt nur als erfolgreich, wenn HTTP 200 **und** kein `error`-Schlüssel
|
||||
> **und** `favorites` als Liste vorhanden ist. Ein fehlender `favorites`-Schlüssel
|
||||
> ist ein **Fehler, keine leere Menge**; `parseFavoriten` wirft dann
|
||||
> `CloudException`, genau wie `parseListe` (`melo_cloud_service.dart:99-100`) und
|
||||
> `parseUpload` (`:111-112`) es bereits tun. Hintergrund: der Router verdrahtet
|
||||
> für `GET /favorites` hart 200 (`melo_cloud.py:1292-1293`), und sechs Handler
|
||||
> desselben Servers geben Fehler im 200er-Körper zurück (`:418, 491, 545, 766,
|
||||
> 1021, 1118`).
|
||||
>
|
||||
> **Bestandsänderung, Teil des Arbeitspakets:**
|
||||
> `test/services/melo_cloud_service_test.dart:98-101` prüft heute ausdrücklich
|
||||
> das Gegenteil (`expect(parseFavoriten(jsonEncode({'status':'ok'})), isEmpty)`)
|
||||
> und wird mit umgeschrieben. Das ist kein Regressionsfehler.
|
||||
>
|
||||
> Zwei Pflichttests: „200 mit Fehlerkörper → Phase übersprungen, kein Push" und
|
||||
> „200 mit `favorites: []` → Phase läuft normal durch" (der zweite verhindert,
|
||||
> dass der erste durch ein zu scharfes „wirf bei leer" trivial erfüllt wird).
|
||||
|
||||
---
|
||||
|
||||
### A3 · P1 · [VERIFIZIERT] [PLAN-RISIKO] · §Feature 1, letzter Punkt (Z. 108-112)
|
||||
**Die falsche Lösch-Garantie streichen und durch die richtige Einschränkung ersetzen.**
|
||||
|
||||
Zu streichen: „Ent-Favorisierungen propagieren über den Sofort-Push (online) …
|
||||
Offline entfernte Herzen kommen beim nächsten Sync zurück". Das ist falsch — es
|
||||
betrifft auch **online** entfernte Herzen.
|
||||
|
||||
> **Bekannte Einschränkung (bewusst, dokumentiert):** Der Abgleich ist rein
|
||||
> additiv. Ein Ent-Favorisieren wirkt sofort lokal und — online — auch auf dem
|
||||
> Server, ist aber **nicht geräteübergreifend garantiert**: Hält ein zweites
|
||||
> Gerät den Favoriten noch, bringt dessen nächster Sync ihn auf allen Geräten
|
||||
> zurück. Das gilt für offline **und für online** entfernte Herzen. Bewusster
|
||||
> Tausch, wie in v2: kein Datenverlust ist wichtiger als verlässliche
|
||||
> Lösch-Propagation.
|
||||
> Falls das in der Praxis stört: Folgeschritt „Basis-Snapshot" (Backlog). Er
|
||||
> lässt sich später **ohne Umbau** ergänzen, weil der Schreibweg dann schon der
|
||||
> Delta-Push ist — aus `set:true` wird zusätzlich `set:false`.
|
||||
|
||||
---
|
||||
|
||||
### A4 · P1 · [VERIFIZIERT] [PLAN-RISIKO] · §Risiken, neuer Unterabschnitt „Verworfene Alternativen"
|
||||
**Den Basis-Snapshot ausdrücklich ablehnen und begründen (Gegen-Empfehlung zum Panel).**
|
||||
|
||||
> **Verworfen: Drei-Wege-Merge gegen einen persistierten Basis-Snapshot**
|
||||
> (`neu = (lokal ∪ server) − (Basis \ lokal) − (Basis \ server)`). Er löst zwar
|
||||
> die Lösch-Propagation, führt aber den **einzigen neuen Datenverlustpfad** der
|
||||
> ganzen Planung ein: Liefert `GET /favorites` fälschlich eine leere Liste, wird
|
||||
> `Basis \ server = Basis`, die Formel kollabiert zu `lokal − Basis` und löscht
|
||||
> den gesamten bestätigten Bestand — lokal **und** über den Delta-Push auf dem
|
||||
> Server und damit auf allen Geräten. Zusätzlich braucht er kontogebundenen
|
||||
> Zustand mit Aufräumpflicht beim Abmelden; `BakaAuth.abmelden`
|
||||
> (`baka_auth.dart:114-120`) räumt heute nichts ab und es gibt kein
|
||||
> Token-Refresh. Für drei Nutzer, 0 Favoriten und 0 Playlisten auf dem Server ist
|
||||
> der additive Abgleich der bessere Tausch. Der Basis-Snapshot bleibt als
|
||||
> Folgeschritt im Backlog.
|
||||
|
||||
---
|
||||
|
||||
### A5 · P1 · [VERIFIZIERT] [KONSENS] [PLAN-RISIKO] · §Ziel 4, §Feature 2 (Z. 114-140), §Nicht-Ziele, §Tests, §Offene Abhängigkeiten
|
||||
**Playlist-Sync auf einseitige Sicherung reduzieren. Der beidseitige Merge wird eigene Spec.**
|
||||
|
||||
> **Playlist-Sync Stufe 1 = einseitige Sicherung.** Die App meldet lokale
|
||||
> Playlisten-Änderungen (anlegen / Song hinzufügen / Song entfernen /
|
||||
> Reihenfolge) online fire-and-forget an den Server und persistiert die vom
|
||||
> Server vergebene cloudId (Drift-Migration `Playlists.cloudId TEXT NULL` bleibt).
|
||||
> Es findet **kein Rück-Merge** statt: Server-Playlisten werden nur dann lokal
|
||||
> angelegt, wenn die lokale Playlisten-Tabelle **leer** ist (Neuinstallation /
|
||||
> Wiederherstellung).
|
||||
>
|
||||
> Damit entfallen ersatzlos: Namenszuordnung, Reihenfolge-Schiedsrichter
|
||||
> („längerer Stand gewinnt"), Gleichstands-Regel, Tombstone-Semantik und die
|
||||
> Abhängigkeit vom fehlenden Rename-Endpunkt. Die Identität stammt **immer** aus
|
||||
> dem eigenen `POST /playlists` und ist stabil (`user_playlists.id` ist
|
||||
> `INTEGER PRIMARY KEY AUTOINCREMENT`).
|
||||
>
|
||||
> **Nicht abgedeckt (dokumentiert):** Umbenennungen propagieren nicht.
|
||||
> Playlisten-Änderungen auf einem zweiten Gerät erscheinen auf dem ersten nicht.
|
||||
> Der beidseitige Playlist-Merge ist eine eigene, spätere Spec.
|
||||
>
|
||||
> **Vorbedingung (Server-Auftrag, blockierend):** `melo_cloud.py:505` löscht
|
||||
> `user_playlist_songs` für jede `playlist_id` **ohne `user`-Bedingung** — echte
|
||||
> Fremddaten-Löschung. Muss geflickt sein, bevor die App diesen Endpunkt
|
||||
> regelmäßig benutzt.
|
||||
|
||||
*Ebenfalls vertretbar und heute kostenlos (0 Playlisten auf dem Server): Feature
|
||||
2 ganz herausschneiden. Entscheidung liegt bei Dustin; das Weiterlaufen im
|
||||
jetzigen Zustand (vier offene Semantik-Entscheidungen) ist es nicht.*
|
||||
|
||||
---
|
||||
|
||||
### A6 · P1 für die Spec / **P0 im Backlog** · [VERIFIZIERT] [KONSENS] [BESTANDSFEHLER] · §Kontext (Z. 15-17) + §Risiken
|
||||
**Den irreversiblen Löschpfad benennen — und die eine Frage stellen, die kein Reviewer entscheiden kann.**
|
||||
|
||||
§Kontext, Korrektur des Wortes „vollständig":
|
||||
> „… der Weg Handy→Server→Navidrome-Bibliothek existiert also bereits — **und er
|
||||
> läuft auch rückwärts**: verschwindet die lokale Datei, meldet der Sync die
|
||||
> Löschung, und der Server entfernt die Audiodatei aus Registry **und**
|
||||
> Navidrome-Bibliothek."
|
||||
|
||||
§Risiken, neuer Absatz:
|
||||
> **Irreversibler Löschpfad (Bestand, nicht von dieser Spec verursacht —
|
||||
> Backlog-P0).** `markMissing` (`database.dart:237-245`) tombstoned jeden Titel,
|
||||
> dessen Datei der Scan nicht findet — auch unbeabsichtigt (SD-Karte nicht
|
||||
> eingehängt, Berechtigung entzogen, Dateimanager). `planeSync`
|
||||
> (`sync_service.dart:68-76`) macht daraus eine Server-Löschung; die Lösch-Bremse
|
||||
> (`:112-113`) greift bei 325 Titeln erst ab 109 — **1 bis 108 Löschungen laufen
|
||||
> ungebremst**. Der Server löscht `registry/<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 0–8192 MB" (`app_settings.dart:28`, `:31`).
|
||||
> - **§Nicht-Ziele:** „Ein Rollback auf eine ältere App-Version ist nach der
|
||||
> Schema-Migration nicht vorgesehen — `database.dart:154-192` kennt nur
|
||||
> `onCreate` und `onUpgrade`."
|
||||
> - **§Offene Abhängigkeiten:** Schema-Divergenz `ist_korrupt` / `uq_user_song`
|
||||
> (nur in der Live-DB, nicht im `CREATE TABLE` des Skripts) als Backlog-Punkt —
|
||||
> trifft nur wiederhergestellte oder Test-Instanzen.
|
||||
> - **Backlog-Notiz:** `handle_favorites_get` liefert bereits `favorited_at`
|
||||
> (`melo_cloud.py:620`), lokal existiert `Favorites.createdAtMs` — die Zutat
|
||||
> für einen späteren echten Merge ist beidseitig vorhanden.
|
||||
|
||||
---
|
||||
|
||||
### A17 · P1 · [KONSENS] · §Neuer Abschnitt „Reihenfolge" (vor §Tests)
|
||||
**Auslieferungsreihenfolge festschreiben — sie fehlt heute ganz.**
|
||||
|
||||
> 1. **Favoriten-Fix allein** (additiver Delta-Push, `parseFavoriten`,
|
||||
> Testanpassung). Wartet auf **nichts**: kein Server-Auftrag, keine neue
|
||||
> Dependency, keine Gradle-Änderung, kein neues UI-Muster. Einzige Stufe mit
|
||||
> belegtem Datenverlust-Bezug.
|
||||
> 2. Auswahl-Upload + Einzel-Song-Offline (UI, keine neue Dependency).
|
||||
> 3. Sync-Bericht (Stufe A, kein Plugin).
|
||||
> 4. Playlist-Sicherung Stufe 1 — **nach** dem Server-Fix `melo_cloud.py:505`.
|
||||
> 5. Notification Stufe B — beginnt mit dem grünen Beweis-Build.
|
||||
> 6. SSE — eigene Spec, nach dem `song_upload`-Event.
|
||||
|
||||
---
|
||||
|
||||
### §Offene Abhängigkeiten — geschlossene Liste (ersetzt Z. 274-281)
|
||||
|
||||
| # | Auftrag | Art | Blockiert |
|
||||
|---|---|---|---|
|
||||
| 1 | `melo_cloud.py:505` — Owner-Prüfung in `handle_playlist_delete` | **Voraussetzung** | A5 / Feature 2 |
|
||||
| 2 | Datei-Wiederherstellung im Dedup-Zweig von `upload()` | **Voraussetzung, falls Antwort auf A6 „nein"** | Feature 3 |
|
||||
| 3 | `song_upload`-SSE-Event | Voraussetzung für die spätere SSE-Stufe | nichts in dieser Spec |
|
||||
| 4 | Playlist-Rename-Endpunkt (`PUT`/`PATCH`) | optional | nichts (A5 braucht ihn nicht mehr) |
|
||||
| 5 | Playlist-Tombstones serverseitig | optional / Backlog | nichts |
|
||||
|
||||
---
|
||||
|
||||
## 7. Meta-Beobachtung
|
||||
|
||||
**Zur Spec-Qualität.** Das Dokument ist auffallend ehrlich in seinen Nicht-Zielen
|
||||
und faktisch überwiegend korrekt — rund zwanzig Bestandsbehauptungen haben fünf
|
||||
unabhängigen Prüfungen standgehalten, und zwei angebliche Fehler des Panels waren
|
||||
in Wahrheit Fehler des Panels. Seine zwei echten Schwächen sind typisch für
|
||||
Specs, die von jemandem geschrieben werden, der den Code gut kennt: Es
|
||||
**verwechselt „ich weiß, wie das geht" mit „das ist schon da"** (fünf zu
|
||||
optimistische Bestandssätze), und es **formuliert Sicherheit als Regel statt als
|
||||
Konstruktion** („nie ohne erfolgreiches GET POSTen") — Regeln haben Lücken,
|
||||
Konstruktionen nicht. Der größte Einzelmangel ist aber der Umfang: sechs Ziele,
|
||||
von denen genau eines auf nichts wartet und einen belegten Bug behebt, während
|
||||
die anderen fünf zusammen vier Server-Aufträge, eine Build-Umbauaktion und acht
|
||||
Fehlerklassen mitbringen. Das ist kein Denkfehler, das ist fehlende
|
||||
Reihenfolge — und sie ist mit einem Abschnitt behoben.
|
||||
|
||||
**Zum Panel-Prozess.** Der Prozess war stark, wo er üblicherweise schwach ist,
|
||||
und schwach an einer Stelle, die der Prozess nicht vorsieht.
|
||||
*Stark:* Die Beleglage ist außergewöhnlich — ~70 geprüfte Datei:Zeile-Belege,
|
||||
**keine einzige Halluzination**, keine Fehlzuordnung. Mehrere Reviewer haben
|
||||
eigene P0 aktiv falsifiziert (DA hat seine eigene Rettungshypothese widerlegt,
|
||||
Risk hat sein eigenes P0 konzediert, Feasibility hat drei eigene Befunde fallen
|
||||
lassen). Das ist das Gegenteil von Konsens-Drift.
|
||||
*Schwach:* **Der Prozess prüft die Spec adversarial, aber die eigene Empfehlung
|
||||
nur konsensual.** Sobald Runde 2 auf den Basis-Snapshot konvergiert war, hat
|
||||
niemand ihn mehr mit derselben Härte angegriffen wie zuvor die Union — obwohl DA
|
||||
und Risk den entscheidenden Einwand (0.3) je einmal notiert hatten. Beide
|
||||
Phase-6-Zusammenfassungen führen ihn nicht als Finding. Ein Panel, das eine
|
||||
Therapie empfiehlt, braucht eine Runde, in der die *Therapie* der
|
||||
Review-Gegenstand ist. Erst die separate Verifikation hat das aufgedeckt — sie
|
||||
ist der wertvollste Bestandteil dieses Verfahrens gewesen.
|
||||
*Zweiter blinder Fleck:* Der Suchraum war zu klein. Zwei Runden über „Outbox oder
|
||||
Basis-Snapshot" haben den Schreibweg (`POST /favorites` als Voll-Ersatz) als
|
||||
gegeben behandelt. Die billigste Lösung lag außerhalb der Debattenachse — sie
|
||||
bestand darin, einen Aufruf zu **streichen**, nicht einen Mechanismus zu bauen.
|
||||
Das ist die wiederkehrende Blindstelle von Review-Panels: Sie sind darauf
|
||||
trainiert, Lücken zu finden, und finden deshalb Dinge, die man **hinzufügen**
|
||||
muss.
|
||||
*Dritter Punkt:* Die Testschuld wurde erst vom Vollständigkeits-Auditor entdeckt.
|
||||
Fünf Reviewer haben `sync_service.dart` zeilengenau seziert und `test/` nur
|
||||
einmal betreten. Für ein Projekt mit TDD-Pflichtworkflow ist das die
|
||||
bemerkenswerteste Lücke des ganzen Durchlaufs — und die mit den unmittelbarsten
|
||||
Folgen, weil der schnellste Weg zu Grün darin bestünde, die neue Sicherheitsregel
|
||||
im Mock wieder aufzuweichen.
|
||||
|
||||
---
|
||||
|
||||
*Alle eigenen Prüfungen read-only (grep/sed/read auf `lib/`, `test/`,
|
||||
`android/`, `melo_cloud.py`). Keine git-Operationen, keine `.env`, keine
|
||||
Server-Prozesse. Geschrieben wurde ausschließlich diese Datei.*
|
||||
Reference in New Issue
Block a user