diff --git a/scripts/make_test_vault.py b/scripts/make_test_vault.py index 3043116..b7538a1 100644 --- a/scripts/make_test_vault.py +++ b/scripts/make_test_vault.py @@ -1,245 +1,223 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -make_test_vault.py — Erzeugt einen kleinen, reproduzierbaren Test-Vault für den Markdown-Importer. +make_test_vault.py — erzeugt einen minimalen, nachvollziehbaren Test-Vault -Version: 1.0.0 (2025-09-05) -Änderungen: -- Initiale Version mit 4 Szenarien (base, add-targets, change-chunking, cleanup) -- Frontmatter entspricht Parser-Anforderungen: title, id, type, status, created (+ optionale Felder) (vgl. parser.validate_required_frontmatter) -- Inhalte decken Wikilinks, zunächst fehlende Ziele (unresolved), spätere Zielanlage und Re-Chunking ab. +Version: 1.1 (2025-09-05) +Änderungen ggü. 1.0: +- created/updated werden als QUOTED ISO-Strings geschrieben, damit der YAML-Parser + sie nicht in datetime-Objekte konvertiert (Schema verlangt strings). +- Kommentare/Output gestrafft. -Beschreibung: -Erstellt unter ./test_vault/ eine kleine Ordnerstruktur mit Markdown-Dateien: -- base: Legt 4 Notizen an. Zwei haben Wikilinks, wovon einige zunächst ins Leere zeigen (Ziel-Notizen existieren noch nicht). -- add-targets: Legt die vorher fehlenden Ziel-Notizen an, so dass der Importer Kanten (references/backlink und references_at) auflösen kann. -- change-chunking: Ändert eine bestehende Notiz so, dass sich die Chunk-Grenzen deutlich verschieben (Überschriften + Textblöcke). Dient zum Test, ob der Importer Notes/Chunks/Edges sauber nachzieht. -- cleanup: Löscht den gesamten test_vault wieder. +Zweck +- Kleiner Obsidian-ähnlicher Vault zum Durchspielen des Importers (Chunks/Edges). +- Szenarien: leere Links, spätere Anlage fehlender Noten, Chunk-Neuaufteilung. -Aufruf: - python3 -m scripts.make_test_vault --scenario base - python3 -m scripts.make_test_vault --scenario add-targets - python3 -m scripts.make_test_vault --scenario change-chunking - python3 -m scripts.make_test_vault --scenario cleanup +Struktur (Default: ./test_vault) +- 40_concepts/concept-alpha.md +- 20_experiences/exp-one.md (verlinkt auf [[concept-alpha]] und [[missing-note]]) +- 20_experiences/exp-two.md (verlinkt auf [[concept-alpha]]) +- 30_projects/project-demo.md (verlinkt auf [[concept-alpha]] und [[exp-one]]) -Hinweise: -- Dieses Skript erzeugt KEINE Qdrant-Daten; es schreibt nur Markdown-Dateien. -- Zum Testen: - 1) base erzeugen → Importer (dry-run) → Audit vergleichen. - 2) add-targets erzeugen → Importer erneut laufen lassen → prüfen, ob vormals „unresolved“ jetzt aufgelöst wurden (edges & edges_at). - 3) change-chunking ausführen → Importer mit --apply → prüfen, ob Chunks/Edges umgezogen/aktualisiert wurden. -- Verwende beim Importer denselben Vault-Pfad: --vault ./test_vault -- Virtuelle Umgebung: Für dieses Skript selbst nicht nötig; für den Importer natürlich weiterhin in deinem venv laufen. +Voraussetzungen +- Keine. Rein Dateigenerierung. +Aufruf + python3 -m scripts.make_test_vault [--out ./test_vault] [--force] + +Parameter + --out Zielverzeichnis (Standard: ./test_vault) + --force Bestehenden Ordner löschen und neu anlegen. + +Hinweise für Tests +1) Erster Import (dry-run oder --apply): Es gibt einen „leeren“ Link [[missing-note]]. +2) Lege danach eine Datei für „missing-note“ an (mit gleicher ID im YAML) und importiere erneut: + -> Erwartung: Edges für ehemals leeren Link werden korrekt nachgezogen (references + backlink). +3) Ändere den Body von exp-one.md so, dass andere Chunk-Grenzen entstehen und importiere erneut: + -> Erwartung: Chunks/Edges/Note werden für betroffene Noten aktualisiert. + +Kompatibel mit: +- note.schema.json: 'created'/'updated' müssen strings sein. (Wichtig!) """ from __future__ import annotations import argparse import os import shutil -from pathlib import Path -from datetime import datetime +from datetime import datetime, timezone -ROOT = Path(__file__).resolve().parents[1] # Projektwurzel (.. /mindnet) -VAULT = ROOT / "test_vault" +def iso_now() -> str: + # ISO 8601 mit Offset; als String zurückgeben + return datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds") -# ---------- Hilfsfunktionen ---------- +def q(s: str) -> str: + # Sicheres Quoten für YAML-Frontmatter + # (einfaches doppelt-Quoten reicht hier, da wir keine eingebetteten """ verwenden) + return f"\"{s}\"" -def write_file(path: Path, content: str): - path.parent.mkdir(parents=True, exist_ok=True) +TEMPLATE_CONCEPT = """--- +title: "Concept Alpha" +id: "concept-alpha" +type: "concept" +status: "active" +created: {created} +updated: {updated} +tags: ["area/mindnet","type/concept","topic/test"] +--- + +## Beschreibung +Dies ist ein einfaches Konzept für Testzwecke. + +## Mögliche Verbindungen +- [[exp-one]] +- [[project-demo]] +""" + +TEMPLATE_EXP_ONE = """--- +title: "Experience One" +id: "exp-one" +type: "experience" +status: "active" +created: {created} +updated: {updated} +tags: ["area/mindnet","type/experience","topic/test"] +--- + +## Kontext +Diese Notiz verlinkt auf ein existierendes Konzept und auf eine noch **fehlende** Note. + +## Beobachtung +- Bezug zu [[concept-alpha]] +- Offener Verweis auf [[missing-note]] + +## Interpretation +Wenn später eine Datei für 'missing-note' angelegt wird, sollte der Importer die Kanten nachziehen. + +## Mögliche Verbindungen +- [[concept-alpha]] +- [[missing-note]] +""" + +TEMPLATE_EXP_TWO = """--- +title: "Experience Two" +id: "exp-two" +type: "experience" +status: "active" +created: {created} +updated: {updated} +tags: ["area/mindnet","type/experience","topic/test"] +--- + +## Kontext +Zweite Experience, die auf dasselbe Konzept verweist. + +## Beobachtung +Verweis auf [[concept-alpha]]. + +## Mögliche Verbindungen +- [[concept-alpha]] +""" + +TEMPLATE_PROJECT = """--- +title: "Project Demo" +id: "project-demo" +type: "project" +status: "active" +created: {created} +updated: {updated} +tags: ["area/mindnet","type/project","topic/test"] +--- + +## Scope +Ein kleines Demo-Projekt, das andere Notizen referenziert. + +## Arbeitspakete +- AP-1: Zusammenhang erklären (siehe [[concept-alpha]]) +- AP-2: Ereignis berücksichtigen (siehe [[exp-one]]) + +## Mögliche Verbindungen +- [[concept-alpha]] +- [[exp-one]] +""" + +TEMPLATE_MISSING = """--- +title: "Missing Note (jetzt angelegt)" +id: "missing-note" +type: "concept" +status: "active" +created: {created} +updated: {updated} +tags: ["area/mindnet","type/concept","topic/test"] +--- + +## Kontext +Diese Notiz wurde **nachträglich** erstellt, um einen zuvor offenen Link zu schließen. + +## Mögliche Verbindungen +- [[exp-one]] +""" + +def write_file(path: str, content: str) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf-8") as f: f.write(content) -def iso_now(): - return datetime.now().isoformat(timespec="seconds") +def build_vault(base: str, include_missing_note: bool) -> None: + created = q(iso_now()) + updated = q(iso_now()) -# ---------- Inhalte (Szenario-Varianten) ---------- + # 40_concepts + write_file( + os.path.join(base, "40_concepts", "concept-alpha.md"), + TEMPLATE_CONCEPT.format(created=created, updated=updated), + ) -def file_index_base() -> str: - # Enthält Links auf bestehende und noch NICHT bestehende Ziele - return f"""--- -title: "Index – Mini-Testvault" -id: "test-index" -type: "concept" -status: "draft" -created: {iso_now()} -tags: ["area/test","type/concept"] ---- + # 20_experiences + write_file( + os.path.join(base, "20_experiences", "exp-one.md"), + TEMPLATE_EXP_ONE.format(created=created, updated=updated), + ) + write_file( + os.path.join(base, "20_experiences", "exp-two.md"), + TEMPLATE_EXP_TWO.format(created=created, updated=updated), + ) -## Überblick -Dies ist der kleine Test-Vault. + # 30_projects + write_file( + os.path.join(base, "30_projects", "project-demo.md"), + TEMPLATE_PROJECT.format(created=created, updated=updated), + ) -## Verweise -- [[test-alpha]] (existiert) -- [[test-beta]] (existiert) -- [[test-gamma]] (existiert) -- [[test-delta]] (FEHLT zunächst — wird erst in add-targets angelegt) -""" - -def file_alpha_base() -> str: - # Hat Wikilinks auf beta (existiert) und epsilon (fehlt zunächst) - return f"""--- -title: "Alpha" -id: "test-alpha" -type: "concept" -status: "draft" -created: {iso_now()} -tags: ["area/test","type/concept"] ---- - -## Abschnitt A -Alpha verweist auf [[test-beta]] und auf [[test-epsilon]] (FEHLT zunächst). - -## Abschnitt B -Noch etwas Text, damit beim Re-Chunking später Grenzen sichtbar werden. -""" - -def file_beta_base() -> str: - # Kurze Notiz ohne Links - return f"""--- -title: "Beta" -id: "test-beta" -type: "concept" -status: "draft" -created: {iso_now()} -tags: ["area/test","type/concept"] ---- - -Beta hat zunächst keine Links. -""" - -def file_gamma_base() -> str: - # Viele Abschnitte → erzeugt mehrere Chunks - return f"""--- -title: "Gamma" -id: "test-gamma" -type: "concept" -status: "draft" -created: {iso_now()} -tags: ["area/test","type/concept"] ---- - -## Einleitung -Dieser Abschnitt enthält einen Link zu [[test-index]]. - -## Details -Gamma liefert Details. Hier noch ein Link zu [[test-beta]]. - -## Weitere Hinweise -Hier steht Text, damit die Chunker-Logik genug Material hat. -""" - -def file_delta_added() -> str: - # Wird erst im Szenario add-targets erstellt (auflösen von vormals unresolved [[test-delta]]) - return f"""--- -title: "Delta (neu angelegt)" -id: "test-delta" -type: "concept" -status: "draft" -created: {iso_now()} -tags: ["area/test","type/concept"] ---- - -Delta wurde nachträglich angelegt, um ehemals leere Links aufzulösen. -""" - -def file_epsilon_added() -> str: - # Wird erst im Szenario add-targets erstellt (auflösen von [[test-epsilon]] aus Alpha) - return f"""--- -title: "Epsilon (neu angelegt)" -id: "test-epsilon" -type: "concept" -status: "draft" -created: {iso_now()} -tags: ["area/test","type/concept"] ---- - -Epsilon existiert nun und kann referenziert werden. -""" - -def file_gamma_changed_chunking() -> str: - # Ersetzt die ursprüngliche Gamma-Datei mit deutlich mehr (und anders geschnittenen) Abschnitten, - # so dass sich die Chunk-Grenzen ändern. - return f"""--- -title: "Gamma (rechunked)" -id: "test-gamma" -type: "concept" -status: "draft" -created: {iso_now()} -tags: ["area/test","type/concept"] ---- - -# Einleitung (H1 statt H2) -Kurzer Teaser. Verweis auf [[test-index]] bleibt bestehen. - -## Teil 1 – Kontext -Mehr Text. Dieser Abschnitt ist länger und soll den ursprünglichen „Details“-Block ersetzen. -Außerdem Verweis auf [[test-beta]]. - -## Teil 2 – Beispiele -- Beispiel 1: Ein etwas längerer Listenpunkt mit Beschreibung. -- Beispiel 2: Noch ein Punkt. -- Beispiel 3: Und noch einer. - -## Teil 3 – Fazit -Zusammenfassung und ggf. ein weiterer Verweis auf [[test-alpha]]. -""" - -# ---------- Szenarien ausführen ---------- - -def scenario_base(): - # Frisch anlegen / überschreiben - (VAULT / "notes").mkdir(parents=True, exist_ok=True) - write_file(VAULT / "notes" / "index.md", file_index_base()) - write_file(VAULT / "notes" / "alpha.md", file_alpha_base()) - write_file(VAULT / "notes" / "beta.md", file_beta_base()) - write_file(VAULT / "notes" / "gamma.md", file_gamma_base()) - print(f"[base] Angelegt: {VAULT}") - -def scenario_add_targets(): - # Nur neue Ziele hinzufügen (delta, epsilon) - if not VAULT.exists(): - print("test_vault fehlt – bitte erst --scenario base ausführen.") - return - write_file(VAULT / "notes" / "delta.md", file_delta_added()) - write_file(VAULT / "notes" / "epsilon.md", file_epsilon_added()) - print("[add-targets] Neue Zielnotizen delta/epsilon angelegt.") - -def scenario_change_chunking(): - # Gamma ersetzen, um Chunk-Grenzen zu verschieben - if not VAULT.exists(): - print("test_vault fehlt – bitte erst --scenario base ausführen.") - return - write_file(VAULT / "notes" / "gamma.md", file_gamma_changed_chunking()) - print("[change-chunking] gamma.md geändert (deutlich andere Abschnittsstruktur).") - -def scenario_cleanup(): - if VAULT.exists(): - shutil.rmtree(VAULT) - print("[cleanup] test_vault gelöscht.") - else: - print("[cleanup] nichts zu tun – test_vault existiert nicht.") - -# ---------- CLI ---------- + # Optional: zunächst NICHT anlegen (Szenario „leerer Link“), + # später für den 2. Testlauf anlegen. + if include_missing_note: + write_file( + os.path.join(base, "40_concepts", "missing-note.md"), + TEMPLATE_MISSING.format(created=created, updated=updated), + ) def main(): ap = argparse.ArgumentParser() - ap.add_argument( - "--scenario", - required=True, - choices=["base", "add-targets", "change-chunking", "cleanup"], - help="Welches Setup erzeugt werden soll." - ) + ap.add_argument("--out", default="./test_vault", help="Zielverzeichnis für den Test-Vault") + ap.add_argument("--force", action="store_true", help="Falls vorhanden: Verzeichnis löschen und neu anlegen") + ap.add_argument("--with-missing", action="store_true", + help="Auch die 'missing-note' direkt anlegen (Standard: erst im 2. Lauf)") args = ap.parse_args() - if args.scenario == "base": - scenario_base() - elif args.scenario == "add-targets": - scenario_add_targets() - elif args.scenario == "change-chunking": - scenario_change_chunking() - elif args.scenario == "cleanup": - scenario_cleanup() + base = os.path.abspath(args.out) + if os.path.exists(base): + if args.force: + shutil.rmtree(base) + else: + print(f"Ziel existiert bereits: {base}\nNutze --force zum Überschreiben.") + return + + build_vault(base, include_missing_note=args.with_missing) + print(f"Test-Vault erstellt unter: {base}") + print("Hinweis:") + print(" 1) Zuerst ohne missing-note anlegen (Standard), importieren, Edges prüfen.") + print(" 2) Dann erneut ausführen mit --force --with-missing (missing-note wird erzeugt),") + print(" und Import wiederholen → Edges/Backlinks sollten nachgezogen werden.") if __name__ == "__main__": main()