Files
claude-regelwerk/.vault-tools/linkcheck/docs/plans/2026-09-29-waisen/task-06-nachbesserung-obsidian-regeln.md
T
Pascal MichelandClaude Opus 5 eb48f8822d Automatischer Push: 45 Datei(en) geändert
- .claude/agents/planer.md
- .claude/agents/quorum-stimme.md
- .claude/skills/bookstack-upload/SKILL.md
- .claude/skills/bookstack-upload/scripts/bookstack/archiv.py
- .claude/skills/bookstack-upload/scripts/bookstack/auswahl.py
- .claude/skills/bookstack-upload/scripts/bookstack/beschreibung.py
- .claude/skills/bookstack-upload/scripts/bookstack/bilder.py
- .claude/skills/bookstack-upload/scripts/bookstack/ergebnis.py
- .claude/skills/bookstack-upload/scripts/bookstack/hochladen.py
- .claude/skills/bookstack-upload/scripts/bookstack/konvertierung.py
- .claude/skills/bookstack-upload/scripts/bookstack/struktur.py
- .claude/skills/bookstack-upload/scripts/bookstack/verweise.py
- … und 33 weitere

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-01 04:00:02 +02:00

45 KiB

type, title, status, summary, tags, created, updated, modell, modell_begruendung
type title status summary tags created updated modell modell_begruendung
plan-task Task 06 — Nachbesserung: userIgnoreFilters und Linkziele wie Obsidian 1.13.7 draft 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.
plan-task
vault-tools
2026-09-30 2026-09-30 haiku 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

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:

  * 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:

  * 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:

  * 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:

  * 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:

import os
import posixpath
import re

wird:

import os
import re

Namensverzeichnis ohne Pfadtabelle.

Aus:

@dataclass
class Index:
    pfade: dict = field(default_factory=dict)
    namen: dict = field(default_factory=dict)

wird:

@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:

    ohne_treffer: list
    filter_anzahl: int

wird:

    ohne_treffer: list
    filter_anzahl: int
    filter_uebersprungen: list

Filter nach Obsidian 1.13.7.

Aus:

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:

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:

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:

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:

    filter_liste = lade_filter(vault)

wird:

    filter_liste, filter_uebersprungen = lade_filter(vault)

Rückgabe von analysiere().

Aus:

    return Ergebnis(notizen, waisen, ausgenommen, ausgehend, muster, ohne_treffer, len(filter_liste))

wird:

    return Ergebnis(notizen, waisen, ausgenommen, ausgehend, muster, ohne_treffer, len(filter_liste),
                    filter_uebersprungen)

Ausgabe der übersprungenen Filter.

Aus:

    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:

    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:

        sys.stdout.reconfigure(encoding="utf-8")

wird:

        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:

    ohne_pythonioencoding: bool = False
    vault_fehlt: bool = False

wird:

    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:

    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:

    # 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:

    Fall("markdown    | Laufwerkspfad zaehlt nicht",

wird:

    Fall("markdown    | Laufwerkspfad findet keine Notiz",

Filter ohne Rücksicht auf die Schreibung.

Aus:

    Fall("ausblendung | regulaerer Ausdruck in der Tiefe",
         neu("P/node_modules/pkg/README.md", "# R\n"), ["C/allein.md"]),

wird:

    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:

    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:

    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:

    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:

    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:

    Fall("zahl        | Abbruch ohne Zahl", lambda v: shutil.rmtree(v / ".obsidian"), None,
         ["Waisen - ABGEBROCHEN"], zahl=True),
]

wird:

    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:

    code, out, err, dauer = fahre(ziel_skript, argumente, umgebung)
    probleme = pruefe_ausgabe(fall, code, out)

wird:

    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:

    ("regulaerer Filter als Praefix",
     "                liste.append((\"regex\", re.compile(eintrag[1:-1])))",
     "                liste.append((\"praefix\", eintrag))",
     ["ausblendung | regulaerer Ausdruck in der Tiefe"]),

wird:

    ("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:

    ("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:

    ("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:

| 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:

| 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:

| 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:

| 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:

| 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:

| 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:

| 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:

| 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:

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:

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:

| 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:

| 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:

| Wertebereich | Muster mit Backslash, führendem Schrägstrich, `..`, `.` als Pfadteil, leerem Pfadteil, Laufwerk; Stern über Ordnergrenze |

wird:

| 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:

| Absturz | Notiz nicht lesbar, Ordner nicht lesbar, unerwarteter Fehler (alle im Prozess) |

wird:

| 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/<pfad>", 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.