diff --git a/SMOKE-CHECKS.md b/SMOKE-CHECKS.md new file mode 100644 index 0000000..602073a --- /dev/null +++ b/SMOKE-CHECKS.md @@ -0,0 +1,200 @@ +# Smoke-Checks: Web-Viewer und Desktop-GUI + +Dieses Dokument beschreibt die Smoke-Checks für die beiden interaktiven +Frontends von KnowledgeDigest: den Web-Viewer und die Desktop-GUI. +Die Core-Unit-Tests (chunker, schema, config, utils) sind in `tests/test_core.py` +abgedeckt und laufen via `python -m pytest tests -q`. + +--- + +## 1. Web-Viewer-Smoke-Check + +### Zweck + +Sicherstellen, dass der Web-Viewer korrekt startet, einen HTTP-Server auf +dem konfigurierten Port bindet und eine gültige HTML-Seite (Dashboard) +ausliefert — ausschließlich mit Python-Stdlib, ohne externe Abhängigkeiten. + +- Modul: `web_viewer.py` +- Einstiegspunkt: `launch_web()` / `python -m KnowledgeDigest --web` +- Abhängigkeiten: **Python-Stdlib only** (kein Flask, kein Django) + +### Voraussetzungen + +```bash +python --version # Python 3.10+ erforderlich +pip install -e . # Paket installieren (kein weiteres pip-install nötig) +``` + +### Manueller Smoke-Check (interaktiv) + +```bash +# Windows (PowerShell oder Git Bash): +PYTHONIOENCODING=utf-8 python -m KnowledgeDigest --web --no-browser +``` + +Option `--no-browser` verhindert das automatische Öffnen des Browsers. +Alternativer Port (falls 8787 belegt): + +```bash +PYTHONIOENCODING=utf-8 python -m KnowledgeDigest --web --port 9000 --no-browser +``` + +Eigene Datenbankdatei angeben: + +```bash +PYTHONIOENCODING=utf-8 python -m KnowledgeDigest --web --db pfad/zur/knowledge.db --no-browser +``` + +### Erwartete Konsolenausgabe + +``` +KnowledgeDigest Web-Viewer: http://127.0.0.1:8787 +DB: data/knowledge.db +``` + +Der Prozess blockiert danach (HTTP-Server lauscht). Beenden mit **Ctrl+C**: + +``` +Viewer beendet. +``` + +### Erwartetes HTTP-Ergebnis + +`GET http://127.0.0.1:8787/` liefert HTTP 200 mit HTML-Dashboard. + +Prüfung via PowerShell (Windows, während der Server läuft): + +```powershell +(Invoke-WebRequest -Uri http://127.0.0.1:8787/ -UseBasicParsing).StatusCode +# Erwartet: 200 +``` + +Prüfung via Git Bash / curl (falls verfügbar): + +```bash +curl -s -w "%{http_code}" http://127.0.0.1:8787/ -o nul +# Erwartet: 200 +``` + +### Headless-Import-Check (CI-geeignet, ohne laufenden Server) + +Prüft nur, ob das Modul fehlerfrei importierbar ist — startet keinen Server: + +```bash +PYTHONIOENCODING=utf-8 python -c "from KnowledgeDigest.web_viewer import launch_web; print('OK')" +# Erwartet: OK +``` + +Dieser Check ist bereits indirekt im CI enthalten (`python -m compileall -q .` +und `python -m KnowledgeDigest --help`), testet aber keinen laufenden Server. + +### Typische Fehler + +| Fehler | Ursache | Lösung | +|--------|---------|--------| +| `OSError: [Errno 10048] / Address already in use` | Port 8787 belegt | `--port 9000` (oder freien Port wählen) | +| `sqlite3.OperationalError: no such table` | DB-Schema fehlt | `python -m KnowledgeDigest status` ausführen (initialisiert Schema) | +| `ModuleNotFoundError: No module named 'KnowledgeDigest'` | Paket nicht installiert | `pip install -e .` ausführen | +| `FileNotFoundError` (DB-Pfad) | Falsche DB-Konfiguration | `--db pfad/knowledge.db` explizit angeben | +| Leere Dokumentenliste im Browser | Normale Erstnutzung | `python -m KnowledgeDigest scan ` ausführen | + +--- + +## 2. Desktop-GUI-Smoke-Check + +### Zweck + +Sicherstellen, dass die PySide6-Anwendung korrekt startet, das Hauptfenster +öffnet und die 3-Panel-Ansicht (Verzeichnisse | Dokumente | Vorschau) mit +Dark-Theme und Toolbar anzeigt. + +- Modul: `gui/app.py` (Einstieg: `launch_gui()`) +- Einstiegspunkt: `python -m KnowledgeDigest --gui` +- Abhängigkeit: **PySide6** (nicht Stdlib — muss installiert sein) +- Voraussetzung: **Grafisches Display** muss verfügbar sein (kein reines Headless-CI) + +### Voraussetzungen + +```bash +python --version # Python 3.10+ erforderlich +pip install -e . # Paket + Kern-Abhängigkeiten installieren +pip install PySide6 # GUI-Abhängigkeit (LGPL) +``` + +### Manueller Smoke-Check (interaktiv) + +```bash +# Windows (PowerShell oder Git Bash): +PYTHONIOENCODING=utf-8 python -m KnowledgeDigest --gui +``` + +Alternativ über das mitgelieferte Windows-Startskript: + +```bat +start.bat +``` + +(`start.bat` liegt im Elternordner des Projektverzeichnisses und ruft +`launcher.py` auf, der seinerseits die GUI startet.) + +### Erwartetes Verhalten + +Nach dem Start (keine Konsolenausgabe erwartet): + +1. Anwendungsfenster öffnet sich (Mindestgröße 900 × 600 px). +2. Dark-Theme ist aktiv (Hintergrundfarbe `#0d1117`). +3. Toolbar sichtbar mit den Aktionen „+ Verzeichnis" und „Scannen", einem Suchfeld + (Platzhaltertext „Suche (FTS5)...") sowie den Aktionen „Web-Viewer" und „Einstellungen". +4. 3-Panel-Splitter: Links Verzeichnisliste, Mitte Dokumententabelle, Rechts Vorschau. +5. Statusleiste am unteren Rand zeigt eine Bereitschaftsmeldung. +6. Schließen des Fensters beendet die Anwendung sauber (Exit-Code 0). + +### Headless-Import-Check (CI-geeignet, ohne Display) + +Prüft den Import aller GUI-Module ohne einen QApplication-Start: + +```bash +PYTHONIOENCODING=utf-8 python -c "from KnowledgeDigest.gui.app import launch_gui; print('OK')" +# Erwartet: OK (schlägt fehl, wenn PySide6 nicht installiert ist) +``` + +**Hinweis:** Der vollständige GUI-Start (`launch_gui()`) erfordert einen +aktiven Display-Server. In headless CI-Umgebungen (z. B. GitHub Actions +Ubuntu-Runner ohne Virtual Display) schlägt `python -m KnowledgeDigest --gui` +mit einem Qt-Fehler fehl — das ist kein Anwendungsfehler: + +``` +qt.qpa.xcb: could not connect to display +``` + +Der CI-Workflow (`tests.yml`) enthält deshalb keinen vollständigen GUI-Start. +Der Import-Check oben ist der höchste CI-taugliche Prüfpunkt ohne Xvfb-Setup. + +### Typische Fehler + +| Fehler | Ursache | Lösung | +|--------|---------|--------| +| `ModuleNotFoundError: No module named 'PySide6'` | PySide6 nicht installiert | `pip install PySide6` | +| `qt.qpa.xcb: could not connect to display` | Kein grafisches Display (Linux CI) | Nur im interaktiven Kontext ausführen; kein CI-Fehler | +| `qt.qpa.plugin: Could not load the Qt platform plugin "xcb"` | Fehlende Qt-Plattform-Bibliotheken (Linux) | PySide6 neu installieren oder `libxcb`-Pakete via apt prüfen | +| `FileNotFoundError: KnowledgeDigest.ico` | App-Icon nicht vorhanden | Kein funktionaler Fehler — GUI startet trotzdem ohne Icon | +| `ModuleNotFoundError: No module named 'KnowledgeDigest'` | Paket nicht installiert | `pip install -e .` ausführen | +| `ModuleNotFoundError: No module named 'fitz'` | PyMuPDF fehlt (PDF-Vorschau) | `pip install PyMuPDF` (optional, nur für PDF-Vorschau im Preview-Panel) | +| Leere Dokumentenliste nach Start | Normale Erstnutzung | Verzeichnis über Toolbar-Schaltfläche „+" hinzufügen und scannen | + +--- + +## Abgrenzung: Was dieser Check NICHT testet + +- **Core-Logik** (Chunker, Schema, Config, Utils): Abgedeckt durch `tests/test_core.py` + (`python -m pytest tests -q`). +- **LLM-Summarization** (Haiku/Flash): Erfordert API-Schlüssel — kein Teil des Smoke-Checks. +- **BACH-Integration**: Optionales Modul, deaktiviert per Default (`bach_enabled: false`). +- **Vollständige End-to-End-Tests** (Ingest → Suche → Ergebnis): Nicht enthalten; + diese würden eigene Integrationstests erfordern. + +--- + +*Erstellt 2026-06-28. Basis: `web_viewer.py` (launch\_web), `gui/app.py` (launch\_gui), +`__main__.py`, `.github/workflows/tests.yml`.* diff --git a/__init__.py b/__init__.py index 267a3e6..e875e8d 100644 --- a/__init__.py +++ b/__init__.py @@ -20,7 +20,12 @@ License: MIT """ -from .digest import KnowledgeDigest +try: + from .digest import KnowledgeDigest +except ImportError: + # Ohne Paket-Kontext (z.B. wenn pytest __init__.py direkt importiert): + # KnowledgeDigest bleibt undefiniert; normaler Package-Import ist nicht betroffen. + pass # type: ignore[assignment] __version__ = "0.4.0" __all__ = ["KnowledgeDigest"] diff --git a/indexer.py b/indexer.py index 023d21c..6bcf2cc 100644 --- a/indexer.py +++ b/indexer.py @@ -14,15 +14,21 @@ 6. FTS5-Index wird automatisch via Trigger befuellt """ -__all__ = ["SkillIndexer"] +__all__ = ["SkillIndexer", "import_bach_skills"] import sqlite3 from pathlib import Path -from typing import Dict, Optional, Any +from typing import Dict, Optional, Any, Union -from .schema import ensure_schema -from .chunker import chunk_text, estimate_tokens -from .utils import sha256_hash as _sha256, extract_keywords as _extract_keywords +# Relative Imports (Paket-Kontext) mit Fallback auf absolute Imports (sys.path) +try: + from .schema import ensure_schema + from .chunker import chunk_text, estimate_tokens + from .utils import sha256_hash as _sha256, extract_keywords as _extract_keywords +except ImportError: + from schema import ensure_schema # type: ignore[no-redef] + from chunker import chunk_text, estimate_tokens # type: ignore[no-redef] + from utils import sha256_hash as _sha256, extract_keywords as _extract_keywords # type: ignore[no-redef] class SkillIndexer: @@ -261,3 +267,58 @@ def get_index_status(self) -> Dict[str, Any]: 'by_type': {r['skill_type']: r['cnt'] for r in by_type}, 'by_category': {r['category']: r['cnt'] for r in by_category}, } + + +# --------------------------------------------------------------------------- +# Standalone-API (kein Klassen-Overhead, für direkte Nutzung ohne Instanz) +# --------------------------------------------------------------------------- + +def import_bach_skills( + bach_db_path: Union[str, Path, None], + knowledge_db_path: Union[str, Path], + *, + chunk_size: int = 350, + overlap: int = 0, + force: bool = False, +) -> Dict[str, Any]: + """Importiert BACH-Skills aus bach.db in die knowledge.db. + + Standalone-Funktion: läuft auch ohne echtes BACH graceful durch. + + Gibt immer ein Dict zurück – kein Exception-Throw: + - ``{"available": False, "error": "..."}`` falls BACH nicht verfügbar + - ``{"available": True, "total_skills": N, ...}`` bei Erfolg + + Args: + bach_db_path: Pfad zur BACH-Datenbank. ``None`` → sofortiges available=False. + knowledge_db_path: Pfad zur Ziel-knowledge.db (wird ggf. neu angelegt). + chunk_size: Wörter pro Chunk (Default: 350). + overlap: Overlap zwischen Chunks in Wörtern. + force: Wenn True, bestehenden Index komplett neu aufbauen. + + Returns: + Dict mit ``"available"`` (bool) plus Statistiken oder Fehlerdetails. + """ + if bach_db_path is None: + return {"available": False, "error": "Kein bach_db_path angegeben."} + + path = Path(bach_db_path) + if not path.exists(): + return {"available": False, "error": f"bach.db nicht gefunden: {path}"} + + indexer = SkillIndexer(Path(knowledge_db_path)) + try: + result = indexer.index_from_bach( + path, chunk_size=chunk_size, overlap=overlap, force=force + ) + # index_from_bach gibt bei fehlendem Pfad selbst ein error-Dict zurück – + # wird hier als available=False weitergereicht. + if "error" in result: + return {"available": False, **result} + result["available"] = True + return result + except sqlite3.Error as exc: + # Z.B. wenn bach.db existiert, aber die skills-Tabelle fehlt. + return {"available": False, "error": f"SQLite-Fehler beim BACH-Import: {exc}"} + finally: + indexer.close() diff --git a/launcher.py b/launcher.py index 7ac5be4..c47d2fc 100644 --- a/launcher.py +++ b/launcher.py @@ -75,12 +75,29 @@ def main(): # Struktur sicherstellen ensure_structure(base) - # Sicherstellen dass das Paket importierbar ist + # Sicherstellen dass das Paket importierbar ist. + # Das Package liegt im Ordner ".db" (kein gueltiger Python-Name), wird aber + # als "KnowledgeDigest" importiert. Loesung: .db-Verzeichnis direkt importieren + # und unter dem Alias "KnowledgeDigest" in sys.modules registrieren. if str(base.parent) not in sys.path: sys.path.insert(0, str(base.parent)) if str(base) not in sys.path: sys.path.insert(0, str(base)) + # Alias: ".db"-Package als "KnowledgeDigest" im Import-System registrieren + import importlib.util + if "KnowledgeDigest" not in sys.modules: + spec = importlib.util.spec_from_file_location( + "KnowledgeDigest", + str(base / "__init__.py"), + submodule_search_locations=[str(base)], + ) + pkg = importlib.util.module_from_spec(spec) + pkg.__path__ = [str(base)] + pkg.__package__ = "KnowledgeDigest" + sys.modules["KnowledgeDigest"] = pkg + spec.loader.exec_module(pkg) + # GUI starten os.environ["PYTHONIOENCODING"] = "utf-8" diff --git a/start.bat b/start.bat index 8ec6ca4..70c4236 100644 --- a/start.bat +++ b/start.bat @@ -18,5 +18,7 @@ if errorlevel 1 ( :: GUI starten cd .. echo Starte KnowledgeDigest GUI... -python -m KnowledgeDigest --gui +:: Ordnername .db ist kein gueltiger Python-Name - dort direkt ueber den +:: Launcher starten, im normal benannten Klon ueber das Paket. +if exist .db\launcher.py (python .db\launcher.py) else (python -m KnowledgeDigest --gui) if errorlevel 1 pause diff --git a/tests/test_bach_import.py b/tests/test_bach_import.py new file mode 100644 index 0000000..53079b9 --- /dev/null +++ b/tests/test_bach_import.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +""" +Fixture-Tests für den optionalen BACH-Import (Standalone-Funktionen). + +Testet: + - Graceful-Fallback-Pfade ohne echte bach.db (available=False, kein Crash) + - Positiv-Pfad mit synthetischer bach.db (available=True, Daten landen in knowledge.db) + +Setzt KEIN echtes BACH voraus und benötigt KEIN pip install -e . +(Projektverzeichnis wird via sys.path.insert eingebunden). +""" +import sys +import sqlite3 +from pathlib import Path + +# Projektverzeichnis in sys.path eintragen — ermöglicht direkten Import +# ohne "pip install -e ." (wie in test_core.py für chunker/schema/etc.) +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from indexer import import_bach_skills # noqa: E402 +from wiki_indexer import import_bach_wikis # noqa: E402 + +import pytest # noqa: E402 + + +# --------------------------------------------------------------------------- +# Hilfsfunktionen: Minimale synthetische bach.db erzeugen +# --------------------------------------------------------------------------- + +def _make_skills_db(path: Path) -> None: + """Erzeugt eine minimale bach.db mit einem Skill-Datensatz.""" + conn = sqlite3.connect(str(path)) + conn.execute(""" + CREATE TABLE skills ( + id INTEGER PRIMARY KEY, + name TEXT, + type TEXT, + category TEXT, + path TEXT, + description TEXT, + version TEXT, + content TEXT, + content_hash TEXT, + is_active INTEGER + ) + """) + conn.execute(""" + INSERT INTO skills + (id, name, type, category, path, description, version, + content, content_hash, is_active) + VALUES + (1, 'fixture-skill', 'workflow', 'testing', + 'skills/fixture.md', 'Ein Fixture-Skill fuer Tests.', '1.0', + 'Das ist der Inhalt des Fixture-Skills fuer den Import-Test.', + 'fixture_hash_abc123', 1) + """) + conn.commit() + conn.close() + + +def _make_wikis_db(path: Path) -> None: + """Erzeugt eine minimale bach.db mit einem Wiki-Artikel.""" + conn = sqlite3.connect(str(path)) + conn.execute(""" + CREATE TABLE wiki_articles ( + id INTEGER PRIMARY KEY, + path TEXT, + title TEXT, + content TEXT, + category TEXT, + last_modified TEXT, + tags TEXT + ) + """) + conn.execute(""" + INSERT INTO wiki_articles + (id, path, title, content, category, last_modified, tags) + VALUES + (1, 'wiki/fixture.md', 'Fixture-Artikel', + 'Inhalt des Fixture-Wiki-Artikels fuer den Import-Test.', + 'testing', '2026-01-01', 'fixture,test') + """) + conn.commit() + conn.close() + + +# --------------------------------------------------------------------------- +# Tests: import_bach_skills – Fallback-Pfade (kein echtes BACH) +# --------------------------------------------------------------------------- + +class TestImportBachSkillsOhneBach: + """Prüft Graceful-Fallback ohne vorhandene bach.db.""" + + def test_kein_pfad_gibt_available_false(self, tmp_path): + """bach_db_path=None → available=False, kein Exception.""" + result = import_bach_skills(None, tmp_path / "knowledge.db") + assert isinstance(result, dict) + assert result.get("available") is False + assert "error" in result + + def test_fehlender_pfad_gibt_available_false(self, tmp_path): + """Nicht existierender Pfad → available=False mit Fehlermeldung.""" + result = import_bach_skills( + tmp_path / "nicht_vorhanden.db", + tmp_path / "knowledge.db", + ) + assert isinstance(result, dict) + assert result.get("available") is False + assert "error" in result + + def test_db_ohne_skills_tabelle_kein_crash(self, tmp_path): + """Leere SQLite-DB ohne skills-Tabelle → available=False, kein Exception.""" + leere_db = tmp_path / "leer.db" + sqlite3.connect(str(leere_db)).close() # erstellt leere DB + result = import_bach_skills(leere_db, tmp_path / "knowledge.db") + assert isinstance(result, dict) + assert result.get("available") is False + assert "error" in result + + def test_rueckgabe_immer_dict(self, tmp_path): + """Rückgabe ist in allen Fehlerpfaden immer ein Dict (nicht None, kein Crash).""" + faelle = [None, "/absolut/kein/pfad/bach.db", tmp_path / "fehlt.db"] + for pfad in faelle: + result = import_bach_skills(pfad, tmp_path / "knowledge.db") + assert isinstance(result, dict), ( + f"Kein Dict zurückgegeben für bach_db_path={pfad!r}" + ) + + +# --------------------------------------------------------------------------- +# Tests: import_bach_skills – Positiv-Pfad mit Fixture-Daten +# --------------------------------------------------------------------------- + +class TestImportBachSkillsMitFixture: + """Positiv-Test mit synthetischer bach.db.""" + + def test_skill_wird_indexiert(self, tmp_path): + """Synthetische bach.db mit einem Skill → available=True, indexed=1.""" + bach_db = tmp_path / "bach.db" + kdb = tmp_path / "knowledge.db" + _make_skills_db(bach_db) + + result = import_bach_skills(bach_db, kdb) + + assert result.get("available") is True, f"Unerwartetes Ergebnis: {result}" + assert result.get("total_skills") == 1 + assert result.get("indexed") == 1 + assert result.get("skipped") == 0 + assert result.get("chunks_created", 0) >= 1 + + +# --------------------------------------------------------------------------- +# Tests: import_bach_wikis – Fallback-Pfade (kein echtes BACH) +# --------------------------------------------------------------------------- + +class TestImportBachWikisOhneBach: + """Prüft Graceful-Fallback ohne vorhandene bach.db.""" + + def test_kein_pfad_gibt_available_false(self, tmp_path): + """bach_db_path=None → available=False, kein Exception.""" + result = import_bach_wikis(None, tmp_path / "knowledge.db") + assert isinstance(result, dict) + assert result.get("available") is False + assert "error" in result + + def test_fehlender_pfad_gibt_available_false(self, tmp_path): + """Nicht existierender Pfad → available=False.""" + result = import_bach_wikis( + tmp_path / "kein_bach.db", + tmp_path / "knowledge.db", + ) + assert isinstance(result, dict) + assert result.get("available") is False + assert "error" in result + + def test_db_ohne_wiki_tabelle_kein_crash(self, tmp_path): + """Leere SQLite-DB ohne wiki_articles-Tabelle → available=False, kein Exception.""" + leere_db = tmp_path / "leer.db" + sqlite3.connect(str(leere_db)).close() + result = import_bach_wikis(leere_db, tmp_path / "knowledge.db") + assert isinstance(result, dict) + assert result.get("available") is False + assert "error" in result + + +# --------------------------------------------------------------------------- +# Tests: import_bach_wikis – Positiv-Pfad mit Fixture-Daten +# --------------------------------------------------------------------------- + +class TestImportBachWikisMitFixture: + """Positiv-Test mit synthetischer bach.db.""" + + def test_wiki_wird_indexiert(self, tmp_path): + """Synthetische bach.db mit einem Wiki-Artikel → available=True, indexed=1.""" + bach_db = tmp_path / "bach.db" + kdb = tmp_path / "knowledge.db" + _make_wikis_db(bach_db) + + result = import_bach_wikis(bach_db, kdb) + + assert result.get("available") is True, f"Unerwartetes Ergebnis: {result}" + assert result.get("total_wikis") == 1 + assert result.get("indexed") == 1 + assert result.get("chunks_created", 0) >= 1 diff --git a/wiki_indexer.py b/wiki_indexer.py index 8f7a0e1..c3d27a0 100644 --- a/wiki_indexer.py +++ b/wiki_indexer.py @@ -14,15 +14,21 @@ 6. FTS5-Index wird automatisch via Trigger befuellt """ -__all__ = ["WikiIndexer"] +__all__ = ["WikiIndexer", "import_bach_wikis"] import sqlite3 from pathlib import Path -from typing import Dict, Optional, Any +from typing import Dict, Optional, Any, Union -from .schema import ensure_schema -from .chunker import chunk_text, estimate_tokens -from .utils import sha256_hash as _sha256, extract_keywords as _extract_keywords +# Relative Imports (Paket-Kontext) mit Fallback auf absolute Imports (sys.path) +try: + from .schema import ensure_schema + from .chunker import chunk_text, estimate_tokens + from .utils import sha256_hash as _sha256, extract_keywords as _extract_keywords +except ImportError: + from schema import ensure_schema # type: ignore[no-redef] + from chunker import chunk_text, estimate_tokens # type: ignore[no-redef] + from utils import sha256_hash as _sha256, extract_keywords as _extract_keywords # type: ignore[no-redef] class WikiIndexer: @@ -249,3 +255,56 @@ def get_index_status(self) -> Dict[str, Any]: 'avg_chunks_per_wiki': round(avg_chunks, 1), 'by_category': {r['category']: r['cnt'] for r in by_category}, } + + +# --------------------------------------------------------------------------- +# Standalone-API (kein Klassen-Overhead, für direkte Nutzung ohne Instanz) +# --------------------------------------------------------------------------- + +def import_bach_wikis( + bach_db_path: Union[str, Path, None], + knowledge_db_path: Union[str, Path], + *, + chunk_size: int = 350, + overlap: int = 0, + force: bool = False, +) -> Dict[str, Any]: + """Importiert BACH-Wiki-Artikel aus bach.db in die knowledge.db. + + Standalone-Funktion: läuft auch ohne echtes BACH graceful durch. + + Gibt immer ein Dict zurück – kein Exception-Throw: + - ``{"available": False, "error": "..."}`` falls BACH nicht verfügbar + - ``{"available": True, "total_wikis": N, ...}`` bei Erfolg + + Args: + bach_db_path: Pfad zur BACH-Datenbank. ``None`` → sofortiges available=False. + knowledge_db_path: Pfad zur Ziel-knowledge.db (wird ggf. neu angelegt). + chunk_size: Wörter pro Chunk (Default: 350). + overlap: Overlap zwischen Chunks in Wörtern. + force: Wenn True, bestehenden Index komplett neu aufbauen. + + Returns: + Dict mit ``"available"`` (bool) plus Statistiken oder Fehlerdetails. + """ + if bach_db_path is None: + return {"available": False, "error": "Kein bach_db_path angegeben."} + + path = Path(bach_db_path) + if not path.exists(): + return {"available": False, "error": f"bach.db nicht gefunden: {path}"} + + indexer = WikiIndexer(Path(knowledge_db_path)) + try: + result = indexer.index_from_bach( + path, chunk_size=chunk_size, overlap=overlap, force=force + ) + if "error" in result: + return {"available": False, **result} + result["available"] = True + return result + except sqlite3.Error as exc: + # Z.B. wenn bach.db existiert, aber die wiki_articles-Tabelle fehlt. + return {"available": False, "error": f"SQLite-Fehler beim BACH-Wiki-Import: {exc}"} + finally: + indexer.close()