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