real-debrid-downloader/tasks/lessons.md
Sucukdeluxe 288a0762a6 Renaming 100%: collect leitet sauberen Namen selbst ab (gemeinsame Entscheidungsfunktion + Wurzel-Schutz)
User-Report (aus dem Desktop-Rename-Log): 17 Dateien landeten ROH in der Library
("tvarchiv...s07e12-720.mkv", "4sf-...s04e01.mkv") — Auto-Rename hatte sie verpasst, der
MKV-Collect schob sie mit dem rohen Scene-Namen weg.

Root Cause 1: Auto-Rename und collectMkvFilesToLibrary sind entkoppelte Scans. Auto-Rename
benennt nur present-and-stable Dateien in extractDir um; eine verpasste Datei (verpasster
Zyklus ODER lag in "Downloader Unfertig" ausserhalb extractDir) wurde von collect roh
weggeschoben (collect behielt blind den Basename).
Root Cause 2: decideAutoRenameBaseName fabrizierte Namen fuer token-lose generische Ordner
("Mega-Direct-Pack" -> "Mega-Direct-Pack.S01E01") wegen eines hasSceneGroupSuffix-Falsch-
Positivs auf "-Pack" — derselbe latente Bug haette Auto-Rename getroffen.

Fix:
- Namens-Entscheidung in EINE pure Funktion extrahiert: decideAutoRenameBaseName (Single
  Source of Truth fuer Auto-Rename UND Collect — koennen nicht mehr divergieren).
- Wurzel-Schutz darin: Rename nur, wenn ein folderCandidate einen echten Season-/Episode-
  Token traegt (kein Fabrizieren aus token-losen Ordnern). Fixt beide Pfade.
- collectMkvFilesToLibrary leitet den sauberen Namen via dieser Funktion ab (gegated auf
  autoRename4sf4sj — respektiert die Umbenenn-Einstellung), inkl. Companion-Untertitel und
  Dedup gegen den sauberen Namen. mkvFiles traegt jetzt sourceRoot fuer die Ordner-Herleitung.
- Auto-Rename-Loop nutzt jetzt die gemeinsame Funktion (behebt nebenbei 2 latente
  use-before-declaration/TDZ-Fehler an resolveRenameItem).
- Latenter Bug: Casing-Zaehler renamedCount -> renamed (war undeklariert -> ReferenceError,
  vom catch verschluckt -> Casing-Korrekturen wurden still verworfen).

Verifiziert: tsc 6 (von 9 — 3 latente Fehler nebenbei behoben), 678 Tests + 9 neue (7
Charakterisierung der Entscheidung + 2 Collect-Integration: raw->clean + Companion/.srt folgt
+ Datei ausserhalb extractDir), Build gruen. Adversarialer Review-Workflow (4 Linsen) + Advisor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 01:09:21 +02:00

