- .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]>
11 KiB
Entwurf: Waisen-Zähler
Angelegt am 2026-09-29 als Entwurfsschritt nach AI/Regelwerk/pruefwerkzeuge, Abschnitt 1, vor der ersten Codezeile. Anforderung: Spec Waisen im Graphen vom 2026-09-29 (liegt in der Inbox, nicht veröffentlicht). Plan: docs/plans/2026-09-29-waisen/ in diesem Ordner.
Anlass
Am 2026-09-29 hatten laut Messung der Spec 852 von 1.996 Notizen keine Kante im Graphen, 664 davon Plandateien. Das Messskript lag nur im Scratchpad. waisen.py macht die Messung wiederholbar, und der Sitzungsstart zeigt die Zahl, damit neue Waisen auffallen.
Entscheidungsliste
| # | Frage | Antwort | Herkunft |
|---|---|---|---|
| 1 | Was ist ein Knoten? | Jede Datei mit Endung .md, Groß- und Kleinschreibung der Endung egal. Anhänge sind keine Knoten. |
Projektvorgabe: Spec, Befund: Anhänge zeigt der Graph nicht |
| 2 | Punktordner und Punktdateien? | Weder Knoten noch Quelle einer Kante. | Projektvorgabe: Spec, Befund |
| 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 |
| 5 | Ordner vorab überspringen? | Ja, wenn ein Filter auf den Ordnerpfad mit Schrägstrich passt. Sonst läuft der Zähler durch jedes node_modules. |
Projektvorgabe, Lücke für Ausdrücke mit Endanker benannt |
| 6 | Was ist ein Wikilink? | [[Ziel]], [[Ziel|Alias]], [[Ziel#Abschnitt]], [[Ziel^Block]], Einbettung mit !. Ein Backslash am Ende des Ziels fällt weg (escaptes Pipe in Tabellen). |
Quelle: _WIKILINK_RE, vaultlib.py Zeile 61; Backslash: linkcheck.py, wikilinks() |
| 7 | Wikilink im Frontmatter? | Zählt, jede Fundstelle im Frontmatter-Text, egal ob in Anführungszeichen. | Projektvorgabe: Spec. Ob Obsidian das so zählt: Prüfschritt P2 |
| 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 |
| 9 | Markdown-Link im Frontmatter? | Zählt nicht. | Projektvorgabe |
| 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 |
| 13 | Code? | Zäune und Code-Spans blendet strip_code() aus. |
Quelle: ENTWURF-codeblock-erkennung.md |
| 14 | Kommentare mit %% oder als HTML? |
Nicht ausgeblendet. | Lücke, benannt |
| 15 | Selbstreferenz, Link ins Leere, Link auf ausgeblendete Notiz? | Keine Kante. | Projektvorgabe |
| 16 | Was ist eine Waise? | Ein Knoten ohne ausgehende und ohne eingehende Kante. | Projektvorgabe: Spec |
| 17 | Ausnahmen? | Positivliste waisen-ausnahmen.txt, ein Muster je Zeile relativ zur Wurzel (* ohne Schrägstrich, ** über Ordner, ? ein Zeichen), Groß- und Kleinschreibung egal. Ausgenommene Notizen zählen nicht als Waisen, bleiben aber Knoten und Quelle von Kanten. Ein Muster ohne Treffer wird genannt. |
Projektvorgabe: Spec, Positivliste beim Zähler, keine Kundennamen |
| 18 | Ausnahmedatei fehlt, unlesbar, Muster ungültig oder doppelt? | Befund, Exit 2, keine Zahl. | Projektvorgabe: pruefwerkzeuge.md Abschnitt 6 |
| 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 |
| 20 | Datei oder Ordner nicht lesbar? | Befund, Exit 2, keine Zahl, weil eine Zahl ohne diese Dateien falsch wäre. | Projektvorgabe: pruefwerkzeuge.md Abschnitte 4 und 6 |
| 21 | Ausgabe und Exit? | Kopf, Zahlen und die Zeile Waisen außerhalb der Ausnahmen: N; --liste dazu die Pfade; --zahl nur N. Exit 0 ohne, 1 mit Waisen, 2 bei Abbruch. |
Projektvorgabe |
| 22 | Laufzeit? | Unter 10 s über den Vault, gemessen mit faelle_waisen.py --echt. Im Sitzungsstart bricht der Hook nach 15 s ab. |
Projektvorgabe: Hook-Budget 30 s in .claude/settings.json |
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,getLinkpathDestund die Vergleichsfunktion nach Pfadlänge, mit dergetLinkpathDestsortiert. - Gelesen am 2026-09-30 mit
grep -aim Programmabbildobsidian-1.13.7.asarunter%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 vonwaisen.pyin 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
Die einzelnen Fälle stehen an genau einer Stelle, in faelle_waisen.py. Hier steht, welche Klassen aus pruefwerkzeuge.md Abschnitt 3 abgedeckt sind.
| Klasse | Fälle |
|---|---|
| fehlt | .obsidian, app.json, Schlüssel userIgnoreFilters, Ausnahmedatei, linkcheck.py, Vault-Ordner, Schlusszeile des Frontmatters |
| null | userIgnoreFilters ist null |
| falscher Typ | app.json als Liste, Filtereintrag als Zahl, app.json ohne JSON |
| 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 |
| zusätzlich | Punktordner, Punktdatei, andere Endung, Endung in Großbuchstaben, per Dateifilter ausgeblendete Notiz als Knoten und als Quelle |
| 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 |
| Verweis ins Leere | Wikilink ins Leere, auf Anhang, auf ausgeblendete Notiz, Selbstreferenz, Anker allein |
| Reihenfolge | Waise an erster und letzter Stelle, Muster in erster und letzter Zeile, Filter an erster und letzter Stelle, Link am Dateiende ohne Zeilenumbruch |
| Umgebung | CRLF, BOM, ungültiges UTF-8 in Notiz und Ausnahmedatei, Umlaut und Emoji im Namen ohne PYTHONIOENCODING |
| Code | Wikilink im Zaun, Markdown-Link im Code-Span, Link nach dem Zaun, Zaun ohne Ende |
| 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.
Absturzfestigkeit
Der Hauptablauf läuft in try. Ein erwarteter Befund (Abbruch) und ein unerwarteter Fehler enden beide mit Exit 2 und einer Zeile Waisen - ABGEBROCHEN:, ein unerwarteter Fehler ohne --zahl zusätzlich mit Traceback. Eine Teilzahl gibt es bewusst nicht: Der Zähler sammelt keine Einzelbefunde, die man trotz Abbruch zeigen könnte, und eine Zahl über einen Teil des Vaults wäre falsch.
Rotlauf
faelle_waisen.py --rotlauf baut jeden Mutanten aus MUTANTEN in eine Kopie von waisen.py und fährt die genannten Fälle dagegen. Jeder Mutant entfernt genau eine Entscheidung aus der Liste oben. Ein Mutant, dessen Zeile nicht genau einmal in waisen.py steht, ist ein Befund, kein stilles Überspringen.
Abbruchbedingung
Schluss ist, wenn der Treiber mit grüner Grundlinie ohne Fehlschlag durchläuft, der Rotlauf jeden Mutanten tötet, --echt unter 10 s bleibt, der Koordinator zehn Pfade aus --liste in Obsidian geöffnet und ohne Kante gefunden hat und eine Prüfrunde nur noch Umformulierungen findet.
Was dieses Skript nicht ist
Ein Fangnetz. Es zählt nach nachgebauten Regeln. Wie der Graph aussieht, zeigt nur Obsidian selbst.