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
c064c729ed
82 files changed
+13428
No files matched your search
@@ -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