195 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Lessons
## 2026-05-31 — Fix-Diagnose EMPIRISCH bestätigen, bevor man released (Timeout ≠ Account-Hänger)
**Muster:** "acc2/acc3 nie versucht" wurde als "acc1 hängt → Per-Account-Timeout +
Rotation" diagnostiziert und als v1.7.168 released. Falsch: Mega-Debrid-**Web** ist eine
180s-Polling-Schleife (`mega-web-fallback.ts`) — acc1 *pollte* legitim, der 60s-Global-
Timeout (nicht "Hängen") schnitt es ab. Mein 25s-Per-Account-Cap machte es SCHLIMMER
(endlose 25s-Rotation, Datei nie aufgelöst). Erst der User-Log + Lesen der Provider-
Impl deckte es auf. Revert v1.7.169.
**Regel:**
- Ein Timeout bei einem langsam-pollenden Provider ist KEIN Account-Fehler → darf keine
Rotation/kein Skippen auslösen. Vor "Account hängt"-Annahmen die Provider-Impl lesen
(Polling? internes Ceiling? wie lange dauert ein Erfolg legitim?).
- Bei zwei gegensätzlichen Diagnosen (hier: Timeout-zu-kurz vs. IP-Block — stand in der
EIGENEN Memory!) NICHT die bequeme wählen + releasen. Erst empirisch diskriminieren
(Env-Var auf Server, Beobachtung, oder gezielte User-Frage). Ein Symptom, das BEIDE
Hypothesen gleich gut erklärt ("Timeout nach Xs"), beweist keine.
- NICHT lokal "verifizieren" wenn das Problem umgebungsspezifisch ist (geblockte
Server-IP) — lokaler Erfolg ist falsch-positiv.
## 2026-05-30 — Abgestürzten/„aufgehängten" Chat fortsetzen: zuerst reflog lesen
**Muster:** User bat, einen anderen, aufgehängten Chat-Strang „zu Ende zu bringen".
Der Working Tree sah harmlos aus (nur untracked), aber der eigentliche Fortschritt lag
in einem per `reset --hard HEAD~1` weggesetzten Commit, der nur noch im **reflog**
(dangling) lebte.
**Regel:** Bei „mach weiter wo es hing":
1. `git reflog` + `git log --oneline -20` zuerst — Ground Truth, NICHT der
(evtl. stale) gitStatus-Snapshot oder Konversations-interne Annahmen.
2. Reset-weggesetzte/dangling Commits (`git fsck --lost-found`, reflog) inspizieren
(`git show <sha>`) — dort steckt oft die unfertige Arbeit.
3. **Verstehen WARUM weggesetzt**, bevor man blind cherry-picked: hier brach ein
bestehender Test (`.toBe(signal)`-Identitätscheck), den der Fix zwingend ändert.
Der Reset war die Reaktion darauf, nicht „Fix war falsch". Erst die Reset-Ursache
beheben (Test auf Verhalten umstellen), dann den Fix recovern.
4. Eigene Memory (`project_*`) lesen — sie dokumentierte Bug + intendierten Fix exakt.
## 2026-05-30 — Release verifizieren BEVOR "fertig" gesagt wird; curl -F mit Leerzeichen im Pfad
**Muster A (Edit ins Leere + trotzdem released):** Ein Edit schlug fehl ("String not
found"), ich habe es übersehen, committet und v1.7.165 released — die Datei enthielt
das Feature NICHT. Erst der nächste Blick zeigte es.
**Regel:** Nach jedem Feature-Edit VOR dem Release `git show HEAD:datei | grep <marker>`
— bestätigen dass der Code wirklich im Release-Commit ist, nicht nur dass `git commit`
durchlief.
**Muster B (Gitea UNIQUE constraint):** `npm run release:gitea` pusht erst den Tag,
dann erstellt es den Release. Gitea legt beim Tag-Push automatisch einen Tag-Release-
Eintrag an (name=null). `fetchExistingRelease` im Script matcht den nicht → POST create
`UNIQUE constraint failed: release.repo_id, release.tag_name`. Commit + Tag sind dann
schon gepusht, nur der Release+Assets fehlen.
**Recovery:** `GET /api/v1/repos/.../releases/tags/<tag>` → id holen → `PATCH releases/<id>`
mit name/body/draft:false → Assets per `POST releases/<id>/assets?name=<url-encoded>` hochladen.
**Muster C (curl -F Datei mit Leerzeichen):** `curl -F "attachment=@release/Datei mit
Leerzeichen.exe.blockmap"` lädt FALSCHEN Inhalt hoch (Server-Size != lokale Size).
**Regel:** Datei mit Leerzeichen im Namen erst nach `/tmp/leerzeichenfrei` kopieren,
DAS hochladen, Asset-Name über `?name=<url-encoded>` setzen. Danach Server-Size gegen
lokale Size prüfen.
## 2026-05-30 — Nicht in chaotische Parallel-Tool-Batches verfallen (User-Korrektur: "bist du in nem endless loop")
**Muster:** Bei einem großen Multi-File-Edit habe ich Dutzende Tool-Calls (Bash-Probes,
Reads, Edits, Python-Inline-Skripte, mehrfache tsc-Läufe) in EINEN Message-Block gepackt.
Resultat: Ein einzelner Fehler/Cancel hat die ganze parallele Kette abgebrochen, Edits
landeten halb, ich verlor den Überblick welche Änderung wirklich auf Disk war, und es
wirkte wie eine Endlosschleife. Dazu: wegwerf-`scripts/_*.py`/`_*.txt` als Workaround
gegen Output-Encoding statt der dedizierten Tools.
**Regel:**
- Edits über mehrere Dateien **sequenziell, einer nach dem anderen**, mit kurzer
Verifikation dazwischen — nicht 20 spekulative Calls auf einmal.
- Nach jedem Edit, der fehlschlagen kann (Anchor evtl. nicht eindeutig), das Ergebnis
lesen, bevor der nächste folgt. Edit/Write erroren laut — darauf vertrauen.
- KEINE Wegwerf-Python-Skripte ins Repo schreiben, um Shell-Output zu parsen. `Grep`/
`Read`/`Edit` nutzen. Wenn doch ein Temp nötig ist: nach `os.tmpdir()`, nie nach
`scripts/`, und sofort wieder löschen.
- Verifikation gebündelt am ENDE (1× tsc, 1× build, 1× vitest), nicht 10× zwischendrin.
## 2026-05-28 — Analyse-Befund gegen beobachtete Realität gaten (Advisor-Korrektur)
**Muster:** Meine Analyse sagte einen *häufigen* Bug voraus (jede letzte Datei im
Standard-Modus + jede Nested-Datei landet unbenannt), während der User nur "1-2 pro
Staffel" meldete. Ich habe die Diskrepanz bemerkt ("zu schwer um unbemerkt zu bleiben")
und sie mit weiterem Timing-Argument wegrationalisiert.
**Regel:** Wenn die eigene Analyse etwas vorhersagt, das der beobachteten Realität
widerspricht, NICHT die bequeme Lesart wählen — **mit einem Reproduktions-Test gaten**,
bevor man fixt. Failing Test gegen den Ist-Stand zuerst (TDD/systematic-debugging Phase 4):
- reproduziert → Bug bestätigt, mit Sicherheit fixen.
- reproduziert nicht → Analyse hat eine Mitigation übersehen, kein Fix für Nicht-Bug.
## 2026-05-28 — Crash-Debris im Working Tree: stashen, nicht verwerfen
**Muster:** Eine abgestürzte Session (API 400) hinterließ ein uncommittetes Working Tree,
das drei releaste Commits revertierte. Verlockung: `git checkout`/discard, um clean HEAD
zu bekommen.
**Regel:** Fremde/unverstandene uncommittete Änderungen **`git stash`** (non-destruktiv,
recoverable), nie blind verwerfen. Gibt clean HEAD, nichts geht verloren, kein Stall auf
User-Rückfrage. Danach dem User sagen WAS gestasht wurde und WARUM.
## Wiring-Lock vs. Mechanism-Test
Ein Test, der eine Hilfsfunktion mit dem richtigen Flag direkt aufruft, beweist nur, dass
das Flag funktioniert — NICHT, dass der Produktionspfad das Flag setzt. Für echte
Absicherung einen End-to-End-Test durch den realen Einstiegspunkt fahren und per
Negativ-Gate (Flag temporär entfernen → Test muss fallen) verifizieren.
## 2026-05-31 — Log-Symptom ≠ User-Wortlaut: greppen, bevor man auf eine Meldung triggert
**Muster:** User meldete Mega-Debrid-Tageslimit als „Kein Server für diesen Hoster". Ich
wollte den Fix an genau diese Meldung (`MEGA_DEBRID_NO_SERVER_RE`) hängen. Der Advisor
stoppte: der Screenshot zeigte als Cooldown-Grund **„Antwort leer"**, nicht „Kein Server".
**Beweis (Support-Bundle gegrept):** „Kein Server"/„Erreur"/„aucun serveur" = **0** Treffer
im ganzen Bundle, „Antwort leer" = **20.861** Treffer. Der limitierte Account liefert im
Web-Pfad NIE eine unterscheidbare Meldung — `generate()` findet ohne `processDebrid`-Code
keinen Code → `return null` → der Aufrufer macht daraus „Antwort leer". Ein Trigger auf
„Kein Server" wäre toter Code gewesen (= die v1.7.172-Falle, zum 2. Mal fast getreten).
**Regel:** Bevor man einen Fix an einen bestimmten Meldungstext hängt, in den ECHTEN Logs
greppen, ob dieser Text dort überhaupt vorkommt (`count`-Mode, alt-Text vs. Ist-Text). Sind
zwei Fälle auf Message-Ebene nicht unterscheidbar (Tageslimit vs. transienter Blip → beide
„Antwort leer"), nicht raten — über ein **Verhaltens-Signal** klassifizieren: hier eine
Streak (3× hintereinander leer → geparkt), nicht der einmalige Wortlaut.
**Wiring-Test nicht vergessen** (eigene Lesson): die Helfer-Unit-Tests beweisen nur den
Zähler. Ein E2E-Test muss eine ECHTE leere Antwort durch den realen Einstiegspunkt
(`unrestrictWithAccounts` → `classifyAccountFailure` → catch → Park) treiben, sonst bleibt
unbewiesen, dass der Produktionspfad das Signal überhaupt setzt.
## 2026-06-01 — Ein Verifizierer muss dieselbe Pfad-Normalisierung nutzen wie die verifizierte Operation
**Muster:** Neues Renaming-Logging sollte nach jedem Rename verifizieren, ob die Datei
wirklich unter dem Zielnamen liegt. `verifyRename` machte statSync/readdirSync auf den
ROHEN Pfaden — der echte Rename lief aber über `toWindowsLongPathIfNeeded` (\?\-Prefix
ab >=248 Zeichen). Bei langen Scene-Release-Pfaden (genau das, was die App routinemäßig
umbenennt) scheiterten die rohen fs-Calls → falsches „Ziel nicht gefunden" UND — schlimmer —
die Quell-Prüfung scheiterte ebenfalls → `sourceGone` fälschlich true → **falsches „OK"**,
das einen halb-fertigen Verschiebevorgang maskiert. Der Diagnose-Log hätte genau die
schwersten Fälle vergiftet. (Adversarialer Review-Workflow fand es, Confidence 0.8.)
**Regel:** Wenn Code eine Operation VERIFIZIERT, muss er exakt dieselbe Pfad-/Encoding-/
Normalisierung verwenden wie die Operation selbst (hier: \?\-Long-Path-Prefix). Sonst
mis-reportet der Verifizierer still — und am verlässlichsten bei den Edge-Cases, die man
eigentlich fangen wollte. Ein falsches OK in einem Diagnose-Log ist schlimmer als ein
falsches ERROR. Zusatz: readdir-Fehler darf nicht zu „Schreibweise ok" degradieren
(stilles False-OK) → eigenes WARN-Level „nicht verifizierbar".
**Meta:** Bei einem Feature, dessen ganzer Zweck Beobachtbarkeit/Verifikation ist, lohnt
ein adversarialer Review mit Fokus „würde die Verifikation auf der ECHTEN Last (lange
Pfade, case-insensitive FS, EXDEV) korrekt urteilen?" — nicht nur „kompiliert + Happy-Path-Test".
## 2026-06-03 — Renaming „nie 100%": entkoppelte Scans + Namens-Fabrikation aus token-losen Ordnern
**Symptom (aus dem Desktop-Rename-Log diagnostiziert):** 17 Dateien landeten ROH in der
Library ("tvarchiv...s07e12-720.mkv", "4sf-...s04e01.mkv"). KEINE [ERROR]-Zeile — alle [INFO],
weil die Verifikation nur „liegt die Datei am Zielnamen?" prüft, nicht „ist der Zielname
sinnvoll?". Das Logging hat den Bug sichtbar gemacht (genau sein Zweck).
**Root Cause 1 (entkoppelte Scans):** Auto-Rename (scannt nur extractDir, nur present-and-
stable Dateien, Freshness-Gate loggt nur via logger.info → keine Session-Spur) und
collectMkvFilesToLibrary (verschiebt JEDE .mkv, behielt den rohen Basename) sind getrennte
Scans. Eine von Auto-Rename verpasste Datei (verpasster Zyklus ODER lag in „Downloader
Unfertig" außerhalb extractDir) wurde von collect roh weggeschoben. **Fix:** collect leitet
den sauberen Namen SELBST ab — über dieselbe Funktion wie Auto-Rename (decideAutoRenameBaseName,
single source of truth) → Race wird egal, beide Pfade können nicht mehr divergieren.
**Root Cause 2 (latente Fabrikation, vom Advisor gefunden):** decideAutoRenameBaseName
fabrizierte „Mega-Direct-Pack.S01E01" für einen generischen Paketordner, weil
`hasSceneGroupSuffix("Mega-Direct-Pack")` auf „-Pack" falsch-positiv matcht und Guard B dann
die Quell-Episode an einen token-losen Ordner anhängt. Das hätte AUTO-RENAME genauso getroffen
(nur dormant, weil echte Releases saubere Ordner haben). **Fix an der Wurzel:** Rename nur,
wenn IRGENDEIN folderCandidate einen echten Season-/Episode-Token trägt — ein token-loser
Ordner kann keine Episode autoritativ benennen.
**Meta-Lektionen:**
1. Bei „X nie 100%": die Fehler aus dem ECHTEN Log ziehen (greppen), nicht raten. Hier:
„Kein Server" 0×, „Antwort leer" 20k×; und 17 vs vermutete 12 (5 begannen mit Ziffer „4").
2. Symptom-Fix vs Wurzel-Fix: ein collect-seitiger Guard (Quell-Auflösung+Codec) hätte das
Symptom kaschiert + eine Restlücke gelassen; der Wurzel-Fix in der gemeinsamen Funktion
schließt BEIDE Pfade + ermöglicht ehrliches 100%.
3. Wenn ein (Sub-)Agent eine empirische Behauptung aufstellt, die der beobachteten Realität
widerspricht (Review: „liefert no-target" vs Test: „benennt um"), NICHT raten — mit einem
Wegwerf-Diagnose-Test die echte Rückgabe sichtbar machen, DANN entscheiden.
4. „raw-keep ist der Boden" als Guard-Prinzip: ein Rename darf nie einen schlechteren Namen
erzeugen als der Originalname.