Regelwerk, Agenten, Skills und Werkzeuge veröffentlichen
Erster Stand des öffentlichen Ausschnitts aus dem Vault. Die .gitignore arbeitet als Positivliste: ausgeschlossen ist alles, eingeschlossen nur, was ausdrücklich wieder aufgenommen wird. Notizen, Projekte, Finanzen, das Sitzungsgedächtnis und settings.local.json bleiben damit außen vor. Der Kundenname in AI/Regelwerk/seo.md ist durch eine neutrale Bezeichnung ersetzt; die Regeln selbst sind unverändert. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
commit
2c9a4475fc
97 files changed
+14627
No files matched your search
@@ -0,0 +1,130 @@
|
||||
# Entwurf: Codeblock-Erkennung bei der Link-Extraktion
|
||||
|
||||
Angelegt 2026-09-03. Entwurfsschritt nach [[AI/Regelwerk/pruefwerkzeuge|pruefwerkzeuge.md]], Abschnitt 1 — geschrieben **vor** der ersten Codezeile.
|
||||
|
||||
## Anlass
|
||||
|
||||
`vault_doctor.py` meldete 92 tote Wikilinks. Die Stichprobe zeigt fast ausschließlich Fehlalarme:
|
||||
|
||||
- `[[ $EUID -eq 0 ]]` — Bash-Test aus einem Codeblock
|
||||
- `[[:space:]]` — POSIX-Zeichenklasse aus einem Regex
|
||||
- `[[bild.png]]`, `[[Ziel]]`, `[[Notiz]]` — Platzhalter in der Dokumentation des Bookstack-Upload-Skills, das Wikilinks verarbeitet
|
||||
- `[[1, 2]]`, `[[float]]`, `[[A]]` — Typangaben und Indizes aus Codebeispielen
|
||||
|
||||
Ursache: `_WIKILINK_RE` (Zeile 61 in `vaultlib.py`) läuft über den gesamten Text. Das Wort `fence` kommt in der Datei kein einziges Mal vor.
|
||||
|
||||
**Ein Prüfwerkzeug, dessen Befunde zu vier Fünfteln Rauschen sind, wird nicht mehr gelesen.** Das ist der eigentliche Schaden, nicht die 92.
|
||||
|
||||
## Entscheidungsliste
|
||||
|
||||
| # | Frage | Antwort | Herkunft |
|
||||
|---|---|---|---|
|
||||
| 1 | Was ist ein Wikilink? | `[[Ziel]]`, `[[Ziel\|Alias]]`, `[[Ziel#Überschrift]]`, `[[Ziel^Block]]` | **Quelle** — `_WIKILINK_RE`, `vaultlib.py` Zeile 61 |
|
||||
| 2 | Wo darf keiner erkannt werden? | In eingezäunten Codeblöcken (` ``` `, `~~~`) und in Code-Spans (Backticks) | **Quelle** — CommonMark: Inhalt von Code wird nicht als Markup interpretiert; Obsidian rendert dort keine Links |
|
||||
| 3 | Zählt das YAML-Frontmatter mit? | Nein, ist bereits abgetrennt | **Quelle** — `vaultlib.py` Zeile 117: `body = text[m.end():]` |
|
||||
| 4 | Eingerückte Codeblöcke (vier Leerzeichen)? | **Nicht behandelt.** Der Vault verwendet durchgängig eingezäunte Blöcke; eine Vier-Leerzeichen-Regel würde eingerückte Listenpunkte mit legitimen Links verschlucken | **Projektvorgabe**, als Lücke im Kopfkommentar benannt |
|
||||
| 5 | Ungeschlossener Zaun am Dateiende? | Gilt bis Dateiende als Code | **Projektvorgabe** — die konservative Wahl: lieber einen echten Link übersehen als einen Fehlalarm erzeugen |
|
||||
| 6 | Auch Überschriften betroffen? | **Ja, gleicher Fehler** — `extract_headings` liest `# Kommentar` aus Bash-Blöcken als Überschrift. **In dieser Runde nicht geändert**, weil es den Index-Inhalt verschiebt und einen eigenen Nachweis braucht | **Befund, benannt statt stillschweigend miterledigt** |
|
||||
| 7 | Welche Zeile schließt einen Zaun? | Nur eine Zeile aus demselben Zaunzeichen, mindestens so lang wie der Öffner, bis zu drei LEERZEICHEN eingerückt (kein Tab), danach nur Leerzeichen oder Tab. Eine Zeile mit Info-String (drei Backticks und `bash`) im Block ist Inhalt | **Quelle** — Fundstellen im Abschnitt „Nachbesserung 2026-09-13" und „Nachbesserung Runde 2" |
|
||||
|
||||
Keine Zeile bleibt auf „Annahme" stehen.
|
||||
|
||||
## Angriffsliste
|
||||
|
||||
Je Fall ein eigener Durchlauf, Grundlinie zuerst.
|
||||
|
||||
| # | Fall | Erwartung |
|
||||
|---|---|---|
|
||||
| 1 | leerer Text | keine Links, kein Absturz |
|
||||
| 2 | nur Fließtext mit einem Link | Link bleibt |
|
||||
| 3 | Link **vor** und **nach** einem Zaun | beide bleiben — *Reihenfolge: erste und letzte Position* |
|
||||
| 4 | ` ```bash ` mit Link darin | Link weg |
|
||||
| 5 | `~~~` als Zaunzeichen | Link weg |
|
||||
| 6 | Zaun ohne Abschluss, Link danach | Link weg |
|
||||
| 7 | Inline-Code `` `[[x]]` `` | Link weg |
|
||||
| 8 | doppelte Backticks | Link weg |
|
||||
| 9 | einzelner Backtick ohne Partner | Link bleibt — darf nicht den Rest verschlucken |
|
||||
| 10 | vier Backticks als Zaun | Link weg |
|
||||
| 11 | eingerückter Zaun in einer Liste | Link weg |
|
||||
| 12 | Link unmittelbar an der Zaunzeile | bleibt |
|
||||
| 13 | echter Bestandsfall `[[ $EUID -eq 0 ]]` in einem Block | kein Link |
|
||||
| 14 | echter Bestandsfall `[[:space:]]` in Inline-Code | kein Link |
|
||||
| 15 | escaptes Pipe in einer Tabellenzelle `[[Ziel\|Alias]]` | Ziel ohne Backslash |
|
||||
| 16 | escaptes Pipe ohne Alias | Ziel ohne Backslash |
|
||||
| 17 | Dreier-Zaun mit Info-String innerhalb eines Vierer-Zauns, darin `[[:space:]]` | kein Link aus dem Block, Link danach bleibt |
|
||||
| 18 | Dreier-Schließzaun in einem Vierer-Zaun | schließt nicht |
|
||||
| 19 | Vierer-Zaun schließt Vierer-Zaun | schließt, Link danach bleibt |
|
||||
| 20 | Zeile mit drei Backticks und `bash` in einem Dreier-Block | schließt nicht |
|
||||
| 21 | Schließzaun mit Leerzeichen und Tab dahinter | schließt — neuer Ausdruck, eigener Fall |
|
||||
| 22 | Schließzaun mit CR am Zeilenende | schließt — neuer Ausdruck, eigener Fall |
|
||||
| 23 | anderes Zaunzeichen (Backtick-Öffner, Tilde-Schließzeile) | schließt nicht — eigener Fall für „gleiches Zaunzeichen" |
|
||||
| 24 | Schließzaun mit vier Leerzeichen davor | schließt nicht — Einrückungsgrenze |
|
||||
| 25 | Schließzaun mit Tab davor | schließt nicht — Tab zählt nicht als Einrückung, siehe „Nachbesserung Runde 2" |
|
||||
|
||||
## Nachbesserungen aus der ersten Messung
|
||||
|
||||
Der Lauf über den Vault brachte nach dem Codeblock-Filter noch 18 Befunde — wieder Fehlalarme, zwei neuer Klassen. Beide bekamen nach Abschnitt 5 des Regelwerks ihren eigenen Fall:
|
||||
|
||||
- **Escaptes Pipe.** In Obsidian-Tabellenzellen muss das Pipe eines Alias als `\|` geschrieben werden. `_WIKILINK_RE` schneidet am Pipe, lässt den Backslash aber stehen — das Ziel hieß dann `Pfad\` und fand nichts. Fälle 15 und 16.
|
||||
- **Bildeinbettungen.** `![[Pasted image 123.png]]` ist ein gültiger Link auf eine Nicht-Markdown-Datei. Die Nachschlagemenge von `vault_doctor.py` enthält nur `.md`. Behoben durch eine zweite Menge aus allen übrigen Dateien.
|
||||
|
||||
Ergebnis: **92 gemeldete, 0 echte tote Links.**
|
||||
|
||||
## Nachbesserung 2026-09-13: Schließzaun nach CommonMark
|
||||
|
||||
**Anlass.** Der Linkcheck meldete am 2026-09-13 einen toten Wikilink `[[:space:]]` in `Code Projekte/active - Logging/docs/plans/2026-09-08-a-betriebsmodelle/task-09-gate.md`. Die Datei öffnet in Zeile 356 einen Block mit vier Backticks und zeigt darin ab Zeile 406 ein eingerücktes Beispiel mit drei Backticks und `bash`. Die alte Regel schloss den äußeren Block schon an Zeile 406, weil jede Zaunzeile aus demselben Zeichen genügte. Zeile 407 mit der POSIX-Zeichenklasse lag damit im Fließtext.
|
||||
|
||||
**Regel.** Ein Block schließt nur mit einer Zeile, die
|
||||
|
||||
- aus demselben Zaunzeichen besteht wie der Öffner (Backtick oder Tilde),
|
||||
- mindestens so viele Zeichen hat wie der Öffner,
|
||||
- nach den Zaunzeichen nur Leerzeichen oder Tab trägt, also keinen Info-String.
|
||||
|
||||
Die Einrückung von bis zu drei Zeichen übernimmt der Schließzaun vom Öffner (`\s{0,3}`). Ein CR am Zeilenende ist zugelassen, damit eine Eingabe mit CRLF nicht an jedem Schließzaun vorbeiläuft.
|
||||
|
||||
**Herkunft.**
|
||||
|
||||
| Aussage | Fundstelle |
|
||||
|---|---|
|
||||
| dasselbe Zeichen | markdown-it-py 4.2.0, `markdown_it/rules_block/fence.py` Zeile 106 |
|
||||
| mindestens so lang | ebenda Zeilen 42–43, `closing_len >= opening_len` |
|
||||
| danach nur Leerraum | ebenda Zeilen 119–123; Leerraum heißt Leerzeichen oder Tab laut `markdown_it/common/utils.py` Zeilen 167–169 |
|
||||
|
||||
Gelesen am 2026-09-13 unter `Code Projekte/active - Agentic Cockpit/.venv/Lib/site-packages/markdown_it/`. Die Bibliothek nennt ihre Regel selbst CommonMark-konform (`fence.py` Zeile 145). Referenz der Regel ist die CommonMark-Spezifikation 0.31.2, Abschnitt 4.5 Fenced code blocks, https://spec.commonmark.org/0.31.2/#fenced-code-blocks. **Den Wortlaut der Spezifikation hat beim Planen niemand gezogen, der Planlauf hatte keinen Netzzugriff. Belegt ist die Regel an der Implementierung. Der Abgleich mit dem Spezifikationstext ist offen.**
|
||||
|
||||
**Benannte Lücke.** Ein Öffner mit Backtick im Info-String ist nach markdown-it-py kein Zaun (`fence.py` Zeilen 79–80). Hier gilt er weiter als Zaun. Diese Runde ändert nur den Schließzaun. Die Öffnerregel verschiebt, welche Zeilen überhaupt als Code gelten, und braucht einen eigenen Lauf über den Vault.
|
||||
|
||||
**Was schiefgehen kann.** Hat bisher eine Zeile mit Info-String einen Block versehentlich geschlossen, läuft der Block jetzt weiter, notfalls bis zum Dateiende. Wikilinks dahinter prüft der Linkcheck dann nicht mehr. Deshalb gehört zur Abnahme ein Vergleichslauf alt gegen neu über den ganzen Vault.
|
||||
|
||||
## Nachbesserung Runde 2: gleiches Zaunzeichen und Einrückung ohne Tab
|
||||
|
||||
**Anlass.** Die erste Prüfrunde nach der Nachbesserung 2026-09-13 fand zwei Lücken in der Angriffsliste, keine im Verhalten der zwei damals geänderten Bedingungen — aber beide Bedingungen hatten bis dahin keinen eigenen Fall:
|
||||
|
||||
- Die Bedingung „gleiches Zaunzeichen" (`s.group(1)[0] == zaun[0]`) hatte keinen Fall, der einen Backtick-Öffner gegen eine Tilde-Schließzeile prüft. Fall 23.
|
||||
- Die Einrückung vor dem Schließzaun ließ `\s{0,3}` zu, also auch einen Tab. Ein Tab zählt bei markdown-it-py als vier Spalten (`state_block.py` Zeilen 79–80 für die Tab-Rechnung, 257–261 für den Vergleich `>= 4`) und macht die Zeile zu eingerücktem Code — sie schließt dort nicht. Fall 25 belegt das: Gegen die alte Fassung mit `\s{0,3}` schlägt er fehl (rot), gegen `[ ]{0,3}` besteht er.
|
||||
|
||||
**Korrektur.** `_SCHLUSS_RE` in `linkcheck.py` verlangt jetzt `^[ ]{0,3}` statt `^\s{0,3}` vor dem Zaunzeichen — nur Leerzeichen, kein Tab. Die Bedingung „gleiches Zaunzeichen" selbst war schon vorher im Code (`s.group(1)[0] == zaun[0]`), nur ungeprüft; Fall 23 sichert sie jetzt ab, Fall 24 die Grenze von drei Einrückungszeichen.
|
||||
|
||||
**Herkunft.** Wie in der Nachbesserung 2026-09-13, `markdown_it/rules_block/fence.py` und zusätzlich `markdown_it/rules_block/state_block.py` Zeilen 257–261 für die Tab-als-vier-Spalten-Regel, gelesen unter `Code Projekte/active - Agentic Cockpit/.venv/Lib/site-packages/markdown_it/` am 2026-09-13.
|
||||
|
||||
## Nachbesserung nach Prüfrunde 2: Schließregel wieder symmetrisch
|
||||
|
||||
**Anlass.** Die zweite Prüfrunde hat gezeigt, dass `^[ ]{0,3}` vor dem Schließzaun eine Rückentwicklung war. In einem Listenpunkt, der mit Tab eingerückt ist, öffnet `linkcheck.py` den Block an der Tab-Zeile (die Öffnerregel erlaubt `\s{0,3}`), schließt ihn aber nie mehr, und alles bis zum Dateiende gilt als Code. markdown-it-py zieht dort die Einrückung des Listeninhalts ab und schließt den Block. Belegt mit markdown-it-py 4.2.0 (`MarkdownIt('commonmark')`) gegen beide Fassungen.
|
||||
|
||||
**Korrektur.** `_SCHLUSS_RE` verlangt wieder `^\s{0,3}`, symmetrisch zum Öffner. Fall 25 erwartet jetzt, dass ein Tab-Zaun schließt; Fall 26 prüft den Listenfall.
|
||||
|
||||
**Benannte Lücke.** Auf oberster Ebene zählt markdown-it-py den Tab als vier Spalten und liest die Zeile als eingerückten Code; dort schließt ein Tab-Zaun nicht, `linkcheck.py` schließt ihn trotzdem. Listen-Einrückung bildet das Werkzeug nicht nach, deshalb bleibt es bei der einfachen, symmetrischen Regel. Im Vault gab es am 2026-09-13 keine Datei, die das betrifft (Messung des Prüfers).
|
||||
|
||||
**Die Regelzeile oben** („Die Einrückung von bis zu drei Zeichen übernimmt der Schließzaun vom Öffner") gilt damit wieder uneingeschränkt.
|
||||
|
||||
## Absturzfestigkeit
|
||||
|
||||
`extract_links` wird von `build_index.py` über den ganzen Vault gerufen. Ein Absturz legt den Index still. Die Hilfsfunktion arbeitet deshalb rein zeilenweise über Zeichenketten, ohne Indexarithmetik über Zeilengrenzen, und ersetzt Codeinhalt durch Leerzeichen statt ihn zu löschen — Länge und Zeilenzahl bleiben erhalten, nichts kann über eine entfernte Stelle hinweg zusammenwachsen.
|
||||
|
||||
## Abbruchbedingung
|
||||
|
||||
Schluss ist, wenn ein Lauf über den Vault die Fehlalarme aus der Stichprobe nicht mehr meldet **und** alle Angriffsfälle aus `faelle.py` grün sind. Was danach an toten Links übrig bleibt, ist eine Aufräumliste — kein Prüfauftrag mehr.
|
||||
|
||||
## Was dieses Skript nicht ist
|
||||
|
||||
Ein Fangnetz gegen offensichtliche Fehler. Es belegt nicht, dass alle Wikilinks im Vault stimmen — nur, dass die gemeldeten nicht aus Codeblöcken stammen.
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Angriffsfaelle fuer strip_code() aus linkcheck.py.
|
||||
|
||||
Vorgehen nach AI/Regelwerk/pruefwerkzeuge.md Abschnitt 3: je Fall ein
|
||||
eigener Durchlauf, Grundlinie zuerst. Faellt die Grundlinie, misst der
|
||||
Rest nichts.
|
||||
|
||||
Der Fall "Reihenfolge" (Link an erster UND letzter Position) ist Nr. 3 -
|
||||
laut Regelwerk der Fall, dessen Fehlen am teuersten war.
|
||||
|
||||
Aufruf: python .vault-tools/linkcheck/faelle.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from linkcheck import strip_code, wikilinks # noqa: E402
|
||||
|
||||
_LINK = re.compile(r"\[\[([^\]\|#\^]+)")
|
||||
|
||||
|
||||
class _V:
|
||||
"""Minimaler Ersatz fuer vaultlib in den Faellen."""
|
||||
_WIKILINK_RE = _LINK
|
||||
|
||||
|
||||
def links(text):
|
||||
return wikilinks(_V, text)
|
||||
|
||||
|
||||
# (Nummer, Name, Text, erwartete Links)
|
||||
FAELLE = [
|
||||
(0, "GRUNDLINIE: einfacher Link", "Siehe [[Zielnotiz]] dazu.", ["Zielnotiz"]),
|
||||
(1, "leerer Text", "", []),
|
||||
(2, "nur Fliesstext mit Link", "Ein [[A/B]] mitten im Satz.", ["A/B"]),
|
||||
(3, "Reihenfolge: Link vor UND nach einem Zaun",
|
||||
"[[Vorher]]\n\n```bash\n[[ $EUID -eq 0 ]]\n```\n\n[[Nachher]]",
|
||||
["Vorher", "Nachher"]),
|
||||
(4, "Zaun mit Sprachangabe", "```bash\nif [[ -f x ]]; then :; fi\n```", []),
|
||||
(5, "Tilde-Zaun", "~~~\n[[drin]]\n~~~", []),
|
||||
(6, "Zaun ohne Abschluss", "Text\n```\n[[drin]]\nnoch drin", []),
|
||||
(7, "Inline-Code", "Der Ausdruck `[[:space:]]` trifft Leerzeichen.", []),
|
||||
(8, "doppelte Backticks", "Siehe ``[[roh]]`` im Text.", []),
|
||||
(9, "einzelner Backtick ohne Partner",
|
||||
"Ein ` einzelner Backtick und danach [[Echt]].", ["Echt"]),
|
||||
(10, "vier Backticks als Zaun", "````\n[[drin]]\n````", []),
|
||||
(11, "eingerueckter Zaun in einer Liste",
|
||||
"- Punkt:\n ```\n [[drin]]\n ```\n- [[Danach]]", ["Danach"]),
|
||||
(12, "Link unmittelbar an der Zaunzeile",
|
||||
"[[Davor]]\n```\ncode\n```\n[[Danach]]", ["Davor", "Danach"]),
|
||||
(13, "Bestandsfall EUID im Block",
|
||||
"```bash\nif [[ $EUID -eq 0 ]]; then exit 1; fi\n```", []),
|
||||
(14, "Bestandsfall Zeichenklasse inline",
|
||||
"Mit `grep -o '[[:space:]]'` filtern.", []),
|
||||
(15, "escaptes Pipe in einer Tabellenzelle",
|
||||
r"| Datei | [[Pfad/Zur/Notiz\|Alias]] | Rest |", ["Pfad/Zur/Notiz"]),
|
||||
(16, "escaptes Pipe am Zeilenende ohne Alias",
|
||||
r"Siehe [[Ziel\|]] dort.", ["Ziel"]),
|
||||
(17, "Dreier-Zaun mit Info-String in Vierer-Zaun",
|
||||
"````markdown\n```bash\ngrep '[[:space:]]' datei\n```\n````\n[[Danach]]",
|
||||
["Danach"]),
|
||||
(18, "Dreier-Schliesszaun schliesst Vierer nicht",
|
||||
"````\n```\n[[drin]]\n````\n[[Danach]]", ["Danach"]),
|
||||
(19, "Vierer-Zaun schliesst Vierer-Zaun",
|
||||
"````\n[[drin]]\n````\n[[Danach]]", ["Danach"]),
|
||||
(20, "```bash in einem ```-Block schliesst nicht",
|
||||
"```\n```bash\n[[drin]]\n```\n[[Danach]]", ["Danach"]),
|
||||
(21, "Schliesszaun mit Leerraum dahinter",
|
||||
"```\n[[drin]]\n``` \t\n[[Danach]]", ["Danach"]),
|
||||
(22, "Schliesszaun mit CR am Zeilenende",
|
||||
"```\r\n[[drin]]\r\n```\r\n[[Danach]]\r\n", ["Danach"]),
|
||||
(23, "anderes Zaunzeichen schliesst nicht (Backtick vs. Tilde)",
|
||||
"```\n[[drin]]\n~~~\n[[Danach]]", []),
|
||||
(24, "Schliesszaun mit vier Leerzeichen davor schliesst nicht",
|
||||
"```\n[[drin]]\n ```\n[[Danach]]", []),
|
||||
(25, "Schliesszaun mit Tab davor schliesst (symmetrische Regel, benannte Luecke oberste Ebene)",
|
||||
"```\n[[drin]]\n\t```\n[[Danach]]", ["Danach"]),
|
||||
(26, "Liste: mit Tab eingerueckter Block schliesst mit Tab-Zaun",
|
||||
"- Punkt:\n\t```\n\t[[drin]]\n\t```\n- [[Danach]]", ["Danach"]),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
grundlinie = FAELLE[0]
|
||||
ist = links(grundlinie[2])
|
||||
if ist != grundlinie[3]:
|
||||
sys.stdout.write("GRUNDLINIE FAELLT: erwartet %r, bekommen %r\n"
|
||||
% (grundlinie[3], ist))
|
||||
sys.stdout.write("Der Rest der Faelle misst damit nichts. Abbruch.\n")
|
||||
return 2
|
||||
|
||||
fehler = 0
|
||||
for nr, name, text, erwartet in FAELLE:
|
||||
try:
|
||||
ist = links(text)
|
||||
except Exception as exc: # ein Absturz ist ein Befund
|
||||
sys.stdout.write(" FALL %-2d ABSTURZ %s -> %s\n" % (nr, name, exc))
|
||||
fehler += 1
|
||||
continue
|
||||
ok = ist == erwartet
|
||||
if not ok:
|
||||
fehler += 1
|
||||
sys.stdout.write(" FALL %-2d %-7s %-42s erwartet %r, bekommen %r\n"
|
||||
% (nr, "ok" if ok else "FEHLER", name, erwartet, ist))
|
||||
|
||||
gepruefte = len(FAELLE)
|
||||
sys.stdout.write("\n%d von %d Faellen bestanden.\n" % (gepruefte - fehler, gepruefte))
|
||||
return 1 if fehler else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,236 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Findet tote Wikilinks im Vault - ohne die Fehlalarme aus Codebloecken.
|
||||
|
||||
WARUM ES DIESES WERKZEUG GIBT
|
||||
-----------------------------
|
||||
`vault_doctor.py` aus dem mitgelieferten Navigator-Toolkit meldete am
|
||||
2026-09-03 92 tote Wikilinks. Die Stichprobe bestand fast ausschliesslich
|
||||
aus Rauschen: `[[ $EUID -eq 0 ]]` (Bash-Test), `[[:space:]]` (POSIX-
|
||||
Zeichenklasse), `[[bild.png]]` und `[[Ziel]]` (Platzhalter in der Doku des
|
||||
Bookstack-Upload-Skills). Ursache ist `_WIKILINK_RE` in `vaultlib.py`
|
||||
Zeile 61: der Ausdruck laeuft ueber den gesamten Text, das Wort "fence"
|
||||
kommt in der Datei kein einziges Mal vor.
|
||||
|
||||
Das Navigator-Toolkit ist schreibgeschuetzt und stammt aus einem fremden
|
||||
Skill (siehe AI/Regelwerk/navigation.md). Ein Patch dort waere beim
|
||||
naechsten Update verloren. Deshalb liegt die Korrektur hier daneben.
|
||||
|
||||
HERKUNFT DER REGELN
|
||||
-------------------
|
||||
- Wikilink-Syntax: QUELLE - `_WIKILINK_RE`, vaultlib.py Zeile 61.
|
||||
- Zielaufloesung (lookup-Menge und Treffer-Bedingung): QUELLE - woertlich
|
||||
uebernommen aus vault_doctor.py Zeilen 66-74 und 102-107.
|
||||
- Codebloecke und Code-Spans zaehlen nicht: QUELLE - CommonMark; Obsidian
|
||||
rendert dort keine Links.
|
||||
- Frontmatter zaehlt nicht mit: QUELLE - vaultlib.py Zeile 117 trennt es ab.
|
||||
- Ungeschlossener Zaun gilt bis Dateiende: PROJEKTVORGABE - die
|
||||
konservative Wahl, lieber einen echten Link uebersehen als Rauschen
|
||||
erzeugen.
|
||||
- Schliesszaun: QUELLE - dasselbe Zeichen wie der Oeffner, mindestens so
|
||||
lang, danach nur Leerzeichen oder Tab. Abgeschrieben aus markdown-it-py
|
||||
4.2.0, rules_block/fence.py Zeilen 42-43, 106 und 119-123; Referenz ist
|
||||
CommonMark 0.31.2, Abschnitt 4.5 Fenced code blocks. Fundstellen und der
|
||||
offene Abgleich mit dem Spezifikationstext: ENTWURF-codeblock-erkennung.md.
|
||||
- Einrueckung vor dem Schliesszaun: wie beim Oeffner hoechstens drei
|
||||
Leerraumzeichen, ein Tab eingeschlossen. Die Regel ist bewusst symmetrisch
|
||||
(Entscheidung nach Pruefrunde 2): Eine strengere Schliessregel liess einen
|
||||
in einer Liste mit Tab eingerueckten Block bis zum Dateiende offen, dort
|
||||
schliesst markdown-it-py ihn aber.
|
||||
|
||||
BEWUSSTE LUECKEN - nicht fuer erledigt halten
|
||||
---------------------------------------------
|
||||
- Eingerueckte Codebloecke (vier Leerzeichen) werden NICHT erkannt. Sie
|
||||
wuerden eingerueckte Listenpunkte mit echten Links verschlucken.
|
||||
- Ein Tab vor dem Schliesszaun auf oberster Ebene schliesst hier den Block.
|
||||
markdown-it-py zaehlt den Tab dort als vier Spalten und liest die Zeile als
|
||||
eingerueckten Code (state_block.py Zeilen 79-80 und 257-261). In Listen
|
||||
schliesst er auch dort. Listen-Einrueckung bildet dieses Werkzeug nicht nach.
|
||||
- Ein Oeffner mit Backtick im Info-String gilt hier als Zaun. CommonMark
|
||||
liest so eine Zeile als Inline-Code. Nicht behandelt.
|
||||
- `extract_headings()` im Toolkit hat denselben Fehler: `# Kommentar` aus
|
||||
einem Bash-Block zaehlt dort als Ueberschrift. Hier nicht behandelt.
|
||||
- Dieses Werkzeug ist ein Fangnetz gegen offensichtliche Fehler. Es belegt
|
||||
nicht, dass alle Wikilinks stimmen - nur, dass die gemeldeten nicht aus
|
||||
Codebloecken stammen.
|
||||
|
||||
Entwurf und Angriffsliste: ENTWURF-codeblock-erkennung.md im selben Ordner.
|
||||
|
||||
Aufruf:
|
||||
python .vault-tools/linkcheck/linkcheck.py --vault .
|
||||
python .vault-tools/linkcheck/linkcheck.py --vault . --alle
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
SKRIPT = os.path.dirname(os.path.abspath(__file__))
|
||||
TOOLKIT = os.path.join(os.path.dirname(SKRIPT), "obsidian-vault-navigator", "scripts")
|
||||
|
||||
_FENCE_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})")
|
||||
# Schliesszaun: dieselbe Einrueckung wie beim Oeffner (symmetrisch, siehe
|
||||
# Kopfkommentar), danach nur Leerzeichen, Tab und ein CR. Eine Zeile mit
|
||||
# Info-String im Block ist deshalb Inhalt, kein Ende.
|
||||
_SCHLUSS_RE = re.compile(r"^\s{0,3}(`{3,}|~{3,})[ \t]*\r?$")
|
||||
_INLINE_CODE_RE = re.compile(r"(`+)(?:(?!\1).)*?\1")
|
||||
|
||||
|
||||
def strip_code(body):
|
||||
"""Ersetzt Codebloecke und Code-Spans durch Leerzeichen.
|
||||
|
||||
Laenge und Zeilenzahl bleiben erhalten (Ersetzen statt Loeschen), damit
|
||||
nichts ueber eine entfernte Stelle hinweg zusammenwaechst.
|
||||
"""
|
||||
if not body:
|
||||
return ""
|
||||
zaun = None
|
||||
aus = []
|
||||
for zeile in body.split("\n"):
|
||||
m = _FENCE_RE.match(zeile)
|
||||
if zaun is None:
|
||||
if m:
|
||||
zaun = m.group(1)
|
||||
aus.append(" " * len(zeile))
|
||||
continue
|
||||
aus.append(_INLINE_CODE_RE.sub(lambda t: " " * len(t.group(0)), zeile))
|
||||
else:
|
||||
s = _SCHLUSS_RE.match(zeile)
|
||||
# Gleiches Zeichen und mindestens so lang wie der Oeffner: ein
|
||||
# Dreier-Zaun schliesst keinen Vierer-Zaun.
|
||||
if s and s.group(1)[0] == zaun[0] and len(s.group(1)) >= len(zaun):
|
||||
zaun = None
|
||||
aus.append(" " * len(zeile))
|
||||
return "\n".join(aus)
|
||||
|
||||
|
||||
def lade_toolkit():
|
||||
"""Importiert vaultlib aus dem schreibgeschuetzten Toolkit.
|
||||
|
||||
Fehlt es, ist das ein BEFUND und kein Grund zu schweigen.
|
||||
"""
|
||||
if TOOLKIT not in sys.path:
|
||||
sys.path.insert(0, TOOLKIT)
|
||||
try:
|
||||
import vaultlib as V
|
||||
return V
|
||||
except ImportError as exc:
|
||||
raise RuntimeError(
|
||||
"vaultlib.py nicht gefunden oder nicht importierbar unter %s (%s). "
|
||||
"Ohne das Toolkit kann diese Pruefung nicht arbeiten." % (TOOLKIT, exc))
|
||||
|
||||
|
||||
def sammle(V, vault):
|
||||
"""Notizen und Anhaenge getrennt einsammeln.
|
||||
|
||||
Anhaenge braucht es, weil `![[Pasted image 123.png]]` ein gueltiger
|
||||
Obsidian-Link auf eine Nicht-Markdown-Datei ist. vault_doctor.py baut
|
||||
seine lookup-Menge nur aus .md-Dateien und meldet solche Einbettungen
|
||||
deshalb als tot.
|
||||
"""
|
||||
notizen, anhaenge = [], set()
|
||||
for ap, rp in V.walk_vault(vault):
|
||||
if not rp.endswith(".md"):
|
||||
anhaenge |= {rp, os.path.basename(rp)}
|
||||
continue
|
||||
with open(ap, "r", encoding="utf-8", errors="replace") as fh:
|
||||
text = fh.read()
|
||||
fm, body = V.parse_frontmatter(text)
|
||||
notizen.append({"abs": ap, "rel": rp, "fm": fm, "body": body})
|
||||
return notizen, anhaenge
|
||||
|
||||
|
||||
def baue_lookup(V, notizen):
|
||||
"""Woertlich aus vault_doctor.py Zeilen 66-74."""
|
||||
lookup = set()
|
||||
for n in notizen:
|
||||
rel = n["rel"]
|
||||
lookup |= {rel, os.path.splitext(rel)[0], os.path.basename(rel),
|
||||
os.path.splitext(os.path.basename(rel))[0]}
|
||||
if n["fm"].get("title"):
|
||||
lookup.add(str(n["fm"]["title"]))
|
||||
if n["fm"].get("uid"):
|
||||
lookup.add(str(n["fm"]["uid"]))
|
||||
return lookup
|
||||
|
||||
|
||||
def wikilinks(V, body):
|
||||
"""Wie V.extract_links, aber mit ausgeblendetem Code.
|
||||
|
||||
Zusaetzlich wird ein abschliessender Backslash entfernt. In Obsidian-
|
||||
Tabellenzellen muss das Pipe eines Alias als `\\|` geschrieben werden,
|
||||
sonst bricht die Tabelle. `_WIKILINK_RE` schneidet am Pipe, laesst den
|
||||
Backslash aber stehen - das Ziel hiesse dann `Pfad\\` und faende nichts.
|
||||
Ein Dateiname kann unter Windows nicht auf Backslash enden, das
|
||||
Abschneiden ist also gefahrlos.
|
||||
"""
|
||||
sauber = strip_code(body)
|
||||
treffer = []
|
||||
for m in V._WIKILINK_RE.finditer(sauber):
|
||||
t = m.group(1).strip().rstrip("\\").strip()
|
||||
if t and t not in treffer:
|
||||
treffer.append(t)
|
||||
return treffer
|
||||
|
||||
|
||||
def lauf(vault, alle):
|
||||
V = lade_toolkit()
|
||||
notizen, anhaenge = sammle(V, vault)
|
||||
if not notizen:
|
||||
raise RuntimeError("Keine Markdown-Dateien unter %s gefunden." % vault)
|
||||
|
||||
lookup = baue_lookup(V, notizen)
|
||||
kaputt = []
|
||||
roh_gesamt = 0
|
||||
for n in notizen:
|
||||
roh_gesamt += len(V.extract_links(n["body"]))
|
||||
for raw in wikilinks(V, n["body"]):
|
||||
t = raw.strip().lstrip("./")
|
||||
# Treffer-Bedingung woertlich aus vault_doctor.py Zeile 104,
|
||||
# erweitert um Anhaenge (Bilder, PDF) - siehe sammle().
|
||||
if t in lookup or t + ".md" in lookup or os.path.basename(t) in lookup:
|
||||
continue
|
||||
if t in anhaenge or os.path.basename(t) in anhaenge:
|
||||
continue
|
||||
kaputt.append((n["rel"], raw))
|
||||
return notizen, kaputt, roh_gesamt
|
||||
|
||||
|
||||
def main():
|
||||
p = argparse.ArgumentParser(description="Tote Wikilinks ohne Codeblock-Fehlalarme")
|
||||
p.add_argument("--vault", default=".")
|
||||
p.add_argument("--alle", action="store_true", help="alle Befunde statt der ersten 40")
|
||||
args = p.parse_args()
|
||||
vault = os.path.abspath(os.path.expanduser(args.vault))
|
||||
|
||||
befunde = []
|
||||
try:
|
||||
notizen, befunde, roh = lauf(vault, args.alle)
|
||||
except Exception:
|
||||
# Absturz ist selbst ein Befund - und die bis dahin gesammelten
|
||||
# Befunde werden trotzdem ausgegeben.
|
||||
sys.stdout.write("# Linkcheck - ABGEBROCHEN\n\n")
|
||||
sys.stdout.write(traceback.format_exc())
|
||||
for rel, raw in befunde:
|
||||
sys.stdout.write(" - %s -> [[%s]]\n" % (rel, raw))
|
||||
return 2
|
||||
|
||||
sys.stdout.write("# Linkcheck - %d Notizen in %s\n" % (len(notizen), vault))
|
||||
sys.stdout.write("\nWikilink-Vorkommen ohne Codeblock-Filter: %d\n" % roh)
|
||||
if not befunde:
|
||||
sys.stdout.write("\nKeine toten Wikilinks ausserhalb von Codebloecken.\n")
|
||||
return 0
|
||||
|
||||
grenze = len(befunde) if args.alle else 40
|
||||
sys.stdout.write("\n## Tote Wikilinks (%d)\n" % len(befunde))
|
||||
for rel, raw in befunde[:grenze]:
|
||||
sys.stdout.write(" - %s -> [[%s]]\n" % (rel, raw))
|
||||
if len(befunde) > grenze:
|
||||
sys.stdout.write(" ... %d weitere (--alle)\n" % (len(befunde) - grenze))
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user