#!/usr/bin/env python3 """Functionality matrix — every destination, live. One PASS/FAIL/SKIP row per target or stage against real services: destinations : obsidian (notes only) + ten vector stores per target : ingest → re-ingest idempotency → retrieval assertions graphs : LightRAG and Graphify engines (one document each) archives : MinIO object stage + filesystem bz2 stage ask : Vanna natural-language SQL (pgvector + mariadb) Exit code 0 iff no FAIL. Services read the standard env (PG*, MARIADB_*, MINIO_*); missing services degrade to SKIP, never crash. Run: .venv/bin/python scripts/func_test.py """ from __future__ import annotations import os import shutil import subprocess import sys import tempfile import time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) LAUNCHER = ROOT / "thicket.py" RESULTS: list[tuple[str, str, str]] = [] # (section, name, outcome) # Local service credentials for the standing test stack. os.environ.setdefault("PGHOST", "localhost") os.environ.setdefault("PGUSER", "thicket") os.environ.setdefault("PGPASSWORD", "thicket") os.environ.setdefault("PGDATABASE", "thicket") os.environ.setdefault("MARIADB_HOST", "127.0.0.1") os.environ.setdefault("MARIADB_USER", "root") os.environ.setdefault("MARIADB_DATABASE", "thicket") os.environ.setdefault("MINIO_ENDPOINT", "127.0.0.1:9000") os.environ.setdefault("MINIO_ACCESS_KEY", "thicket") os.environ.setdefault("MINIO_SECRET_KEY", "thicket-secret") EMBED_MODEL = "jinaai/jina-embeddings-v2-base-code" COLLECTION = "thicket_func" CORPUS = { "tls-rotation.sh": ( "#!/usr/bin/env bash\n" "set -euo pipefail\n" "# rotate tls certificates weekly and reload the edge\n" "certbot renew --quiet --deploy-hook \"systemctl reload nginx\"\n" "find /etc/letsencrypt/archive -mtime +90 -delete\n" ), "timeout-policy.md": ( "# Edge Timeout Policy\n\n" "Production proxies apply strict connection timeouts.\n\n" "## nginx block\n\n" "```nginx\n" "proxy_connect_timeout 300s;\n" "proxy_read_timeout 300s;\n" "client_max_body_size 25m;\n" "```\n\n" "The 300 second ceiling bounds slow-loris exposure.\n" ), "pasta.txt": ( "Kitchen notes: the asparagus pasta with basil pesto needs the\n" "noodles pulled a minute early — residual heat finishes them.\n" ), } QUERIES = [ # (query, expected document stem) ("rotate tls certificates weekly", "tls-rotation"), ("connection timeout 300 nginx", "timeout-policy"), ("asparagus pasta recipe", "pasta"), ] def record(section: str, name: str, outcome: str, detail: str = "") -> None: tag = outcome if not detail else f"{outcome} — {detail}" RESULTS.append((section, name, tag)) print(f" [{outcome:4s}] {name:28s} {detail}") def run_cli(args: list[str], timeout: int = 600) -> tuple[int, str]: proc = subprocess.run( [sys.executable, str(LAUNCHER), *args], capture_output=True, text=True, timeout=timeout, cwd=ROOT, ) return proc.returncode, proc.stdout + proc.stderr def make_corpus(base: Path) -> Path: docs = base / "cold" docs.mkdir(parents=True) for name, body in CORPUS.items(): (docs / name).write_text(body, encoding="utf-8") return docs def fresh_vault(base: Path, name: str) -> Path: vault = base / f"vault-{name}" vault.mkdir(parents=True, exist_ok=True) return vault def search_once(target: str, vault: Path, query: str, port: int = 6333): from thicket.vector_stores import create_store engine = search_once._engine store = create_store(target, collection=COLLECTION, dim=engine.dim, host="localhost", port=port, data_dir=vault / ".thicket" / target) store.set_embedder(engine) try: store.ensure_collection() return store.search(engine.embed_query(query), limit=1) finally: store.close() def drop_stale() -> None: """The matrix owns its collection name; any earlier state at that name (including other dimension eras) goes before the run.""" try: from qdrant_client import QdrantClient c = QdrantClient(url="http://localhost:6333", check_compatibility=False) if c.collection_exists(COLLECTION): c.delete_collection(COLLECTION) except Exception: pass try: import psycopg with psycopg.connect(connect_timeout=3) as conn: conn.execute(f'DROP TABLE IF EXISTS "{COLLECTION}"') conn.commit() except Exception: pass try: import pymysql conn = pymysql.connect(host="127.0.0.1", user="root", database="thicket", connect_timeout=3) with conn.cursor() as cur: cur.execute(f"DROP TABLE IF EXISTS `{COLLECTION}`") conn.commit(); conn.close() except Exception: pass try: import weaviate client = weaviate.connect_to_local( host="127.0.0.1", port=8080, grpc_port=50051) mapped = "Thicket_func" if client.collections.exists(mapped): client.collections.delete(mapped) client.close() except Exception: pass def phase_destinations(base: Path) -> None: from thicket.embedder import EmbeddingEngine from thicket.vector_stores import TARGETS drop_stale() engine = EmbeddingEngine(EMBED_MODEL) engine.load() search_once._engine = engine print("\n== destinations ==") # obsidian: notes only docs = make_corpus(base / "obsidian") vault = fresh_vault(base, "obsidian") rc, out = run_cli(["--ingest", str(docs), "--vault", str(vault), "--target", "obsidian", "--no-qdrant"]) notes = sorted(p.name for p in (vault / "Ingested_Brain").glob("*.md")) ok = rc == 0 and len(notes) == 3 and "INDEX" not in out record("destinations", "obsidian", "PASS" if ok else "FAIL", f"notes={notes}" if ok else out[-160:]) parity: dict[str, float] = {} for target in TARGETS: docs = make_corpus(base / target) vault = fresh_vault(base, target) port = 8080 if target == "weaviate" else 6333 rc, out = run_cli(["--ingest", str(docs), "--vault", str(vault), "--target", target, "--port", str(port), "--collection", COLLECTION]) if rc != 0 or "0 failed" not in out: record("destinations", target, "FAIL", out.strip()[-140:]) continue try: first = search_once(target, vault, QUERIES[0][0], port) rc2, _ = run_cli(["--ingest", str(docs), "--vault", str(vault), "--target", target, "--port", str(port), "--collection", COLLECTION]) second = search_once(target, vault, QUERIES[0][0], port) except Exception as e: # noqa: BLE001 record("destinations", target, "FAIL", f"search: {e}") continue hit_ok = all( search_once(target, vault, q, port)[0]["payload"] ["document_title"].lower().replace(" ", "-").startswith(exp.split("-")[0]) for q, exp in QUERIES[:1] ) idem = (rc2 == 0 and abs(first[0]["score"] - second[0]["score"]) < 1e-3 and second[0]["payload"]["document_title"] == first[0]["payload"]["document_title"]) parity[target] = first[0]["score"] record("destinations", target, "PASS" if (hit_ok and idem) else "FAIL", f"top={first[0]['payload']['document_title'][:22]!r} " f"score={first[0]['score']:.3f} idempotent={idem}") if len(parity) >= 2: scores = sorted(parity.values()) spread = scores[-1] - scores[0] record("destinations", "score parity (all)", "PASS" if spread <= 0.02 else "WARN", f"spread={spread:.4f}") def phase_graphs(base: Path) -> None: print("\n== graph engines ==") for engine, marker in (("lightrag", ".lightrag"), ("graphify", ".graphify")): docs = make_corpus(base / f"g-{engine}") docs_extra = docs / "tls-rotation.sh" docs_extra.write_text(CORPUS["tls-rotation.sh"], encoding="utf-8") vault = fresh_vault(base, f"g-{engine}") rc, out = run_cli(["--ingest", str(docs), "--vault", str(vault), "--target", "obsidian", "--no-qdrant", "--lightrag", "--graph-engine", engine, "--ollama-llm", "llama3.1:latest"], timeout=900) if rc != 0: record("graphs", engine, "FAIL", out.strip()[-160:]) continue if engine == "graphify": ok = (vault / ".graphify" / "graphify-out" / "graph.json").exists() else: graph_dir = vault / ".lightrag" ok = graph_dir.exists() and any(graph_dir.iterdir()) record("graphs", engine, "PASS" if ok else "FAIL", f"artifacts={'yes' if ok else 'missing'}") def phase_archives(base: Path) -> None: import bz2 print("\n== archive stages ==") # filesystem bz2 docs = make_corpus(base / "fsarch") vault = fresh_vault(base, "fsarch") archive = base / "ingested-archive" rc, out = run_cli(["--ingest", str(docs), "--vault", str(vault), "--target", "obsidian", "--no-qdrant", "--archive", "--archive-dir", str(archive)]) packed = sorted(archive.glob("*.bz2")) if archive.exists() else [] roundtrip = bool(packed) and all( bz2.decompress(p.read_bytes()) for p in packed) empty_in = not any(docs.iterdir()) record("archives", "filesystem bz2", "PASS" if (rc == 0 and roundtrip and empty_in) else "FAIL", f"{len(packed)} objects, roundtrip={roundtrip}") # MinIO object stage try: from minio import Minio client = Minio("127.0.0.1:9000", access_key="thicket", secret_key="thicket-secret", secure=False) if not client.bucket_exists("thicket-corpus"): client.make_bucket("thicket-corpus") except Exception as e: # noqa: BLE001 record("archives", "minio objects", "SKIP", f"service: {e}") return docs = make_corpus(base / "minioarch") vault = fresh_vault(base, "minioarch") rc, out = run_cli(["--ingest", str(docs), "--vault", str(vault), "--target", "obsidian", "--no-qdrant", "--minio"]) objects = {o.object_name for o in client.list_objects("thicket-corpus", recursive=True)} uris = "source_uri" in (vault / "Ingested_Brain" / "pasta.md").read_text() expected = set(CORPUS) ok = rc == 0 and uris and expected & objects == expected record("archives", "minio objects", "PASS" if ok else "FAIL", f"{len(expected & objects)}/{len(expected)} objects, uri_in_note={uris}") def phase_ask(base: Path) -> None: print("\n== ask (Vanna + Ollama) ==") from thicket.ask_vanna import SQL_TARGETS, ask for target in SQL_TARGETS: try: answer = ask("How many rows are in the corpus table?", target=target, llm_model="llama3.1:latest") ok = bool(answer.strip()) and "ERROR" not in answer record("ask", target, "PASS" if ok else "FAIL", answer.strip().replace("\n", " ")[:60]) except Exception as e: # noqa: BLE001 record("ask", target, "FAIL", str(e)[:100]) def main() -> int: started = time.time() print(f"thicket functionality matrix — {time.strftime('%Y-%m-%d %H:%M')}") base = Path(tempfile.mkdtemp(prefix="thicket-func-")) try: phase_destinations(base) phase_graphs(base) phase_archives(base) phase_ask(base) finally: shutil.rmtree(base, ignore_errors=True) fails = [r for r in RESULTS if r[2].startswith("FAIL")] print(f"\n{'=' * 60}") print(f"{len(RESULTS)} checks, {len(fails)} failed " f"({time.time() - started:.0f}s)") return 1 if fails else 0 if __name__ == "__main__": sys.exit(main())