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]>
229 lines
7.9 KiB
Python
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())
|