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]>
181 lines
6.4 KiB
Python
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())
|