Files
claude-werkzeuge/.vault-tools/obsidian-vault-navigator/scripts/build_index.py
T
Pascal MichelandClaude Opus 5.5 fa1c26b407 Erste Fassung: Claude-Werkzeuge aus dem Vault
.vault-tools (ohne trading-plattform-pruefung und watermarks-remover),
.claude/agents, .claude/skills, .claude/workflows, .claude/settings.json.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-14 11:27:59 +02:00

229 lines
7.9 KiB
Python

#!/usr/bin/env python3
"""Scannt den Vault und erzeugt die Referenzschicht in `_index/`.
Erzeugt:
_index/vault-index.json kompakter Maschinen-Index (Kurzschlüssel, siehe
references/indexing.md) — wird von vault_query.py
gelesen, nicht direkt vom Agenten
_index/INDEX.md menschenlesbare Übersicht mit Wikilinks
_index/backlinks.json Rückwärtsverweise pro Notiz
Beispiel:
python3 scripts/build_index.py --vault ~/Vault
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from collections import Counter, defaultdict
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import vaultlib as V # noqa: E402
TYPE_ORDER = [
"moc", "area", "project-code", "research", "decision", "template-project",
"resource", "note-template", "inbox", "note", "archive", "asset",
]
TYPE_LABEL = {
"moc": "Maps of Content",
"area": "Areas",
"project-code": "Projekte (Code)",
"research": "Research",
"decision": "Entscheidungen",
"template-project": "Template-Projekte",
"resource": "Ressourcen",
"note-template": "Notiz-Vorlagen",
"inbox": "Inbox",
"note": "Sonstige Notizen",
"archive": "Archiv",
"asset": "Dateien",
}
def normalize_link(target: str, notes_by_key: dict) -> str | None:
"""Wikilink-Ziel auf einen Index-Pfad abbilden."""
t = target.strip().lstrip("./")
for cand in (t, t + ".md", t.replace(".md", "") + ".md"):
if cand in notes_by_key:
return notes_by_key[cand]
base = os.path.basename(t)
for cand in (base, base + ".md"):
if cand in notes_by_key:
return notes_by_key[cand]
return None
def build(vault: str, include_assets: bool, excludes) -> dict:
notes, assets = [], []
for ap, rp in V.walk_vault(vault, excludes):
ext = os.path.splitext(rp)[1].lower()
if ext == ".md":
try:
notes.append(V.read_note(ap, rp))
except OSError as exc:
print(f" übersprungen: {rp} ({exc})", file=sys.stderr)
elif include_assets and ext in V.ASSET_EXTS:
assets.append({
"p": rp,
"t": "asset",
"n": os.path.basename(rp),
"r": rp.split("/")[1] if rp.startswith("10_Projects/") and rp.count("/") > 1 else "",
"w": os.path.getsize(ap),
})
notes.sort(key=lambda n: n["p"])
assets.sort(key=lambda n: n["p"])
all_entries = notes + assets
lookup = {}
for n in notes:
lookup[n["p"]] = n["p"]
lookup[os.path.basename(n["p"])] = n["p"]
lookup[os.path.splitext(n["p"])[0]] = n["p"]
lookup[os.path.splitext(os.path.basename(n["p"]))[0]] = n["p"]
if n.get("n"):
lookup.setdefault(n["n"], n["p"])
if n.get("u"):
lookup.setdefault(n["u"], n["p"])
backlinks = defaultdict(list)
broken = 0
for n in notes:
for raw in n.get("l", []):
tgt = normalize_link(raw, lookup)
if tgt:
if n["p"] not in backlinks[tgt]:
backlinks[tgt].append(n["p"])
else:
broken += 1
meta = {
"v": 1,
"generated": V.iso_now(),
"vault": os.path.abspath(vault),
"notes": len(notes),
"assets": len(assets),
"words": sum(n.get("w", 0) for n in notes),
"types": dict(Counter(n.get("t", "note") for n in notes).most_common()),
"status": dict(Counter(n["s"] for n in notes if n.get("s")).most_common()),
"tags": dict(Counter(t for n in notes for t in n.get("g", [])).most_common(60)),
"broken_links": broken,
}
return {"meta": meta, "notes": all_entries, "backlinks": dict(backlinks)}
def render_markdown(index: dict) -> str:
meta = index["meta"]
by_type = defaultdict(list)
for n in index["notes"]:
by_type[n.get("t", "note")].append(n)
out = [
"---",
"type: moc",
"title: Vault-Index",
f"summary: Generierte Übersicht aller Notizen ({meta['notes']} Stück). "
"Nicht von Hand bearbeiten.",
f"updated: {V.today()}",
"---",
"",
"# Vault-Index",
"",
f"> [!info] Generiert am {meta['generated']} · {meta['notes']} Notizen · "
f"{meta['assets']} Dateien",
"> Diese Datei wird von `build_index.py` erzeugt. Änderungen gehen verloren.",
"",
]
if meta["notes"] > 200:
out += [
"> [!warning] Großer Vault",
"> Diese Datei nicht komplett lesen. Stattdessen gezielt abfragen:",
"> `python3 scripts/vault_query.py --text \"…\"`",
"",
]
ordered = [t for t in TYPE_ORDER if t in by_type]
ordered += sorted(t for t in by_type if t not in TYPE_ORDER)
for t in ordered:
entries = by_type[t]
out.append(f"## {TYPE_LABEL.get(t, t)} ({len(entries)})")
out.append("")
if t == "asset":
for n in entries[:200]:
out.append(f"- `{n['p']}`")
if len(entries) > 200:
out.append(f"- … {len(entries) - 200} weitere")
out.append("")
continue
for n in entries:
link = os.path.splitext(n["p"])[0]
line = f"- [[{link}|{n.get('n', link)}]]"
bits = []
if n.get("s"):
bits.append(f"`{n['s']}`")
if n.get("g"):
bits.append(" ".join(f"#{g}" for g in n["g"][:5]))
if bits:
line += " " + " ".join(bits)
if n.get("m"):
line += f" — {n['m']}"
out.append(line)
out.append("")
if meta["tags"]:
out.append("## Top-Tags")
out.append("")
out.append(" ".join(f"#{t} ({c})" for t, c in list(meta["tags"].items())[:30]))
out.append("")
return "\n".join(out)
def main() -> int:
ap = argparse.ArgumentParser(description="Referenzschicht für den Vault bauen")
ap.add_argument("--vault", default=".", help="Pfad zum Vault (Standard: aktuelles Verzeichnis)")
ap.add_argument("--no-assets", action="store_true", help="Nicht-Markdown-Dateien nicht indexieren")
ap.add_argument("--exclude", action="append", default=[], help="Zusätzlicher Ordnername zum Überspringen (mehrfach möglich)")
ap.add_argument("--quiet", action="store_true")
args = ap.parse_args()
vault = os.path.abspath(os.path.expanduser(args.vault))
if not os.path.isdir(vault):
print(f"Vault-Pfad existiert nicht: {vault}", file=sys.stderr)
return 2
index = build(vault, not args.no_assets, args.exclude)
outdir = os.path.join(vault, V.INDEX_DIR)
os.makedirs(outdir, exist_ok=True)
with open(os.path.join(outdir, V.INDEX_JSON), "w", encoding="utf-8") as fh:
json.dump({"meta": index["meta"], "notes": index["notes"]}, fh,
ensure_ascii=False, separators=(",", ":"))
with open(os.path.join(outdir, "backlinks.json"), "w", encoding="utf-8") as fh:
json.dump(index["backlinks"], fh, ensure_ascii=False, separators=(",", ":"))
with open(os.path.join(outdir, V.INDEX_MD), "w", encoding="utf-8") as fh:
fh.write(render_markdown(index))
if not args.quiet:
m = index["meta"]
print(f"Index gebaut: {m['notes']} Notizen, {m['assets']} Dateien, "
f"{m['words']} Wörter")
print("Typen: " + ", ".join(f"{k}={v}" for k, v in m["types"].items()))
missing = [n["p"] for n in index["notes"]
if n.get("t") != "asset" and not n.get("m")]
if missing:
print(f"Ohne summary: {len(missing)} Notizen "
f"(vault_doctor.py zeigt welche)")
if m["broken_links"]:
print(f"Tote Wikilinks: {m['broken_links']} (vault_doctor.py --links)")
print(f"Geschrieben nach {os.path.join(V.INDEX_DIR, '')}")
return 0
if __name__ == "__main__":
raise SystemExit(main())