--- type: plan-task title: "Task 06 — Nachbesserung: userIgnoreFilters und Linkziele wie Obsidian 1.13.7" status: draft summary: "waisen.py wertet userIgnoreFilters und mehrdeutige Linkziele nach den Regeln von Obsidian 1.13.7 aus, der Fall zum escapten Pipe wird messbar, die Schemafälle werden ehrlich benannt und die Entscheidungsliste zieht nach; das Rot wird gegen eine Kopie des alten Zählers beobachtet." tags: [plan-task, vault-tools] created: 2026-09-30 updated: 2026-09-30 modell: haiku modell_begruendung: "Drei Dateien werden per Skript aus wörtlichen Aus- und wird-Blöcken geändert, jeder Befehl und jede Erwartung steht wörtlich da; die Codeblöcke enthalten keine typografischen Anführungszeichen, Gedankenstriche oder Auslassungspunkte." --- Übersicht: [00-uebersicht.md](00-uebersicht.md) # Task 06 — Nachbesserung: Obsidian-Regeln für Filter und Linkziele Globale Vorgaben: `00-uebersicht.md` im selben Ordner, Abschnitt „Globale Vorgaben“. Voraussetzung: Task 01 ist umgesetzt, `waisen.py`, `faelle_waisen.py` und `ENTWURF-waisen.md` liegen byte-gleich mit Task 01 im Baum. `task-01-zaehler.md` bleibt unverändert; diese Datei ändert den Baum per Aus- und wird-Blöcken. ## Anlass Prüfung der ersten Welle am 2026-09-30, vier Befunde: | Befund | Was diese Nachbesserung tut | |---|---| | wichtig: userIgnoreFilters anders als Obsidian 1.13.7 (Schreibung zählt, leerer Eintrag bricht ab, `//` blendet alles aus, ungültiger Ausdruck bricht ab) | Filter nach `updateUserIgnoreFilters` und `isUserIgnored`: getrimmt, leer übersprungen, Ausdruck erst ab drei Zeichen, ohne Rücksicht auf die Schreibung; ein ungültiger Ausdruck fällt weg und steht als eigene Zeile in der Ausgabe | | klein: mehrdeutige Linkziele anders als `getLinkpathDest` (gleicher Ordner statt Teilbaum, Tiefe statt Länge, Pfadende mit Ordnergrenze) | Auflösung nach `getLinkpathDest` nachgebaut; neue Fälle und Mutanten für jede der drei Abweichungen | | klein: Fall zum escapten Pipe misst das `rstrip` nicht | Die neue Auflösung wandelt Backslashes nicht mehr um, wie `getLinkpathDest`. Damit wird der Fall rot, sobald das `rstrip` fehlt; neuer Mutant `escaptes Pipe nicht entfernt` | | klein: Fälle zu Web-Adresse und Laufwerkspfad messen den Schemafilter nicht | Ehrlich benannt statt neuem Fall: Unter Windows trägt kein Dateiname einen Doppelpunkt, also findet die Auflösung für ein Ziel mit Schema nie eine Notiz, und kein Fall kann an diesem Filter rot werden. Die Fälle heißen jetzt `... findet keine Notiz`, ein Kommentar im Treiber und Entscheidung 8 sagen es | Bewusst weiter strenger als Obsidian: `userIgnoreFilters` null oder keine Liste und ein Eintrag, der kein Text ist, brechen ab (Entscheidung 19). ## Zieldateien - `D:\Claude\.vault-tools\linkcheck\waisen.py` (geändert) - `D:\Claude\.vault-tools\linkcheck\faelle_waisen.py` (geändert) - `D:\Claude\.vault-tools\linkcheck\ENTWURF-waisen.md` (geändert) Mess- und Sicherungsdateien liegen außerhalb des Vaults unter `D:/Claude-waisen-protokoll/nachbesserung-06/`. Das sind keine Zieldateien; sie bleiben für den Koordinator liegen. ## Verbote - `linkcheck.py`, `waisen-ausnahmen.txt`, `faelle.py`, alles unter `.vault-tools/session-hooks/` und jede Taskdatei dieses Plans bleiben unverändert. - Nichts von Hand nachschreiben, auch keinen „offensichtlichen“ Tippfehler korrigieren. Meldet `plan_materialisiere.py` einen Befund: Sicherung aus Schritt 1 zurückspielen (Befehl in Schritt 2), nichts weiter anpassen, melden. - Schritt 1 genau einmal und vor Schritt 2. Ist Schritt 2 schon gelaufen, Schritt 1 nicht wiederholen, sondern melden: Die Sicherung wäre sonst die neue Fassung. - Keine Kunden-, Domain- oder Projektnamen in einer Zieldatei. ## Schritt 1 — Alten Stand sichern und am echten Vault messen ``` mkdir -p D:/Claude-waisen-protokoll/nachbesserung-06/alt D:/Claude-waisen-protokoll/nachbesserung-06/rot && cp D:/Claude/.vault-tools/linkcheck/waisen.py D:/Claude/.vault-tools/linkcheck/faelle_waisen.py D:/Claude/.vault-tools/linkcheck/ENTWURF-waisen.md D:/Claude-waisen-protokoll/nachbesserung-06/alt/ && PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/linkcheck/waisen.py --vault D:/Claude --liste > D:/Claude-waisen-protokoll/nachbesserung-06/vorher.txt; echo "exit=$?"; grep -E '^(Notizen im Graphen|Einträge in userIgnoreFilters|Ausgenommene Waisen|Waisen außerhalb)' D:/Claude-waisen-protokoll/nachbesserung-06/vorher.txt ``` Erwartet: `exit=1` und vier Zeilen mit Zahlen. ## Schritt 2 — Blöcke anwenden ``` python D:/Claude/.vault-tools/plan-kopie/plan_materialisiere.py D:/Claude/.vault-tools/linkcheck/docs/plans/2026-09-29-waisen D:/Claude task-06-nachbesserung-obsidian-regeln.md ``` Erwartet: eine Zeile mit `'neu': 0`, `'ersetzt': 29`, `'angehaengt': 0`, `'geloescht': 0` (`'uebergangen'` zählt die Befehlsblöcke dieser Datei) und die Schlusszeile `0 Befund(e).` Steht dort ein `BEFUND` oder weicht `'ersetzt'` ab, spielst du die Sicherung zurück und hörst auf: ``` cp D:/Claude-waisen-protokoll/nachbesserung-06/alt/waisen.py D:/Claude-waisen-protokoll/nachbesserung-06/alt/faelle_waisen.py D:/Claude-waisen-protokoll/nachbesserung-06/alt/ENTWURF-waisen.md D:/Claude/.vault-tools/linkcheck/; echo "exit=$?" ``` ### `.vault-tools/linkcheck/waisen.py` Kopfkommentar, Lücke zur Linkauflösung. Aus: ```python * Die Auflösung eines Linkziels ist nachgebaut, nicht aus Obsidian abgeschrieben: erst der Pfad ab der Wurzel, dann relativ zur Quelle, dann über den Dateinamen; bei mehreren gleichnamigen Notizen der gleiche Ordner wie die Quelle, sonst der kürzeste Pfad. Die Zahl kann deshalb um einzelne Dateien vom Graphen abweichen. ``` wird: ```python * Die Auflösung eines Linkziels folgt getLinkpathDest aus Obsidian 1.13.7, nachgebaut, nicht abgeschrieben. Abweichungen: Das Namensverzeichnis kennt nur Notizen, die Knoten sind; Obsidian führt dort auch Anhänge und ausgeblendete Dateien. Bei gleich langen Kandidaten entscheidet hier die Reihenfolge des Durchlaufs (alphabetisch je Ordner), in Obsidian die Reihenfolge seines Verzeichnisses. Ob Obsidian Backslashes im Linkziel vor der Auflösung umwandelt, ist nicht belegt; hier bleiben sie stehen. ``` Kopfkommentar, Lücke zu den Filtern. Aus: ```python * userIgnoreFilters werden mit Python-re ausgewertet, nicht mit JavaScript. Ein Ordner wird übersprungen, wenn ein Filter auf seinen Pfad mit abschließendem Schrägstrich passt. Für einen Ausdruck mit Endanker ist das nicht dasselbe wie die Prüfung je Datei. ``` wird: ```python * userIgnoreFilters folgen updateUserIgnoreFilters und isUserIgnored aus Obsidian 1.13.7, werden aber mit Python-re ausgewertet, nicht mit JavaScript. Ein Ausdruck, den nur einer der beiden Dialekte versteht, wirkt verschieden. Ein Ordner wird übersprungen, wenn ein Filter auf seinen Pfad mit abschließendem Schrägstrich passt. Für einen Ausdruck mit Endanker ist das nicht dasselbe wie die Prüfung je Datei. * Strenger als Obsidian: Ist userIgnoreFilters null oder keine Liste oder ein Eintrag kein Text, bricht das Skript ab. Obsidian nimmt null als keinen Filter. ``` Import, der nach der neuen Auflösung nicht mehr gebraucht wird. Aus: ```python import os import posixpath import re ``` wird: ```python import os import re ``` Namensverzeichnis ohne Pfadtabelle. Aus: ```python @dataclass class Index: pfade: dict = field(default_factory=dict) namen: dict = field(default_factory=dict) ``` wird: ```python @dataclass class Index: # Dateiname mit Endung, klein geschrieben -> Notizen in der Reihenfolge des Durchlaufs. namen: dict = field(default_factory=dict) ``` Ergebnis trägt die übersprungenen Filter. Aus: ```python ohne_treffer: list filter_anzahl: int ``` wird: ```python ohne_treffer: list filter_anzahl: int filter_uebersprungen: list ``` Filter nach Obsidian 1.13.7. Aus: ```python def lade_filter(vault: Path) -> list: if not (vault / ".obsidian").is_dir(): raise Abbruch(f"kein Obsidian-Vault: Ordner .obsidian fehlt unter {vault}") app = vault / ".obsidian" / "app.json" if not app.is_file(): return [] try: daten = json.loads(app.read_text(encoding="utf-8-sig")) except (OSError, ValueError) as fehler: raise Abbruch(f".obsidian/app.json nicht lesbar: {fehler}") from fehler if not isinstance(daten, dict): raise Abbruch(".obsidian/app.json ist kein JSON-Objekt") if "userIgnoreFilters" not in daten: return [] roh = daten["userIgnoreFilters"] if not isinstance(roh, list): raise Abbruch(f"userIgnoreFilters ist keine Liste, sondern {type(roh).__name__}") liste = [] for eintrag in roh: if not isinstance(eintrag, str) or not eintrag: raise Abbruch(f"userIgnoreFilters: ein Eintrag ist kein Text oder leer: {eintrag!r}") if len(eintrag) >= 2 and eintrag.startswith("/") and eintrag.endswith("/"): try: liste.append(("regex", re.compile(eintrag[1:-1]))) except re.error as fehler: raise Abbruch( f"userIgnoreFilters: ungültiger regulärer Ausdruck {eintrag!r} ({fehler})" ) from fehler else: liste.append(("praefix", eintrag)) return liste def ausgeblendet(rel: str, filter_liste: list) -> bool: for art, wert in filter_liste: if art == "praefix": if rel.startswith(wert): return True elif wert.search(rel): return True return False ``` wird: ```python def lade_filter(vault: Path) -> tuple: """Liest userIgnoreFilters nach den Regeln von Obsidian 1.13.7. Rückgabe: die wirksamen Filter als kompilierte Ausdrücke und je übersprungenem ungültigem Ausdruck ein Text für die Ausgabe. """ if not (vault / ".obsidian").is_dir(): raise Abbruch(f"kein Obsidian-Vault: Ordner .obsidian fehlt unter {vault}") app = vault / ".obsidian" / "app.json" if not app.is_file(): return [], [] try: daten = json.loads(app.read_text(encoding="utf-8-sig")) except (OSError, ValueError) as fehler: raise Abbruch(f".obsidian/app.json nicht lesbar: {fehler}") from fehler if not isinstance(daten, dict): raise Abbruch(".obsidian/app.json ist kein JSON-Objekt") if "userIgnoreFilters" not in daten: return [], [] roh = daten["userIgnoreFilters"] if not isinstance(roh, list): raise Abbruch(f"userIgnoreFilters ist keine Liste, sondern {type(roh).__name__}") liste = [] uebersprungen = [] for eintrag in roh: if not isinstance(eintrag, str): raise Abbruch(f"userIgnoreFilters: ein Eintrag ist kein Text: {eintrag!r}") # Getrimmt, leer übersprungen, Ausdruck erst ab drei Zeichen, Schreibung egal. text = eintrag.strip() if not text: continue if len(text) > 2 and text.startswith("/") and text.endswith("/"): try: liste.append(re.compile(text[1:-1], re.IGNORECASE)) except re.error as fehler: uebersprungen.append(f"{eintrag!r} ({fehler})") else: liste.append(re.compile("^" + re.escape(text), re.IGNORECASE)) return liste, uebersprungen def ausgeblendet(rel: str, filter_liste: list) -> bool: # Wie isUserIgnored: ausgeblendet, sobald ein Filter im relativen Pfad trifft. return any(muster.search(rel) for muster in filter_liste) ``` Auflösung nach `getLinkpathDest`. Aus: ```python def baue_index(notizen: list) -> Index: index = Index() for rel in notizen: index.pfade[rel.lower()] = rel name = posixpath.basename(rel)[:-3].lower() index.namen.setdefault(name, []).append(rel) return index def _nachschlagen(pfad: str, index: Index): klein = pfad.lower() if klein.endswith(".md") and klein in index.pfade: return index.pfade[klein] return index.pfade.get(klein + ".md") def _nach_namen(ziel: str, quelle: str, index: Index): teil = posixpath.normpath(ziel).lower() if not teil.endswith(".md"): teil += ".md" name = posixpath.basename(teil)[:-3] kandidaten = [r for r in index.namen.get(name, []) if ("/" + r.lower()).endswith("/" + teil)] if not kandidaten: return None ordner = posixpath.dirname(quelle).lower() gleich = sorted(r for r in kandidaten if posixpath.dirname(r).lower() == ordner) if gleich: return gleich[0] return sorted(kandidaten, key=lambda r: (r.count("/"), len(r), r.lower()))[0] def aufloesen(ziel: str, quelle: str, index: Index): ziel = ziel.strip().replace("\\", "/") if not ziel: return None ordner = posixpath.dirname(quelle) if ziel.startswith("/"): kandidaten = [posixpath.normpath(ziel.lstrip("/"))] elif ziel.startswith("./") or ziel.startswith("../"): kandidaten = [posixpath.normpath(posixpath.join(ordner, ziel))] else: kandidaten = [posixpath.normpath(ziel), posixpath.normpath(posixpath.join(ordner, ziel))] for kandidat in kandidaten: if kandidat == ".." or kandidat.startswith("../"): continue treffer = _nachschlagen(kandidat, index) if treffer: return treffer if ziel.startswith(("/", "./", "../")): return None return _nach_namen(ziel, quelle, index) ``` wird: ```python def _name(pfad: str) -> str: return pfad[pfad.rfind("/") + 1:] def _eltern(pfad: str) -> str: stelle = pfad.rfind("/") return "" if stelle == -1 else pfad[:stelle] def baue_index(notizen: list) -> Index: index = Index() for rel in notizen: index.namen.setdefault(_name(rel).lower(), []).append(rel) return index def aufloesen(ziel: str, quelle: str, index: Index): # Nachgebaut nach getLinkpathDest aus Obsidian 1.13.7; gezählt wird der erste Treffer. ziel = ziel.strip() if not ziel: return None pfad = ziel.lower() name = _name(pfad) kandidaten = index.namen.get(name) if "." in name else None if not kandidaten: pfad = (ziel + ".md").lower() name = _name(pfad) kandidaten = index.namen.get(name) if not kandidaten: return None if name == pfad and len(kandidaten) == 1: return kandidaten[0] ordner = _eltern(quelle).lower() if pfad.startswith("./") or pfad.startswith("../"): if pfad.startswith("./../"): pfad = pfad[2:] if pfad.startswith("./"): if ordner: ordner += "/" pfad = ordner + pfad[2:] else: while pfad.startswith("../"): pfad = pfad[3:] ordner = _eltern(ordner) if ordner: ordner += "/" pfad = ordner + pfad for rel in kandidaten: if rel.lower() == pfad: return rel if pfad.startswith("/"): pfad = pfad[1:] for rel in kandidaten: if rel.lower() == pfad: return rel if ziel.startswith("/"): return None # Pfadende ohne Ordnergrenze; erst der Teilbaum der Quelle, je Gruppe der kürzere Pfad. passend = [rel for rel in kandidaten if rel.lower().endswith(pfad)] nah = sorted((rel for rel in passend if rel.lower().startswith(ordner)), key=len) fern = sorted((rel for rel in passend if not rel.lower().startswith(ordner)), key=len) treffer = nah + fern return treffer[0] if treffer else None ``` Aufruf in `analysiere()`. Aus: ```python filter_liste = lade_filter(vault) ``` wird: ```python filter_liste, filter_uebersprungen = lade_filter(vault) ``` Rückgabe von `analysiere()`. Aus: ```python return Ergebnis(notizen, waisen, ausgenommen, ausgehend, muster, ohne_treffer, len(filter_liste)) ``` wird: ```python return Ergebnis(notizen, waisen, ausgenommen, ausgehend, muster, ohne_treffer, len(filter_liste), filter_uebersprungen) ``` Ausgabe der übersprungenen Filter. Aus: ```python anzahl = len(ergebnis.waisen) if args.zahl: print(anzahl) return 1 if anzahl else 0 print(f"# Waisen - {vault}") print(f"Notizen im Graphen: {len(ergebnis.notizen)}") print(f"Einträge in userIgnoreFilters: {ergebnis.filter_anzahl}") ``` wird: ```python anzahl = len(ergebnis.waisen) if args.zahl: # Auf stderr, damit stdout nur die Zahl trägt; der Sitzungsstart liest stdout. for text in ergebnis.filter_uebersprungen: print(f"Filter übersprungen, ungültiger regulärer Ausdruck: {text}", file=sys.stderr) print(anzahl) return 1 if anzahl else 0 print(f"# Waisen - {vault}") print(f"Notizen im Graphen: {len(ergebnis.notizen)}") print(f"Einträge in userIgnoreFilters: {ergebnis.filter_anzahl}") for text in ergebnis.filter_uebersprungen: print(f"Filter übersprungen, ungültiger regulärer Ausdruck: {text}") ``` stderr ebenfalls als UTF-8, sonst kommt der Umlaut der Meldung ohne `PYTHONIOENCODING` kaputt an. Aus: ```python sys.stdout.reconfigure(encoding="utf-8") ``` wird: ```python sys.stdout.reconfigure(encoding="utf-8") sys.stderr.reconfigure(encoding="utf-8") ``` ### `.vault-tools/linkcheck/faelle_waisen.py` Ein Fall kann Textstellen in stderr verlangen. Aus: ```python ohne_pythonioencoding: bool = False vault_fehlt: bool = False ``` wird: ```python ohne_pythonioencoding: bool = False vault_fehlt: bool = False # Textstellen, die in stderr stehen müssen; bei --zahl gehört stdout allein der Zahl. texte_stderr: list = field(default_factory=list) ``` Web-Adresse: ehrlich benannt. Aus: ```python Fall("markdown | Web-Adresse zaehlt nicht", neu("A/neu.md", "[x](https://example.invalid/C/allein.md)\n"), ["A/neu.md", "C/allein.md"]), ``` wird: ```python # Web-Adresse und Laufwerkspfad messen das Ergebnis, nicht den Schemafilter in # markdown_ziele(): Unter Windows trägt kein Dateiname einen Doppelpunkt, also findet # die Auflösung für ein Ziel mit Schema ohnehin keine Notiz. Deshalb kein Mutant dafür. Fall("markdown | Web-Adresse findet keine Notiz", neu("A/neu.md", "[x](https://example.invalid/C/allein.md)\n"), ["A/neu.md", "C/allein.md"]), ``` Laufwerkspfad: ehrlich benannt. Aus: ```python Fall("markdown | Laufwerkspfad zaehlt nicht", ``` wird: ```python Fall("markdown | Laufwerkspfad findet keine Notiz", ``` Filter ohne Rücksicht auf die Schreibung. Aus: ```python Fall("ausblendung | regulaerer Ausdruck in der Tiefe", neu("P/node_modules/pkg/README.md", "# R\n"), ["C/allein.md"]), ``` wird: ```python Fall("ausblendung | regulaerer Ausdruck in der Tiefe", neu("P/node_modules/pkg/README.md", "# R\n"), ["C/allein.md"]), Fall("ausblendung | Praefixfilter ohne Ruecksicht auf die Schreibung", lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["archiv/"]}'), schreibe(v, "Archiv/x.md", "# X\n")), ["C/allein.md"]), Fall("ausblendung | Ausdruck ohne Ruecksicht auf die Schreibung", lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["/(^|/)TMP//"]}'), schreibe(v, "Q/tmp/y.md", "# Y\n")), ["C/allein.md"]), ``` Leere, getrimmte, zu kurze und ungültige Filtereinträge. Aus: ```python Fall("obsidian | Eintrag ist leer", neu(".obsidian/app.json", '{"userIgnoreFilters": [""]}'), None, ["kein Text"]), Fall("obsidian | ungueltiger regulaerer Ausdruck", neu(".obsidian/app.json", '{"userIgnoreFilters": ["/(/"]}'), None, ["ungültiger regulärer Ausdruck"]), ``` wird: ```python Fall("obsidian | leere Eintraege werden uebersprungen", lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["", " "]}'), schreibe(v, "_index/x.md", "# X\n")), ["C/allein.md", "_index/x.md"], ["Einträge in userIgnoreFilters: 0"]), Fall("obsidian | Eintrag mit Leerzeichen wird getrimmt", lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": [" _index/ "]}'), schreibe(v, "_index/x.md", "# X\n")), ["C/allein.md"]), Fall("obsidian | zwei Schraegstriche sind kein Ausdruck", neu(".obsidian/app.json", '{"userIgnoreFilters": ["//"]}'), ["C/allein.md"], ["Einträge in userIgnoreFilters: 1"]), Fall("obsidian | ungueltiger Ausdruck wird uebersprungen und genannt", lambda v: (schreibe(v, ".obsidian/app.json", '{"userIgnoreFilters": ["/(/", "_index/"]}'), schreibe(v, "_index/x.md", "# X\n")), ["C/allein.md"], ["Filter übersprungen, ungültiger regulärer Ausdruck: '/(/'", "Einträge in userIgnoreFilters: 1"]), ``` Neue Fälle zur Auflösung. Aus: ```python Fall("mehrdeutig | sonst der kuerzeste Pfad", lambda v: (schreibe(v, "A/B/doppelt.md", "# D\n"), schreibe(v, "Z/doppelt.md", "# D\n"), schreibe(v, "Q/verweis.md", "[[doppelt]]\n")), ["A/B/doppelt.md", "C/allein.md"]), ``` wird: ```python Fall("mehrdeutig | sonst der kuerzeste Pfad", lambda v: (schreibe(v, "A/B/doppelt.md", "# D\n"), schreibe(v, "Z/doppelt.md", "# D\n"), schreibe(v, "Q/verweis.md", "[[doppelt]]\n")), ["A/B/doppelt.md", "C/allein.md"]), # Regeln aus getLinkpathDest (Obsidian 1.13.7). Wo es eine Wahl gibt, liegt die richtige alphabetisch hinten. Fall("mehrdeutig | Teilbaum der Quelle vor kuerzerem Pfad", lambda v: (schreibe(v, "A/doppelt.md", "# D\n"), schreibe(v, "Z/tief/doppelt.md", "# D\n"), schreibe(v, "Z/verweis.md", "[[doppelt]]\n")), ["A/doppelt.md", "C/allein.md"]), Fall("mehrdeutig | Laenge vor Tiefe", lambda v: (schreibe(v, "Aaaaaaaaaaaa/d.md", "# D\n"), schreibe(v, "Z/B/d.md", "# D\n"), schreibe(v, "R/v.md", "[[d]]\n")), ["Aaaaaaaaaaaa/d.md", "C/allein.md"]), Fall("wikilink | Pfadende ohne Ordnergrenze", lambda v: (schreibe(v, "Obertief/allein.md", "# O\n"), schreibe(v, "R/w.md", "[[tief/allein]]\n")), ["C/allein.md"]), Fall("markdown | ./ vor gleichnamiger Notiz an der Wurzel", lambda v: (schreibe(v, "n.md", "# N\n"), schreibe(v, "Z/Q/n.md", "# N\n"), schreibe(v, "Z/Q/neu.md", "[x](./n.md)\n")), ["C/allein.md", "n.md"]), Fall("markdown | ./../ wie ../", lambda v: (schreibe(v, "n.md", "# N\n"), schreibe(v, "Z/n.md", "# N\n"), schreibe(v, "Z/Q/neu.md", "[x](./../n.md)\n")), ["C/allein.md", "n.md"]), Fall("markdown | fuehrender / sucht nur ab der Wurzel", neu("A/neu.md", "[x](/allein.md)\n"), ["A/neu.md", "C/allein.md"]), # Kein Mutant: Das folgt aus der Schleife über ../, ohne eigene Zeile im Code. Fall("markdown | ../ ueber die Wurzel hinaus bleibt an der Wurzel", neu("neu.md", "[x](../C/allein.md)\n"), []), Fall("markdown | Backslash bleibt stehen", neu("A/neu.md", "[x](..\\C\\allein.md)\n"), ["A/neu.md", "C/allein.md"]), ``` Übersprungener Filter bei `--zahl`. Aus: ```python Fall("zahl | Abbruch ohne Zahl", lambda v: shutil.rmtree(v / ".obsidian"), None, ["Waisen - ABGEBROCHEN"], zahl=True), ] ``` wird: ```python Fall("zahl | Abbruch ohne Zahl", lambda v: shutil.rmtree(v / ".obsidian"), None, ["Waisen - ABGEBROCHEN"], zahl=True), Fall("zahl | uebersprungener Filter bleibt aus der Zahl", neu(".obsidian/app.json", '{"userIgnoreFilters": ["/(/"]}'), ["C/allein.md"], zahl=True, texte_stderr=["Filter übersprungen, ungültiger regulärer Ausdruck: '/(/'"]), ] ``` Prüfung von stderr im Fall. Aus: ```python code, out, err, dauer = fahre(ziel_skript, argumente, umgebung) probleme = pruefe_ausgabe(fall, code, out) ``` wird: ```python code, out, err, dauer = fahre(ziel_skript, argumente, umgebung) probleme = pruefe_ausgabe(fall, code, out) for text in fall.texte_stderr: if text not in err: probleme.append(f"fehlt in stderr: {text}") ``` Mutant zum regulären Filter auf die neue Zeile. Aus: ```python ("regulaerer Filter als Praefix", " liste.append((\"regex\", re.compile(eintrag[1:-1])))", " liste.append((\"praefix\", eintrag))", ["ausblendung | regulaerer Ausdruck in der Tiefe"]), ``` wird: ```python ("regulaerer Filter als Praefix", " liste.append(re.compile(text[1:-1], re.IGNORECASE))", " liste.append(re.compile(\"^\" + re.escape(text), re.IGNORECASE))", ["ausblendung | regulaerer Ausdruck in der Tiefe"]), ``` Mutanten zur alten Auflösung ersetzt, neue Mutanten für Filter und Auflösung. Aus: ```python ("gleicher Ordner ignoriert", " gleich = sorted(r for r in kandidaten if posixpath.dirname(r).lower() == ordner)", " gleich = []", ["mehrdeutig | gleichnamige Notiz im selben Ordner gewinnt"]), ("kein Kandidat relativ zur Quelle", " kandidaten = [posixpath.normpath(ziel), posixpath.normpath(posixpath.join(ordner, ziel))]", " kandidaten = [posixpath.normpath(ziel)]", ["mehrdeutig | relativ zur Quelle vor dem Namen"]), ("mehrdeutig nur alphabetisch", " return sorted(kandidaten, key=lambda r: (r.count(\"/\"), len(r), r.lower()))[0]", " return sorted(kandidaten, key=lambda r: r.lower())[0]", ["mehrdeutig | sonst der kuerzeste Pfad"]), ] ``` wird: ```python ("Praefixfilter mit Schreibung", " liste.append(re.compile(\"^\" + re.escape(text), re.IGNORECASE))", " liste.append(re.compile(\"^\" + re.escape(text)))", ["ausblendung | Praefixfilter ohne Ruecksicht auf die Schreibung"]), ("Ausdruck mit Schreibung", " liste.append(re.compile(text[1:-1], re.IGNORECASE))", " liste.append(re.compile(text[1:-1]))", ["ausblendung | Ausdruck ohne Ruecksicht auf die Schreibung"]), ("Filtereintrag nicht getrimmt", " text = eintrag.strip()", " text = eintrag", ["obsidian | Eintrag mit Leerzeichen wird getrimmt"]), ("leerer Filtereintrag nicht uebersprungen", " if not text:", " if False:", ["obsidian | leere Eintraege werden uebersprungen"]), ("zwei Schraegstriche als Ausdruck", " if len(text) > 2 and text.startswith(\"/\") and text.endswith(\"/\"):", " if len(text) >= 2 and text.startswith(\"/\") and text.endswith(\"/\"):", ["obsidian | zwei Schraegstriche sind kein Ausdruck"]), ("ungueltiger Ausdruck bricht ab", " uebersprungen.append(f\"{eintrag!r} ({fehler})\")", " raise", ["obsidian | ungueltiger Ausdruck wird uebersprungen und genannt", "zahl | uebersprungener Filter bleibt aus der Zahl"]), ("uebersprungener Filter nicht genannt", " print(f\"Filter übersprungen, ungültiger regulärer Ausdruck: {text}\")", " pass", ["obsidian | ungueltiger Ausdruck wird uebersprungen und genannt"]), ("Meldung bei --zahl auf stdout", " print(f\"Filter übersprungen, ungültiger regulärer Ausdruck: {text}\", file=sys.stderr)", " print(f\"Filter übersprungen, ungültiger regulärer Ausdruck: {text}\")", ["zahl | uebersprungener Filter bleibt aus der Zahl"]), ("Meldung bei --zahl fehlt", " print(f\"Filter übersprungen, ungültiger regulärer Ausdruck: {text}\", file=sys.stderr)", " pass", ["zahl | uebersprungener Filter bleibt aus der Zahl"]), ("escaptes Pipe nicht entfernt", ' ziel = treffer.group(1).strip().rstrip("\\\\").strip()', " ziel = treffer.group(1).strip()", ["wikilink | escaptes Pipe in einer Tabellenzelle"]), ("Backslash im Ziel umgewandelt", " ziel = ziel.strip()", ' ziel = ziel.strip().replace("\\\\", "/")', ["markdown | Backslash bleibt stehen"]), ("./ nicht gegen den Ordner der Quelle", " pfad = ordner + pfad[2:]", " pfad = pfad[2:]", ["markdown | ./ vor gleichnamiger Notiz an der Wurzel"]), ("./../ nicht verkuerzt", " pfad = pfad[2:]", " pass", ["markdown | ./../ wie ../"]), ("fuehrender / sucht auch ueber den Namen", " if ziel.startswith(\"/\"):", " if False:", ["markdown | fuehrender / sucht nur ab der Wurzel"]), ("Pfadende mit Ordnergrenze", " passend = [rel for rel in kandidaten if rel.lower().endswith(pfad)]", " passend = [rel for rel in kandidaten if (\"/\" + rel.lower()).endswith(\"/\" + pfad)]", ["wikilink | Pfadende ohne Ordnergrenze"]), ("Teilbaum der Quelle ohne Vorrang", " treffer = nah + fern", " treffer = sorted(passend, key=len)", ["mehrdeutig | gleichnamige Notiz im selben Ordner gewinnt", "mehrdeutig | relativ zur Quelle vor dem Namen", "mehrdeutig | Teilbaum der Quelle vor kuerzerem Pfad"]), ("nur gleicher Ordner statt Teilbaum", " nah = sorted((rel for rel in passend if rel.lower().startswith(ordner)), key=len)", " nah = sorted((rel for rel in passend if _eltern(rel).lower() == ordner), key=len)", ["mehrdeutig | Teilbaum der Quelle vor kuerzerem Pfad"]), ("Tiefe vor Laenge", " fern = sorted((rel for rel in passend if not rel.lower().startswith(ordner)), key=len)", " fern = sorted((rel for rel in passend if not rel.lower().startswith(ordner)), key=lambda rel: (rel.count(\"/\"), len(rel)))", ["mehrdeutig | Laenge vor Tiefe"]), ("ohne Sortierung nach Laenge", " fern = sorted((rel for rel in passend if not rel.lower().startswith(ordner)), key=len)", " fern = [rel for rel in passend if not rel.lower().startswith(ordner)]", ["mehrdeutig | Laenge vor Tiefe", "mehrdeutig | sonst der kuerzeste Pfad"]), ] ``` ### `.vault-tools/linkcheck/ENTWURF-waisen.md` Entscheidungen 3 und 4. Aus: ```markdown | 3 | userIgnoreFilters? | Zur Laufzeit aus `.obsidian/app.json`. Ein Eintrag ohne umschließende Schrägstriche ist ein Präfix ab der Vault-Wurzel, einer mit Schrägstrichen ein regulärer Ausdruck, gesucht im relativen Pfad. Ausgeblendete Dateien sind weder Knoten noch Quelle einer Kante. | **Quelle** für die Filterform: `AI/Regelwerk/navigation.md`, belegt dort am Code von Obsidian 1.13.7 (`updateUserIgnoreFilters`); beim Planen nicht selbst nachgelesen. **Projektvorgabe** für Knoten und Kanten: Spec | | 4 | Dialekt der Ausdrücke? | Python `re` statt JavaScript. Für die Einträge im Vault am 2026-09-29 gleichwertig. | **Projektvorgabe**, Lücke im Kopfkommentar | ``` wird: ```markdown | 3 | userIgnoreFilters? | Zur Laufzeit aus `.obsidian/app.json`. Jeder Eintrag wird getrimmt, ein leerer fällt weg. Ein Eintrag mit mehr als zwei Zeichen, der mit `/` beginnt und endet, ist ein regulärer Ausdruck, gesucht im relativen Pfad; jeder andere ist ein Präfix ab der Vault-Wurzel. Beide gelten ohne Rücksicht auf Groß- und Kleinschreibung. Ausgeblendete Dateien sind weder Knoten noch Quelle einer Kante. | **Quelle** für die Filterregeln: Obsidian 1.13.7, `updateUserIgnoreFilters` und `isUserIgnored`, siehe Abschnitt Quelle der Obsidian-Regeln. **Projektvorgabe** für Knoten und Kanten: Spec | | 4 | Dialekt der Ausdrücke? | Python `re` statt JavaScript. Ein Ausdruck, den nur einer der beiden Dialekte versteht, wirkt verschieden. Für die Einträge im Vault am 2026-09-29 gleichwertig. | **Projektvorgabe**, Lücke im Kopfkommentar | ``` Entscheidung 8. Aus: ```markdown | 8 | Was ist ein Markdown-Link? | `[Text](Ziel)` im Text, auch als Bild, Ziel auch in spitzen Klammern, optional mit Titel. Anker und Abfrage fallen weg, das Ziel wird URL-dekodiert. Ziele mit Schema (`https:`, `mailto:`, `obsidian:`, Laufwerk) und reine Anker zählen nicht. Ein Ziel ohne `.md` wird mit `.md` gesucht. | **Projektvorgabe**: Spec. URL-Dekodierung und fehlende Endung: Prüfschritte P1 und P3 | ``` wird: ```markdown | 8 | Was ist ein Markdown-Link? | `[Text](Ziel)` im Text, auch als Bild, Ziel auch in spitzen Klammern, optional mit Titel. Anker und Abfrage fallen weg, das Ziel wird URL-dekodiert. Ziele mit Schema (`https:`, `mailto:`, `obsidian:`, Laufwerk) und reine Anker zählen nicht. Ein Ziel ohne `.md` wird mit `.md` gesucht. Der Schemafilter ist Doppelschutz: Unter Windows trägt kein Dateiname einen Doppelpunkt, die Auflösung findet für ein Ziel mit Schema ohnehin keine Notiz. Deshalb misst ihn kein Fall und kein Mutant. | **Projektvorgabe**: Spec. URL-Dekodierung und fehlende Endung: Prüfschritte P1 und P3 | ``` Entscheidungen 10 bis 12. Aus: ```markdown | 10 | Wie wird ein Ziel aufgelöst? | Mit `/` am Anfang ab der Wurzel. Mit `./` oder `../` relativ zum Ordner der Quelle. Sonst erst ab der Wurzel, dann relativ zur Quelle, dann über den Dateinamen mit passendem Pfadende. | **Projektvorgabe**, nachgebaut; Lücke benannt | | 11 | Groß- und Kleinschreibung im Ziel? | Egal. | **Projektvorgabe**: das Dateisystem unterscheidet sie nicht | | 12 | Mehrdeutiger Name? | Gleicher Ordner wie die Quelle, sonst kürzester Pfad, dann alphabetisch. | **Projektvorgabe**: Spec nennt das als benannte Lücke | ``` wird: ```markdown | 10 | Wie wird ein Ziel aufgelöst? | Kandidaten sind die Notizen mit demselben Dateinamen samt Endung, klein geschrieben: erst mit dem Ziel wie geschrieben, wenn sein Name einen Punkt trägt, sonst mit angehängtem `.md`. Ein Ziel mit `./` oder `../` wird gegen den Ordner der Quelle gerechnet, `./../` wie `../`, ein `../` über die Wurzel hinaus bleibt an der Wurzel. Dann gewinnt der Kandidat, dessen Pfad dem Ziel gleicht; ein `/` vorn fällt dafür weg. Ein Ziel mit `/` vorn endet danach ohne Treffer. Sonst zählt jeder Kandidat, dessen Pfad auf das Ziel endet, ohne Ordnergrenze. Backslashes bleiben stehen. | **Quelle**: Obsidian 1.13.7, `getLinkpathDest`. Ob der Linkleser von Obsidian Backslashes vorher umwandelt: **Lücke**, benannt; Prüfweg ist der Vergleich vorher und nachher in Task 06 des Plans | | 11 | Groß- und Kleinschreibung im Ziel? | Egal. | **Quelle**: `getLinkpathDest` vergleicht in Kleinschreibung | | 12 | Mehrdeutiger Name? | Erst die Kandidaten, deren Pfad mit dem Ordner der Quelle beginnt (der ganze Teilbaum, ohne Ordnergrenze), dann die übrigen; in beiden Gruppen der kürzere Pfad in Zeichen zuerst. Bei gleicher Länge entscheidet die Reihenfolge des Durchlaufs, alphabetisch je Ordner. `waisen.py` nimmt den ersten Kandidaten. | **Quelle**: Obsidian 1.13.7, `getLinkpathDest` und die Vergleichsfunktion nach Pfadlänge. Reihenfolge bei gleicher Länge: **Lücke**, benannt. Dass der Graph den ersten Kandidaten nimmt: **Projektvorgabe**, der Auszug zeigt es nicht | ``` Entscheidung 19. Aus: ```markdown | 19 | `.obsidian` fehlt? | Befund, Exit 2: kein Obsidian-Vault. Fehlt nur `app.json` oder der Schlüssel, gilt kein Filter, wie in Obsidian ohne Einstellung. `app.json` kaputt oder mit falschem Typ: Befund. | **Projektvorgabe** | ``` wird: ```markdown | 19 | `.obsidian` fehlt? | Befund, Exit 2: kein Obsidian-Vault. Fehlt nur `app.json` oder der Schlüssel, gilt kein Filter, wie in Obsidian ohne Einstellung. `app.json` kaputt, `userIgnoreFilters` null oder keine Liste, ein Eintrag kein Text: Befund, Exit 2. Ein leerer Eintrag fällt weg. Ein ungültiger Ausdruck fällt weg und steht als Zeile `Filter übersprungen, ungültiger regulärer Ausdruck:` in der Ausgabe, bei `--zahl` auf stderr, damit stdout nur die Zahl trägt. | **Projektvorgabe** für die Abbrüche, strenger als Obsidian 1.13.7, das null als keinen Filter nimmt. **Quelle** für leere und ungültige Einträge: `updateUserIgnoreFilters` | ``` Neuer Abschnitt zur Quelle. Aus: ```markdown Keine Zeile steht auf Annahme ohne Prüfweg. Wo eine Antwort nicht aus einer Quelle stammt, ist sie Projektvorgabe und im Plan als Prüfschritt P1 bis P3 in der App benannt. ``` wird: ```markdown Keine Zeile steht auf Annahme ohne Prüfweg. Wo eine Antwort nicht aus einer Quelle stammt, ist sie Projektvorgabe und im Plan als Prüfschritt P1 bis P3 in der App benannt. ## Quelle der Obsidian-Regeln Nachgebessert am 2026-09-30 nach der Prüfung der ersten Welle (Plan `docs/plans/2026-09-29-waisen/`, Task 06). - Filter und Linkauflösung folgen Obsidian 1.13.7: `updateUserIgnoreFilters`, `isUserIgnored`, `getLinkpathDest` und die Vergleichsfunktion nach Pfadlänge, mit der `getLinkpathDest` sortiert. - Gelesen am 2026-09-30 mit `grep -a` im Programmabbild `obsidian-1.13.7.asar` unter `%APPDATA%\obsidian\`. Nachgebaut, nicht abgeschrieben. - Gegenprobe des Prüfers am selben Tag: Sein Nachbau dieser Regeln, der zusätzlich Backslashes in `/` umwandelt, fand im echten Vault 0 Linkziele, die von `waisen.py` in der Fassung vom 2026-09-29 abweichen. - Nicht aus der Quelle belegt: ob der Linkleser von Obsidian Backslashes vor der Auflösung umwandelt, und in welcher Reihenfolge sein Namensverzeichnis gleich lange Pfade führt. - Eine neue Obsidian-Version macht diese Herkunft nicht falsch, aber ungeprüft. Prüfweg: dieselben Funktionen im neuen Abbild nachlesen. ``` Angriffsliste, Zeilen leer und doppelt. Aus: ```markdown | leer | leere Filterliste, leerer Filtereintrag, leere Ausnahmedatei, nur Kommentare, leere Notiz, Vault ohne Notiz | | doppelt | doppeltes Muster, gleichnamige Notizen in zwei Ordnern: gleicher Ordner, relativ zur Quelle, kürzester Pfad; die richtige Wahl liegt jeweils alphabetisch hinten | ``` wird: ```markdown | leer | leere Filterliste, leere und nur aus Leerzeichen bestehende Filtereinträge, leere Ausnahmedatei, nur Kommentare, leere Notiz, Vault ohne Notiz | | doppelt | doppeltes Muster, gleichnamige Notizen in zwei Ordnern: gleicher Ordner, relativ zur Quelle, kürzester Pfad, Teilbaum der Quelle vor kürzerem Pfad, Länge vor Tiefe, `./` und `./../` vor gleichnamiger Notiz an der Wurzel; wo es eine Wahl gibt, liegt die richtige alphabetisch hinten | ``` Angriffsliste, Zeile Wertebereich. Aus: ```markdown | Wertebereich | Muster mit Backslash, führendem Schrägstrich, `..`, `.` als Pfadteil, leerem Pfadteil, Laufwerk; Stern über Ordnergrenze | ``` wird: ```markdown | Wertebereich | Muster mit Backslash, führendem Schrägstrich, `..`, `.` als Pfadteil, leerem Pfadteil, Laufwerk; Stern über Ordnergrenze; Filter aus zwei Schrägstrichen, Filter mit Leerzeichen, Präfixfilter und Ausdruck in anderer Schreibung als der Pfad, ungültiger Ausdruck mit und ohne `--zahl`; Linkziel mit Backslash, mit `/` vorn ohne Treffer an der Wurzel, mit `../` über die Wurzel hinaus, Pfadende ohne Ordnergrenze | ``` Was die Angriffsliste nicht misst. Aus: ```markdown | Absturz | Notiz nicht lesbar, Ordner nicht lesbar, unerwarteter Fehler (alle im Prozess) | ``` wird: ```markdown | Absturz | Notiz nicht lesbar, Ordner nicht lesbar, unerwarteter Fehler (alle im Prozess) | Nicht gemessen: der Schemafilter aus Entscheidung 8. Die Fälle `Web-Adresse findet keine Notiz` und `Laufwerkspfad findet keine Notiz` bestehen auch ohne ihn, weil die Auflösung für ein Ziel mit Doppelpunkt nie eine Notiz findet. ``` ## Schritt 3 — Rot: neuer Treiber gegen den alten Zähler Der neue Treiber läuft in einem Ordner außerhalb des Vaults gegen die Sicherung des alten `waisen.py`. So siehst du das Rot, bevor die neue Fassung gilt. ``` R=D:/Claude-waisen-protokoll/nachbesserung-06/rot; cp D:/Claude-waisen-protokoll/nachbesserung-06/alt/waisen.py D:/Claude/.vault-tools/linkcheck/linkcheck.py D:/Claude/.vault-tools/linkcheck/waisen-ausnahmen.txt D:/Claude/.vault-tools/linkcheck/faelle_waisen.py "$R/" && PYTHONIOENCODING=utf-8 python "$R/faelle_waisen.py"; echo "exit=$?" ``` Erwartet: erste Zeile `BESTANDEN GRUNDLINIE`, Schlusszeile `N von M Faellen bestanden.` mit N kleiner als M, `exit=1`. Genau die folgenden Fälle beginnen mit `FEHLER`, jeder andere mit `BESTANDEN`. Hinter dem Doppelpunkt steht die Problemzeile, die unter der jeweiligen `FEHLER`-Zeile mindestens stehen muss: - `obsidian | leere Eintraege werden uebersprungen`: `Exit 2 statt 1` - `obsidian | Eintrag mit Leerzeichen wird getrimmt`: `Waisen ['C/allein.md', '_index/x.md'] statt ['C/allein.md']` - `obsidian | zwei Schraegstriche sind kein Ausdruck`: `Exit 2 statt 1` - `obsidian | ungueltiger Ausdruck wird uebersprungen und genannt`: `Exit 2 statt 1` - `ausblendung | Praefixfilter ohne Ruecksicht auf die Schreibung`: `Waisen ['Archiv/x.md', 'C/allein.md'] statt ['C/allein.md']` - `ausblendung | Ausdruck ohne Ruecksicht auf die Schreibung`: `Waisen ['C/allein.md', 'Q/tmp/y.md'] statt ['C/allein.md']` - `zahl | uebersprungener Filter bleibt aus der Zahl`: `Exit 2 statt 1` - `mehrdeutig | Teilbaum der Quelle vor kuerzerem Pfad`: `Waisen ['C/allein.md', 'Z/tief/doppelt.md'] statt ['A/doppelt.md', 'C/allein.md']` - `mehrdeutig | Laenge vor Tiefe`: `Waisen ['C/allein.md', 'Z/B/d.md'] statt ['Aaaaaaaaaaaa/d.md', 'C/allein.md']` - `wikilink | Pfadende ohne Ordnergrenze`: `Waisen ['C/allein.md', 'Obertief/allein.md', 'R/w.md'] statt ['C/allein.md']` - `markdown | ../ ueber die Wurzel hinaus bleibt an der Wurzel`: `Waisen ['C/allein.md', 'neu.md'] statt []` - `markdown | Backslash bleibt stehen`: `Waisen [] statt ['A/neu.md', 'C/allein.md']` Die Fälle `markdown | ./ vor gleichnamiger Notiz an der Wurzel`, `markdown | ./../ wie ../` und `markdown | fuehrender / sucht nur ab der Wurzel` bestehen schon gegen den alten Zähler; sie sichern Verhalten, das die neue Auflösung nicht verlieren darf. ## Schritt 4 — Rotlauf gegen den alten Zähler ``` PYTHONIOENCODING=utf-8 python D:/Claude-waisen-protokoll/nachbesserung-06/rot/faelle_waisen.py --rotlauf; echo "exit=$?" ``` Erwartet: Schlusszeile `Rotlauf: mindestens ein Mutant nicht getoetet oder ein Muster falsch.` und `exit=1`, dazu: - `GETOETET` für: Code nicht ausgeblendet, Markdown-Links ignoriert, Frontmatter ignoriert, Ausnahmen nicht abgezogen, Selbstreferenz als Kante, Punktordner mitgezaehlt, fehlende Ausnahmedatei still, keine URL-Dekodierung, Frontmatter ohne Schlusszeile als Kopf, Punkt als Pfadteil erlaubt, leerer Pfadteil erlaubt, Dateifilter ignoriert. - `UEBERLEBT` für `escaptes Pipe nicht entfernt` (grün bleibt `wikilink | escaptes Pipe in einer Tabellenzelle`; das ist der Befund der Prüfung: der alte Zähler wandelt den Backslash um) und für `fuehrender / sucht auch ueber den Namen` (grün bleibt `markdown | fuehrender / sucht nur ab der Wurzel`). - `MUSTER ... steht 0-mal in waisen.py, unbekannte Faelle []` für jeden anderen Mutanten: regulaerer Filter als Praefix, Praefixfilter mit Schreibung, Ausdruck mit Schreibung, Filtereintrag nicht getrimmt, leerer Filtereintrag nicht uebersprungen, zwei Schraegstriche als Ausdruck, ungueltiger Ausdruck bricht ab, uebersprungener Filter nicht genannt, Meldung bei --zahl auf stdout, Meldung bei --zahl fehlt, Backslash im Ziel umgewandelt, ./ nicht gegen den Ordner der Quelle, ./../ nicht verkuerzt, Pfadende mit Ordnergrenze, Teilbaum der Quelle ohne Vorrang, nur gleicher Ordner statt Teilbaum, Tiefe vor Laenge, ohne Sortierung nach Laenge. ## Schritt 5 — Grün: Angriffsliste im Baum ``` PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/linkcheck/faelle_waisen.py; echo "exit=$?" ``` Erwartet: erste Zeile `BESTANDEN GRUNDLINIE`, keine Zeile, die mit `FEHLER` beginnt, Schlusszeile `N von N Faellen bestanden.` mit zweimal derselben Zahl, `exit=0`. ## Schritt 6 — Rotlauf im Baum ``` PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/linkcheck/faelle_waisen.py --rotlauf; echo "exit=$?" ``` Erwartet: je Mutant eine Zeile `GETOETET`, keine Zeile `UEBERLEBT` oder `MUSTER`, Schlusszeile `Rotlauf: alle Mutanten getoetet.`, `exit=0`. ## Schritt 7 — Messung M1 über den echten Vault ``` PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/linkcheck/faelle_waisen.py --echt; echo "exit=$?" ``` Erwartet: die Zeilen `Notizen im Graphen:` und `Waisen außerhalb der Ausnahmen:` mit Zahlen, keine Zeile `Filter übersprungen`, eine Zeile `exit=1 Laufzeit ... s` und `exit=0` am Ende. Steht dort `LAUFZEIT UEBER ZIEL`: nichts ändern, melden. ## Schritt 8 — Vergleich vorher und nachher am echten Vault ``` PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/linkcheck/waisen.py --vault D:/Claude --liste > D:/Claude-waisen-protokoll/nachbesserung-06/nachher.txt; echo "exit=$?"; diff D:/Claude-waisen-protokoll/nachbesserung-06/vorher.txt D:/Claude-waisen-protokoll/nachbesserung-06/nachher.txt; echo "diff exit=$?" ``` Erwartet: `exit=1` und `diff exit=0` ohne Differenzzeilen; die Gegenprobe des Prüfers fand 0 abweichende Linkziele. Zeigt `diff` Zeilen: nichts ändern. Melde jede Zeile und prüfe für jeden genannten Pfad mit `ls -l --time-style=full-iso "D:/Claude/"`, ob die Datei nach Schritt 1 angelegt oder geändert wurde. Eine andere Sitzung kann in der Zwischenzeit geschrieben haben; das entscheidet der Koordinator. ## Schritt 9 — Nachbarn bleiben grün ``` python D:/Claude/.vault-tools/linkcheck/faelle.py > /dev/null; echo "faelle exit=$?"; PYTHONIOENCODING=utf-8 python D:/Claude/.vault-tools/session-hooks/faelle_sitzungsstart.py > /dev/null; echo "faelle_sitzungsstart exit=$?" ``` Erwartet: `faelle exit=0` und `faelle_sitzungsstart exit=0`. Endet einer anders, denselben Befehl ohne `> /dev/null` wiederholen und die Ausgabe melden. ## Bericht Melde die vollständige Ausgabe von Schritt 1, 2, 4, 6, 7, 8 und 9. Von Schritt 3 und 5 die erste Zeile, die Schlusszeile und jede Zeile, die nicht mit `BESTANDEN` beginnt, samt den Problemzeilen darunter. Läuft etwas anders als erwartet: nichts anpassen, melden. Den Ordner `D:/Claude-waisen-protokoll/nachbesserung-06/` lässt du liegen.