Files
claude-regelwerk/.vault-tools/obsidian-vault-navigator/scripts/vault_query.py
T
Pascal MichelandClaude Opus 5 2c9a4475fc 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]>
2026-09-17 22:38:15 +02:00

181 lines
6.4 KiB
Python

#!/usr/bin/env python3
"""Fragt die Referenzschicht ab und gibt kompakte Treffer aus.
Das ist der Standardeinstieg in den Vault: erst hier suchen, dann nur die
Treffer öffnen. Nie `vault-index.json` am Stück lesen.
Beispiele:
python3 scripts/vault_query.py --text "rate limiting"
python3 scripts/vault_query.py --type project-code --status active
python3 scripts/vault_query.py --tag k8s --since 30 --format md
python3 scripts/vault_query.py --links-to 10_Projects/auth-service/index.md
python3 scripts/vault_query.py --orphans --type research
"""
from __future__ import annotations
import argparse
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import vaultlib as V # noqa: E402
def matches_text(note: dict, needles) -> bool:
hay = " ".join([
note.get("n", ""), note.get("m", ""), note.get("p", ""),
" ".join(note.get("g", [])), " ".join(note.get("h", [])),
note.get("r", ""), note.get("u", ""),
]).lower()
return all(n in hay for n in needles)
def days_ago(iso_date: str) -> int | None:
import datetime as dt
try:
d = dt.date.fromisoformat(iso_date[:10])
except (ValueError, TypeError):
return None
return (dt.date.today() - d).days
def fmt_compact(n: dict) -> str:
bits = [n["p"], n.get("n", "")]
tail = n.get("m", "")
meta = []
if n.get("t"):
meta.append(n["t"])
if n.get("s"):
meta.append(n["s"])
head = " :: ".join(b for b in bits if b)
if meta:
head += f" [{'/'.join(meta)}]"
return head + (f" :: {tail}" if tail else "")
def fmt_md(n: dict) -> str:
link = os.path.splitext(n["p"])[0]
line = f"- [[{link}|{n.get('n', link)}]]"
if n.get("s"):
line += f" `{n['s']}`"
if n.get("m"):
line += f" — {n['m']}"
return line
def main() -> int:
ap = argparse.ArgumentParser(description="Vault-Index abfragen")
ap.add_argument("--vault", default=".")
ap.add_argument("--text", action="append", default=[],
help="Suchbegriff in Titel/Summary/Pfad/Tags/Überschriften (mehrfach = UND)")
ap.add_argument("--type", action="append", default=[])
ap.add_argument("--status", action="append", default=[])
ap.add_argument("--tag", action="append", default=[])
ap.add_argument("--project", default=None)
ap.add_argument("--path", default=None, help="Pfadpräfix, z. B. 30_Research")
ap.add_argument("--since", type=int, default=None, help="nur Notizen, die in den letzten N Tagen aktualisiert wurden")
ap.add_argument("--stale", type=int, default=None, help="nur Notizen, die seit N Tagen nicht angefasst wurden")
ap.add_argument("--links-to", default=None, help="Notizen, die auf dieses Ziel verlinken (Pfad oder uid)")
ap.add_argument("--orphans", action="store_true", help="nur Notizen ohne eingehende Links")
ap.add_argument("--no-summary", action="store_true", help="nur Notizen ohne summary (Pflegeliste)")
ap.add_argument("--limit", type=int, default=40)
ap.add_argument("--format", choices=["compact", "paths", "md", "json", "headings"],
default="compact")
ap.add_argument("--sort", choices=["path", "updated", "title"], default="path")
args = ap.parse_args()
vault = os.path.abspath(os.path.expanduser(args.vault))
index = V.load_index(vault)
notes = index["notes"]
meta = index.get("meta", {})
backlinks = {}
bl_path = os.path.join(vault, V.INDEX_DIR, "backlinks.json")
if os.path.exists(bl_path):
with open(bl_path, "r", encoding="utf-8") as fh:
backlinks = json.load(fh)
needles = [t.lower() for t in args.text]
types = {t.lower() for t in args.type}
stati = {s.lower() for s in args.status}
tags = {t.lower().lstrip("#") for t in args.tag}
hits = []
for n in notes:
if types and n.get("t", "").lower() not in types:
continue
if stati and n.get("s", "").lower() not in stati:
continue
if tags and not tags.issubset({g.lower().lstrip("#") for g in n.get("g", [])}):
continue
if args.project and n.get("r", "").lower() != args.project.lower():
continue
if args.path and not n["p"].startswith(args.path.rstrip("/")):
continue
if needles and not matches_text(n, needles):
continue
is_asset = n.get("t") == "asset"
if args.no_summary and (n.get("m") or (is_asset and "asset" not in types)):
continue
age = days_ago(n.get("d", ""))
if args.since is not None and (age is None or age > args.since):
continue
if args.stale is not None and (age is None or age < args.stale):
continue
if args.orphans and (backlinks.get(n["p"]) or (is_asset and "asset" not in types)):
continue
if args.links_to:
target = args.links_to
resolved = None
for cand in notes:
if target in (cand.get("p"), cand.get("u"), cand.get("n")):
resolved = cand["p"]
break
resolved = resolved or target
if n["p"] not in backlinks.get(resolved, []):
continue
hits.append(n)
if args.sort == "updated":
hits.sort(key=lambda n: n.get("d", ""), reverse=True)
elif args.sort == "title":
hits.sort(key=lambda n: n.get("n", "").lower())
total = len(hits)
hits = hits[: args.limit]
if args.format == "json":
print(json.dumps(hits, ensure_ascii=False, indent=1))
elif args.format == "paths":
for n in hits:
print(n["p"])
elif args.format == "md":
for n in hits:
print(fmt_md(n))
elif args.format == "headings":
for n in hits:
print(fmt_compact(n))
for h in n.get("h", []):
print(f" # {h}")
else:
for n in hits:
print(fmt_compact(n))
if not hits:
print("(keine Treffer — Filter lockern oder als Fallback grep über den Vault)",
file=sys.stderr)
if total > len(hits):
print(f"… {total - len(hits)} weitere Treffer (--limit erhöhen)", file=sys.stderr)
newer = V.count_newer_than(vault, meta.get("generated", ""))
if newer:
print(f"[Hinweis] {newer} Notiz(en) sind neuer als der Index — "
"build_index.py erneut laufen lassen.", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())