#!/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())