codegraph-voyage 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,325 @@
1
+ """Sidecar SQLite database for embedding storage.
2
+
3
+ The sidecar stores embeddings keyed by node identity, source content hash,
4
+ model, dimensions, and dtype. It supports incremental indexing (skip unchanged
5
+ records, remove stale records) and segregated indices for incompatible metadata.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sqlite3
11
+ import time
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from .providers import EmbeddingProvider
16
+
17
+ SCHEMA_VERSION = 2
18
+
19
+ SIDECAR_SCHEMA = """
20
+ CREATE TABLE IF NOT EXISTS sidecar_meta (
21
+ key TEXT PRIMARY KEY,
22
+ value TEXT NOT NULL
23
+ );
24
+
25
+ CREATE TABLE IF NOT EXISTS embeddings (
26
+ node_id TEXT NOT NULL,
27
+ source_content_hash TEXT NOT NULL,
28
+ model TEXT NOT NULL,
29
+ dimensions INTEGER NOT NULL,
30
+ dtype TEXT NOT NULL DEFAULT 'float32',
31
+ embedding BLOB NOT NULL,
32
+ node_kind TEXT,
33
+ name TEXT,
34
+ qualified_name TEXT,
35
+ file_path TEXT,
36
+ language TEXT,
37
+ start_line INTEGER,
38
+ end_line INTEGER,
39
+ document_text TEXT,
40
+ created_at INTEGER NOT NULL,
41
+ PRIMARY KEY (node_id, source_content_hash, model, dimensions, dtype)
42
+ );
43
+
44
+ CREATE INDEX IF NOT EXISTS idx_embeddings_file_path
45
+ ON embeddings(file_path);
46
+ CREATE INDEX IF NOT EXISTS idx_embeddings_name
47
+ ON embeddings(name);
48
+ CREATE INDEX IF NOT EXISTS idx_embeddings_content_hash
49
+ ON embeddings(source_content_hash);
50
+ """
51
+
52
+
53
+ class SidecarError(Exception):
54
+ """Raised on sidecar DB operations."""
55
+
56
+
57
+ class SidecarDB:
58
+ """Manages the sidecar SQLite database for embedding storage.
59
+
60
+ The sidecar is read/write. The CodeGraph DB is always opened read-only.
61
+ """
62
+
63
+ def __init__(self, path: str | Path):
64
+ self._path = Path(path)
65
+ self._conn: sqlite3.Connection | None = None
66
+
67
+ def open(self) -> None:
68
+ """Open (or create) the sidecar database and apply schema."""
69
+ self._path.parent.mkdir(parents=True, exist_ok=True)
70
+ self._conn = sqlite3.connect(str(self._path))
71
+ self._conn.execute("PRAGMA journal_mode = WAL")
72
+ self._conn.execute("PRAGMA synchronous = NORMAL")
73
+ self._migrate_legacy_primary_key()
74
+ self._conn.executescript(SIDECAR_SCHEMA)
75
+ # Track schema version
76
+ cur = self._conn.execute(
77
+ "SELECT value FROM sidecar_meta WHERE key = 'schema_version'"
78
+ )
79
+ row = cur.fetchone()
80
+ if row is None:
81
+ self._conn.execute(
82
+ "INSERT INTO sidecar_meta (key, value) VALUES ('schema_version', ?)",
83
+ (str(SCHEMA_VERSION),),
84
+ )
85
+ else:
86
+ self._conn.execute(
87
+ "UPDATE sidecar_meta SET value = ? WHERE key = 'schema_version'",
88
+ (str(SCHEMA_VERSION),),
89
+ )
90
+ self._conn.commit()
91
+
92
+ def _migrate_legacy_primary_key(self) -> None:
93
+ """Upgrade the v1 key without discarding existing embeddings."""
94
+ exists = self.conn.execute(
95
+ "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'embeddings'"
96
+ ).fetchone()
97
+ if not exists:
98
+ return
99
+ pk_columns = [
100
+ row[1]
101
+ for row in sorted(
102
+ (row for row in self.conn.execute("PRAGMA table_info(embeddings)") if row[5]),
103
+ key=lambda row: row[5],
104
+ )
105
+ ]
106
+ expected = ["node_id", "source_content_hash", "model", "dimensions", "dtype"]
107
+ if pk_columns == expected:
108
+ return
109
+ with self.conn:
110
+ self.conn.execute("DROP INDEX IF EXISTS idx_embeddings_file_path")
111
+ self.conn.execute("DROP INDEX IF EXISTS idx_embeddings_name")
112
+ self.conn.execute("DROP INDEX IF EXISTS idx_embeddings_content_hash")
113
+ self.conn.execute("ALTER TABLE embeddings RENAME TO embeddings_legacy")
114
+ self.conn.executescript(SIDECAR_SCHEMA)
115
+ self.conn.execute(
116
+ """INSERT OR REPLACE INTO embeddings
117
+ SELECT node_id, source_content_hash, model, dimensions, dtype,
118
+ embedding, node_kind, name, qualified_name, file_path,
119
+ language, start_line, end_line, document_text, created_at
120
+ FROM embeddings_legacy"""
121
+ )
122
+ self.conn.execute("DROP TABLE embeddings_legacy")
123
+
124
+ def close(self) -> None:
125
+ if self._conn:
126
+ self._conn.close()
127
+ self._conn = None
128
+
129
+ @property
130
+ def conn(self) -> sqlite3.Connection:
131
+ if self._conn is None:
132
+ raise SidecarError("SidecarDB not opened. Call open() first.")
133
+ return self._conn
134
+
135
+ def store_embeddings(
136
+ self,
137
+ records: list[dict[str, Any]],
138
+ provider: EmbeddingProvider,
139
+ ) -> int:
140
+ """Store embedding records, replacing existing ones.
141
+
142
+ Args:
143
+ records: List of dicts with keys:
144
+ node_id, content_hash, embedding (list[float]), node_kind,
145
+ name, qualified_name, file_path, language, start_line,
146
+ end_line, document_text
147
+ provider: The embedding provider (for model/dimensions metadata).
148
+
149
+ Returns:
150
+ Number of records stored.
151
+ """
152
+ now = int(time.time())
153
+ model = provider.model_name
154
+ dims = provider.dimensions
155
+ raw_bytes = provider.raw_bytes
156
+
157
+ count = 0
158
+ # Validate the complete batch before entering one atomic transaction.
159
+ for rec in records:
160
+ if len(rec["embedding"]) != dims:
161
+ raise SidecarError(
162
+ f"Embedding dimension mismatch for node {rec['node_id']}: "
163
+ f"expected {dims}, got {len(rec['embedding'])}"
164
+ )
165
+ with self.conn:
166
+ for rec in records:
167
+ emb_bytes = raw_bytes(rec["embedding"])
168
+ self.conn.execute(
169
+ """INSERT OR REPLACE INTO embeddings
170
+ (node_id, source_content_hash, model, dimensions, dtype,
171
+ embedding, node_kind, name, qualified_name, file_path,
172
+ language, start_line, end_line, document_text, created_at)
173
+ VALUES (?, ?, ?, ?, 'float32', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
174
+ (
175
+ rec["node_id"], rec["content_hash"], model, dims, emb_bytes,
176
+ rec.get("node_kind", ""), rec.get("name", ""),
177
+ rec.get("qualified_name", ""), rec.get("file_path", ""),
178
+ rec.get("language", ""), rec.get("start_line"),
179
+ rec.get("end_line"), rec.get("document_text", ""), now,
180
+ ),
181
+ )
182
+ self.conn.execute(
183
+ """DELETE FROM embeddings
184
+ WHERE node_id = ? AND model = ? AND dimensions = ?
185
+ AND dtype = 'float32' AND source_content_hash <> ?""",
186
+ (rec["node_id"], model, dims, rec["content_hash"]),
187
+ )
188
+ count += 1
189
+ return count
190
+
191
+ def remove_stale_records(
192
+ self,
193
+ current_ids: set[str],
194
+ provider: EmbeddingProvider,
195
+ ) -> int:
196
+ """Remove embeddings for nodes no longer in the index.
197
+
198
+ Args:
199
+ current_ids: Set of node IDs that are current.
200
+ provider: The embedding provider (for model/dimensions filter).
201
+
202
+ Returns:
203
+ Number of records removed.
204
+ """
205
+ if not current_ids:
206
+ result = self.conn.execute(
207
+ """DELETE FROM embeddings
208
+ WHERE model = ? AND dimensions = ? AND dtype = 'float32'""",
209
+ (provider.model_name, provider.dimensions),
210
+ )
211
+ self.conn.commit()
212
+ return result.rowcount
213
+ placeholders = ",".join("?" for _ in current_ids)
214
+ result = self.conn.execute(
215
+ f"""DELETE FROM embeddings
216
+ WHERE model = ? AND dimensions = ? AND dtype = 'float32'
217
+ AND node_id NOT IN ({placeholders})""",
218
+ (provider.model_name, provider.dimensions, *current_ids),
219
+ )
220
+ self.conn.commit()
221
+ return result.rowcount
222
+
223
+ def find_changed_nodes(
224
+ self,
225
+ documents: list[dict[str, Any]],
226
+ provider: EmbeddingProvider,
227
+ ) -> list[dict[str, Any]]:
228
+ """Return only documents whose content hash has changed or are new.
229
+
230
+ Compares against existing embeddings for the same model/dimensions.
231
+ """
232
+ model = provider.model_name
233
+ dims = provider.dimensions
234
+ result: list[dict[str, Any]] = []
235
+ for doc in documents:
236
+ row = self.conn.execute(
237
+ """SELECT 1 FROM embeddings
238
+ WHERE node_id = ? AND source_content_hash = ?
239
+ AND model = ? AND dimensions = ? AND dtype = 'float32'""",
240
+ (doc["node_id"], doc["content_hash"], model, dims),
241
+ ).fetchone()
242
+ if row is None:
243
+ result.append(doc)
244
+ return result
245
+
246
+ def get_embedding(
247
+ self, node_id: str, provider: EmbeddingProvider
248
+ ) -> list[float] | None:
249
+ """Retrieve a single embedding by node ID."""
250
+ row = self.conn.execute(
251
+ """SELECT embedding FROM embeddings
252
+ WHERE node_id = ? AND model = ? AND dimensions = ? AND dtype = 'float32'""",
253
+ (node_id, provider.model_name, provider.dimensions),
254
+ ).fetchone()
255
+ if row is None:
256
+ return None
257
+ return provider.from_bytes(row[0])
258
+
259
+ def get_all_embeddings(
260
+ self, provider: EmbeddingProvider
261
+ ) -> list[dict[str, Any]]:
262
+ """Retrieve all embeddings for the given provider."""
263
+ rows = self.conn.execute(
264
+ """SELECT node_id, embedding, name, qualified_name, file_path,
265
+ start_line, end_line, node_kind, language, document_text
266
+ FROM embeddings
267
+ WHERE model = ? AND dimensions = ? AND dtype = 'float32'""",
268
+ (provider.model_name, provider.dimensions),
269
+ ).fetchall()
270
+ results: list[dict[str, Any]] = []
271
+ for row in rows:
272
+ results.append({
273
+ "node_id": row[0],
274
+ "embedding": provider.from_bytes(row[1]),
275
+ "name": row[2] or "",
276
+ "qualified_name": row[3] or "",
277
+ "file_path": row[4] or "",
278
+ "start_line": row[5],
279
+ "end_line": row[6],
280
+ "node_kind": row[7] or "",
281
+ "language": row[8] or "",
282
+ "document_text": row[9] or "",
283
+ })
284
+ return results
285
+
286
+ def get_status(self) -> dict[str, Any]:
287
+ """Return status information about the sidecar."""
288
+ if self._conn is None:
289
+ return {"connected": False}
290
+ try:
291
+ total = self.conn.execute(
292
+ "SELECT COUNT(*) FROM embeddings"
293
+ ).fetchone()[0]
294
+ models = [
295
+ list(r)
296
+ for r in self.conn.execute(
297
+ "SELECT model, dimensions, dtype, COUNT(*) FROM embeddings "
298
+ "GROUP BY model, dimensions, dtype"
299
+ ).fetchall()
300
+ ]
301
+ schema_v = self.conn.execute(
302
+ "SELECT value FROM sidecar_meta WHERE key = 'schema_version'"
303
+ ).fetchone()
304
+ return {
305
+ "connected": True,
306
+ "path": str(self._path),
307
+ "total_embeddings": total,
308
+ "model_groups": models,
309
+ "schema_version": int(schema_v[0]) if schema_v else None,
310
+ }
311
+ except Exception as exc:
312
+ return {"connected": True, "error": str(exc)}
313
+
314
+ def clear(self, provider: EmbeddingProvider | None = None) -> int:
315
+ """Clear embeddings, optionally filtered by provider."""
316
+ if provider:
317
+ result = self.conn.execute(
318
+ """DELETE FROM embeddings
319
+ WHERE model = ? AND dimensions = ? AND dtype = 'float32'""",
320
+ (provider.model_name, provider.dimensions),
321
+ )
322
+ else:
323
+ result = self.conn.execute("DELETE FROM embeddings")
324
+ self.conn.commit()
325
+ return result.rowcount
@@ -0,0 +1 @@
1
+ """Tests for codegraph-voyage."""
@@ -0,0 +1,278 @@
1
+ #!/usr/bin/env python3
2
+ """Benchmark for codegraph-voyage retrieval quality.
3
+
4
+ This is a **smoke benchmark** — the corpus is small and synthetic, so no
5
+ quality-lift claims are made. It tests that the three retrieval strategies
6
+ (lexical, vector, fused) produce measurable output and that fused can
7
+ improve over the worst single-strategy for at least one conceptual case.
8
+
9
+ Metrics reported:
10
+ - Recall@5
11
+ - MRR (Mean Reciprocal Rank)
12
+ - NDCG@10
13
+
14
+ Run:
15
+ python -m tools.codegraph_voyage.tests.benchmark [--verbose]
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import math
22
+ import sys
23
+ import time
24
+ from pathlib import Path
25
+ from typing import Any
26
+
27
+ # Make sure the project root is importable
28
+ # This script is at tools/codegraph_voyage/tests/benchmark.py
29
+ _PROJECT_ROOT = Path(__file__).resolve().parents[3]
30
+ if str(_PROJECT_ROOT) not in sys.path:
31
+ sys.path.insert(0, str(_PROJECT_ROOT))
32
+
33
+ from tools.codegraph_voyage.providers import FakeEmbeddingProvider
34
+ from tools.codegraph_voyage.ranking import (
35
+ RankingResult,
36
+ reciprocal_rank_fusion,
37
+ rank_by_lexical_similarity,
38
+ rank_by_vector_similarity,
39
+ find_pinned_candidates,
40
+ merge_pinned_into_results,
41
+ )
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Fixture: a small synthetic corpus of symbol documents with ground-truth
46
+ # relevance judgments per query.
47
+ # ---------------------------------------------------------------------------
48
+
49
+ FIXTURE_DOCS: list[dict[str, Any]] = [
50
+ {
51
+ "node_id": "d1", "name": "AuthService", "qualified_name": "app.auth.AuthService",
52
+ "file_path": "src/auth/service.py", "node_kind": "class", "language": "python",
53
+ "start_line": 1, "end_line": 50,
54
+ "document_text": "AuthService handles user authentication login logout password hashing JWT token generation and session management",
55
+ },
56
+ {
57
+ "node_id": "d2", "name": "UserModel", "qualified_name": "app.models.UserModel",
58
+ "file_path": "src/models/user.py", "node_kind": "class", "language": "python",
59
+ "start_line": 1, "end_line": 80,
60
+ "document_text": "UserModel represents a user account with fields for username email password_hash and profile data",
61
+ },
62
+ {
63
+ "node_id": "d3", "name": "PaymentGateway", "qualified_name": "app.payments.PaymentGateway",
64
+ "file_path": "src/payments/gateway.py", "node_kind": "class", "language": "python",
65
+ "start_line": 1, "end_line": 120,
66
+ "document_text": "PaymentGateway processes credit card payments refunds and subscription billing via Stripe API",
67
+ },
68
+ {
69
+ "node_id": "d4", "name": "InvoiceGenerator", "qualified_name": "app.billing.InvoiceGenerator",
70
+ "file_path": "src/billing/invoice.py", "node_kind": "class", "language": "python",
71
+ "start_line": 1, "end_line": 60,
72
+ "document_text": "InvoiceGenerator creates PDF invoices for completed payments and sends them via email",
73
+ },
74
+ {
75
+ "node_id": "d5", "name": "login", "qualified_name": "app.auth.login",
76
+ "file_path": "src/auth/views.py", "node_kind": "function", "language": "python",
77
+ "start_line": 10, "end_line": 25,
78
+ "document_text": "login view function authenticates user credentials and returns a JWT token",
79
+ },
80
+ {
81
+ "node_id": "d6", "name": "hash_password", "qualified_name": "app.auth.hash_password",
82
+ "file_path": "src/auth/utils.py", "node_kind": "function", "language": "python",
83
+ "start_line": 5, "end_line": 15,
84
+ "document_text": "hash_password takes a plaintext password and returns a bcrypt hash for secure storage",
85
+ },
86
+ {
87
+ "node_id": "d7", "name": "send_email", "qualified_name": "app.notifications.send_email",
88
+ "file_path": "src/notifications/email.py", "node_kind": "function", "language": "python",
89
+ "start_line": 1, "end_line": 30,
90
+ "document_text": "send_email dispatches transactional emails via SMTP for invoices and notifications",
91
+ },
92
+ {
93
+ "node_id": "d8", "name": "generate_report", "qualified_name": "app.reports.generate_report",
94
+ "file_path": "src/reports/generator.py", "node_kind": "function", "language": "python",
95
+ "start_line": 1, "end_line": 45,
96
+ "document_text": "generate_report produces CSV and JSON reports from database query results for business analytics",
97
+ },
98
+ {
99
+ "node_id": "d9", "name": "DatabaseConnection", "qualified_name": "app.db.DatabaseConnection",
100
+ "file_path": "src/db/connection.py", "node_kind": "class", "language": "python",
101
+ "start_line": 1, "end_line": 90,
102
+ "document_text": "DatabaseConnection manages connection pooling SQL query execution and transaction management for PostgreSQL",
103
+ },
104
+ {
105
+ "node_id": "d10", "name": "CacheManager", "qualified_name": "app.cache.CacheManager",
106
+ "file_path": "src/cache/manager.py", "node_kind": "class", "language": "python",
107
+ "start_line": 1, "end_line": 60,
108
+ "document_text": "CacheManager provides Redis-backed caching for frequently accessed database queries and API responses",
109
+ },
110
+ ]
111
+
112
+ # Queries with ground-truth relevant node IDs (ordered by decreasing relevance)
113
+ QUERIES: list[tuple[str, list[str], str]] = [
114
+ ("user authentication login", ["d1", "d5", "d2", "d6"], "auth-related symbols"),
115
+ ("payment processing billing", ["d3", "d4", "d7", "d1"], "payment/billing"),
116
+ ("database query caching", ["d9", "d10", "d8"], "data layer"),
117
+ ("email notification invoice", ["d7", "d4", "d2"], "email/billing"),
118
+ ("AuthService", ["d1", "d5", "d6"], "exact identifier AuthService"),
119
+ ]
120
+
121
+
122
+ def dcg(relevances: list[float]) -> float:
123
+ """Discounted cumulative gain."""
124
+ return sum(
125
+ rel / math.log2(i + 2) if i > 0 else rel
126
+ for i, rel in enumerate(relevances)
127
+ )
128
+
129
+
130
+ def ndcg(ranked_ids: list[str], relevant: set[str], k: int = 10) -> float:
131
+ """NDCG@k."""
132
+ ranked = ranked_ids[:k]
133
+ # Binary relevance: 1 if in relevant set
134
+ relevances = [1.0 if rid in relevant else 0.0 for rid in ranked]
135
+ ideal = sorted(relevances, reverse=True)
136
+ dcg_val = dcg(relevances)
137
+ idcg_val = dcg(ideal)
138
+ return dcg_val / idcg_val if idcg_val > 0 else 0.0
139
+
140
+
141
+ def recall_at_k(ranked_ids: list[str], relevant: set[str], k: int = 5) -> float:
142
+ """Recall@k."""
143
+ if not relevant:
144
+ return 0.0
145
+ found = sum(1 for rid in ranked_ids[:k] if rid in relevant)
146
+ return found / len(relevant)
147
+
148
+
149
+ def mrr(ranked_ids: list[str], relevant: set[str]) -> float:
150
+ """Mean Reciprocal Rank."""
151
+ for i, rid in enumerate(ranked_ids):
152
+ if rid in relevant:
153
+ return 1.0 / (i + 1)
154
+ return 0.0
155
+
156
+
157
+ def run_benchmark(verbose: bool = False) -> dict[str, Any]:
158
+ """Run benchmark and return metrics."""
159
+ provider = FakeEmbeddingProvider(dimensions=64)
160
+
161
+ # Pre-compute embeddings for all docs
162
+ texts = [d["document_text"] for d in FIXTURE_DOCS]
163
+ embeddings = provider.embed_documents(texts, input_type="document")
164
+
165
+ # Build candidate dicts for ranking functions
166
+ vector_candidates: list[dict[str, Any]] = []
167
+ lexical_candidates: list[dict[str, Any]] = []
168
+ for doc, emb in zip(FIXTURE_DOCS, embeddings):
169
+ entry = {
170
+ "node_id": doc["node_id"],
171
+ "name": doc["name"],
172
+ "qualified_name": doc["qualified_name"],
173
+ "file_path": doc["file_path"],
174
+ "node_kind": doc["node_kind"],
175
+ "language": doc["language"],
176
+ "start_line": doc["start_line"],
177
+ "end_line": doc["end_line"],
178
+ "document_text": doc["document_text"],
179
+ "embedding": emb,
180
+ }
181
+ vector_candidates.append(entry)
182
+ lexical_candidates.append({k: v for k, v in entry.items() if k != "embedding"})
183
+
184
+ all_metrics: dict[str, dict[str, float]] = {
185
+ "lexical": {"recall@5": 0.0, "mrr": 0.0, "ndcg@10": 0.0},
186
+ "vector": {"recall@5": 0.0, "mrr": 0.0, "ndcg@10": 0.0},
187
+ "fused": {"recall@5": 0.0, "mrr": 0.0, "ndcg@10": 0.0},
188
+ }
189
+
190
+ for query_str, relevant_ids, label in QUERIES:
191
+ relevant = set(relevant_ids)
192
+ query_vector = provider.embed_query(query_str, input_type="query")
193
+
194
+ # Lexical
195
+ lex_results = rank_by_lexical_similarity(query_str, lexical_candidates, top_k=10)
196
+ lex_ids = [r.node_id for r in lex_results]
197
+
198
+ # Vector
199
+ vec_results = rank_by_vector_similarity(query_vector, vector_candidates, top_k=10)
200
+ vec_ids = [r.node_id for r in vec_results]
201
+
202
+ # Fused (RRF)
203
+ fused = reciprocal_rank_fusion([vec_results, lex_results], weights=[0.5, 0.5], k=60)
204
+ # Merge pinned
205
+ pinned = find_pinned_candidates(query_str, lexical_candidates)
206
+ if pinned:
207
+ fused = merge_pinned_into_results(pinned, fused)
208
+ fused_ids = [r.node_id for r in fused]
209
+
210
+ if verbose:
211
+ print(f"\nQuery: {query_str!r} ({label})")
212
+ print(f" Lexical top-5: {lex_ids[:5]}")
213
+ print(f" Vector top-5: {vec_ids[:5]}")
214
+ print(f" Fused top-5: {fused_ids[:5]}")
215
+ print(f" Relevant: {relevant_ids}")
216
+
217
+ for strategy_name, ids in [("lexical", lex_ids), ("vector", vec_ids), ("fused", fused_ids)]:
218
+ all_metrics[strategy_name]["recall@5"] += recall_at_k(ids, relevant, k=5)
219
+ all_metrics[strategy_name]["mrr"] += mrr(ids, relevant)
220
+ all_metrics[strategy_name]["ndcg@10"] += ndcg(ids, relevant, k=10)
221
+
222
+ n = len(QUERIES)
223
+ for strategy in all_metrics:
224
+ for metric in all_metrics[strategy]:
225
+ all_metrics[strategy][metric] = round(all_metrics[strategy][metric] / n, 4)
226
+
227
+ return all_metrics
228
+
229
+
230
+ def main() -> int:
231
+ from pathlib import Path # noqa: F811
232
+
233
+ ap = argparse.ArgumentParser(description="codegraph-voyage benchmark")
234
+ ap.add_argument("--verbose", "-v", action="store_true", help="Show per-query details")
235
+ args = ap.parse_args()
236
+
237
+ print("=" * 60)
238
+ print("codegraph-voyage retrieval benchmark (smoke)")
239
+ print("=" * 60)
240
+ print(f"Corpus: {len(FIXTURE_DOCS)} documents")
241
+ print(f"Queries: {len(QUERIES)}")
242
+ print(f"Provider: FakeEmbeddingProvider(dimensions=64)")
243
+ print()
244
+
245
+ t0 = time.time()
246
+ metrics = run_benchmark(verbose=args.verbose)
247
+ elapsed = time.time() - t0
248
+
249
+ print(f"\nResults ({elapsed:.2f}s):")
250
+ print(f"{'Strategy':<10} {'Recall@5':>10} {'MRR':>10} {'NDCG@10':>10}")
251
+ print("-" * 42)
252
+ for strategy in ["lexical", "vector", "fused"]:
253
+ m = metrics[strategy]
254
+ print(f"{strategy:<10} {m['recall@5']:>10.4f} {m['mrr']:>10.4f} {m['ndcg@10']:>10.4f}")
255
+
256
+ # Compare like-for-like metrics against the stronger single strategy.
257
+ comparisons = [
258
+ metrics["fused"][metric] >= max(metrics["lexical"][metric], metrics["vector"][metric])
259
+ for metric in ("recall@5", "mrr", "ndcg@10")
260
+ ]
261
+ strict_improvement = any(
262
+ metrics["fused"][metric] > max(metrics["lexical"][metric], metrics["vector"][metric])
263
+ for metric in ("recall@5", "mrr", "ndcg@10")
264
+ )
265
+
266
+ if all(comparisons) and strict_improvement:
267
+ print("\n✓ Fused improves like-for-like over the single strategies.")
268
+ else:
269
+ print("\n⚠ Fused did not improve like-for-like on all metrics (smoke corpus limitation).")
270
+
271
+ print("\n⚠ Caveat: This is a SMOKE benchmark with a tiny synthetic corpus.")
272
+ print(" No quality-lift claims should be made from these results.")
273
+
274
+ return 0
275
+
276
+
277
+ if __name__ == "__main__":
278
+ raise SystemExit(main())