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,228 @@
|
||||
#!/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())
|
||||
Reference in new issue
Block a user