pretensor 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.
- pretensor/__init__.py +50 -0
- pretensor/benchmark/__init__.py +54 -0
- pretensor/benchmark/cli.py +294 -0
- pretensor/benchmark/fixtures.py +84 -0
- pretensor/benchmark/l1/__init__.py +23 -0
- pretensor/benchmark/l1/metrics.py +141 -0
- pretensor/benchmark/l1/pipeline.py +188 -0
- pretensor/benchmark/l1/runner.py +245 -0
- pretensor/benchmark/l2/__init__.py +27 -0
- pretensor/benchmark/l2/gold.py +236 -0
- pretensor/benchmark/l2/metrics.py +146 -0
- pretensor/benchmark/l2/pipeline.py +124 -0
- pretensor/benchmark/l2/runner.py +530 -0
- pretensor/benchmark/l3/__init__.py +73 -0
- pretensor/benchmark/l3/agent.py +316 -0
- pretensor/benchmark/l3/db.py +188 -0
- pretensor/benchmark/l3/gold.py +85 -0
- pretensor/benchmark/l3/llm_client.py +395 -0
- pretensor/benchmark/l3/mcp_client.py +357 -0
- pretensor/benchmark/l3/pretensor_runner.py +456 -0
- pretensor/benchmark/l3/prompt.py +132 -0
- pretensor/benchmark/l3/runner.py +358 -0
- pretensor/benchmark/l3/sql_equivalence.py +176 -0
- pretensor/benchmark/release_gate.py +448 -0
- pretensor/benchmark/results.py +298 -0
- pretensor/benchmark/runner.py +109 -0
- pretensor/cli/__init__.py +1 -0
- pretensor/cli/commands/_source_runner.py +147 -0
- pretensor/cli/commands/analyze.py +201 -0
- pretensor/cli/commands/connections/__init__.py +7 -0
- pretensor/cli/commands/connections/add_remove.py +126 -0
- pretensor/cli/commands/connections/register.py +12 -0
- pretensor/cli/commands/export.py +131 -0
- pretensor/cli/commands/index.py +559 -0
- pretensor/cli/commands/list.py +76 -0
- pretensor/cli/commands/quickstart.py +207 -0
- pretensor/cli/commands/reindex.py +646 -0
- pretensor/cli/commands/semantic.py +190 -0
- pretensor/cli/commands/serve.py +144 -0
- pretensor/cli/commands/sync_grants.py +149 -0
- pretensor/cli/commands/validate.py +176 -0
- pretensor/cli/config_file.py +442 -0
- pretensor/cli/constants.py +10 -0
- pretensor/cli/dbt_enrichment.py +96 -0
- pretensor/cli/main.py +109 -0
- pretensor/cli/paths.py +43 -0
- pretensor/cli/plugin.py +52 -0
- pretensor/config.py +226 -0
- pretensor/connectors/__init__.py +29 -0
- pretensor/connectors/base.py +165 -0
- pretensor/connectors/bigquery.py +468 -0
- pretensor/connectors/inspect.py +321 -0
- pretensor/connectors/lineage_sqlglot.py +97 -0
- pretensor/connectors/models.py +130 -0
- pretensor/connectors/mysql.py +402 -0
- pretensor/connectors/pg_array_parse.py +53 -0
- pretensor/connectors/postgres.py +938 -0
- pretensor/connectors/registry.py +93 -0
- pretensor/connectors/snapshot.py +244 -0
- pretensor/connectors/snowflake.py +908 -0
- pretensor/core/__init__.py +1 -0
- pretensor/core/builder.py +307 -0
- pretensor/core/dsn_crypto.py +51 -0
- pretensor/core/graph_schema_manager.py +246 -0
- pretensor/core/graph_store.py +1226 -0
- pretensor/core/ids.py +101 -0
- pretensor/core/portable_export.py +276 -0
- pretensor/core/query_runner.py +67 -0
- pretensor/core/registry.py +209 -0
- pretensor/core/schema.py +473 -0
- pretensor/core/secure_io.py +93 -0
- pretensor/core/store.py +469 -0
- pretensor/enrichment/__init__.py +1 -0
- pretensor/enrichment/analyze/__init__.py +0 -0
- pretensor/enrichment/analyze/classify.py +49 -0
- pretensor/enrichment/analyze/extract_python.py +196 -0
- pretensor/enrichment/analyze/parse.py +141 -0
- pretensor/enrichment/analyze/pipeline.py +195 -0
- pretensor/enrichment/analyze/summary.py +38 -0
- pretensor/enrichment/analyze/walker.py +98 -0
- pretensor/enrichment/analyze/writers.py +214 -0
- pretensor/enrichment/dbt/__init__.py +30 -0
- pretensor/enrichment/dbt/lineage.py +100 -0
- pretensor/enrichment/dbt/manifest.py +300 -0
- pretensor/enrichment/dbt/metadata.py +263 -0
- pretensor/enrichment/dbt/pipeline.py +77 -0
- pretensor/enrichment/dbt/resolution.py +101 -0
- pretensor/enrichment/dbt/signals.py +305 -0
- pretensor/entities/__init__.py +27 -0
- pretensor/entities/builder.py +63 -0
- pretensor/entities/classifier.py +383 -0
- pretensor/entities/llm_extract.py +66 -0
- pretensor/errors.py +35 -0
- pretensor/graph_models/__init__.py +17 -0
- pretensor/graph_models/base.py +11 -0
- pretensor/graph_models/consumer.py +71 -0
- pretensor/graph_models/edge.py +35 -0
- pretensor/graph_models/entity.py +21 -0
- pretensor/graph_models/node.py +79 -0
- pretensor/graph_models/relationship.py +33 -0
- pretensor/integrations/__init__.py +42 -0
- pretensor/integrations/_base.py +138 -0
- pretensor/integrations/google_adk.py +49 -0
- pretensor/integrations/langchain.py +55 -0
- pretensor/integrations/llamaindex.py +53 -0
- pretensor/intelligence/__init__.py +33 -0
- pretensor/intelligence/cluster_labeler.py +425 -0
- pretensor/intelligence/clustering.py +168 -0
- pretensor/intelligence/combining.py +32 -0
- pretensor/intelligence/discovery.py +114 -0
- pretensor/intelligence/embeddings.py +317 -0
- pretensor/intelligence/graph_export.py +200 -0
- pretensor/intelligence/heuristic.py +544 -0
- pretensor/intelligence/join_paths/__init__.py +130 -0
- pretensor/intelligence/join_paths/on_demand.py +516 -0
- pretensor/intelligence/join_paths/storage.py +70 -0
- pretensor/intelligence/llm_infer.py +78 -0
- pretensor/intelligence/llm_runtime.py +62 -0
- pretensor/intelligence/metric_templates.py +193 -0
- pretensor/intelligence/pipeline.py +364 -0
- pretensor/intelligence/role_exemplars.py +263 -0
- pretensor/intelligence/schema_classification.py +360 -0
- pretensor/intelligence/scoring.py +76 -0
- pretensor/intelligence/semantic.py +240 -0
- pretensor/intelligence/shadow_alias.py +101 -0
- pretensor/intelligence/statistical.py +50 -0
- pretensor/intelligence/steps.py +191 -0
- pretensor/intelligence/steps_embedding.py +168 -0
- pretensor/introspection/__init__.py +6 -0
- pretensor/introspection/inspector.py +5 -0
- pretensor/introspection/models/__init__.py +0 -0
- pretensor/introspection/models/base.py +5 -0
- pretensor/introspection/models/config.py +237 -0
- pretensor/introspection/models/dsn.py +550 -0
- pretensor/introspection/models/plan.py +116 -0
- pretensor/introspection/models/schema.py +10 -0
- pretensor/introspection/models/semantic.py +121 -0
- pretensor/introspection/models/validation.py +116 -0
- pretensor/introspection/snapshot.py +46 -0
- pretensor/mcp/__init__.py +16 -0
- pretensor/mcp/config_json.py +24 -0
- pretensor/mcp/payload_types.py +274 -0
- pretensor/mcp/resources/__init__.py +17 -0
- pretensor/mcp/resources/markdown.py +314 -0
- pretensor/mcp/server.py +285 -0
- pretensor/mcp/service.py +49 -0
- pretensor/mcp/service_context.py +142 -0
- pretensor/mcp/service_registry.py +294 -0
- pretensor/mcp/store_cache.py +43 -0
- pretensor/mcp/tool_registry.py +136 -0
- pretensor/mcp/tools/__init__.py +1 -0
- pretensor/mcp/tools/_rank.py +244 -0
- pretensor/mcp/tools/_timed.py +26 -0
- pretensor/mcp/tools/compile_metric.py +144 -0
- pretensor/mcp/tools/consumers.py +161 -0
- pretensor/mcp/tools/context.py +1121 -0
- pretensor/mcp/tools/cypher.py +509 -0
- pretensor/mcp/tools/detect_changes.py +254 -0
- pretensor/mcp/tools/impact.py +271 -0
- pretensor/mcp/tools/list.py +131 -0
- pretensor/mcp/tools/schema.py +170 -0
- pretensor/mcp/tools/search.py +316 -0
- pretensor/mcp/tools/semantic_search.py +282 -0
- pretensor/mcp/tools/traverse.py +1027 -0
- pretensor/mcp/tools/validate_sql.py +150 -0
- pretensor/observability.py +203 -0
- pretensor/py.typed +0 -0
- pretensor/quickstart/README.md +29 -0
- pretensor/quickstart/__init__.py +6 -0
- pretensor/quickstart/docker-compose.yml +18 -0
- pretensor/quickstart/pagila_data.sql +63 -0
- pretensor/quickstart/pagila_ddl.sql +92 -0
- pretensor/search/__init__.py +6 -0
- pretensor/search/base.py +80 -0
- pretensor/search/index.py +435 -0
- pretensor/semantic/__init__.py +24 -0
- pretensor/semantic/base.py +123 -0
- pretensor/semantic/compiler.py +487 -0
- pretensor/semantic/yaml_layer.py +180 -0
- pretensor/skills/__init__.py +5 -0
- pretensor/skills/generator.py +235 -0
- pretensor/staleness/__init__.py +15 -0
- pretensor/staleness/graph_patcher.py +355 -0
- pretensor/staleness/impact_analyzer.py +162 -0
- pretensor/staleness/snapshot_store.py +38 -0
- pretensor/validation/__init__.py +9 -0
- pretensor/validation/query_validator.py +436 -0
- pretensor/visibility/__init__.py +23 -0
- pretensor/visibility/config.py +126 -0
- pretensor/visibility/filter.py +143 -0
- pretensor/visibility/kuzu_helpers.py +32 -0
- pretensor/visibility/runtime.py +36 -0
- pretensor/visibility/sync_grants.py +188 -0
- pretensor-0.1.0.dist-info/METADATA +251 -0
- pretensor-0.1.0.dist-info/RECORD +198 -0
- pretensor-0.1.0.dist-info/WHEEL +4 -0
- pretensor-0.1.0.dist-info/entry_points.txt +2 -0
- pretensor-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Pure L2 metric functions.
|
|
2
|
+
|
|
3
|
+
Each function takes already-collected observations and returns a scalar.
|
|
4
|
+
The runner owns all I/O — these helpers stay testable without spinning
|
|
5
|
+
up Kuzu, the keyword index, or any MCP machinery.
|
|
6
|
+
|
|
7
|
+
Boundary cases (empty input, missing gold per item) are handled here
|
|
8
|
+
so the runner never divides by zero.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Iterable, Sequence
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"JoinPair",
|
|
17
|
+
"RankedHit",
|
|
18
|
+
"compile_metric_correctness",
|
|
19
|
+
"query_recall_at_k",
|
|
20
|
+
"semantic_search_recall_at_k",
|
|
21
|
+
"top_k_with_ties",
|
|
22
|
+
"traverse_correctness",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
JoinPair = tuple[str, str]
|
|
27
|
+
"""``(from_table, to_table)`` — schema-qualified bare names."""
|
|
28
|
+
|
|
29
|
+
RankedHit = tuple[float, str]
|
|
30
|
+
"""``(score, table_name)`` — one search hit; higher score = better."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_SCORE_ROUNDING_DECIMALS = 4
|
|
34
|
+
"""Bucket precision applied to scores before sorting/cutoff.
|
|
35
|
+
|
|
36
|
+
Upstream BM25 (SQLite FTS5) has been observed to emit micro-noise — e.g.
|
|
37
|
+
``-1e-06`` versus ``-0.0`` — for hits that are functionally tied in
|
|
38
|
+
ranking. Rounding to 4 decimals collapses that noise into clean zero
|
|
39
|
+
buckets while preserving meaningful score differences (anything ≥ 1e-4).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def top_k_with_ties(
|
|
44
|
+
ranked: Sequence[RankedHit],
|
|
45
|
+
k: int,
|
|
46
|
+
) -> list[RankedHit]:
|
|
47
|
+
"""Return the top-K hits plus any extra items tied at the K-th score.
|
|
48
|
+
|
|
49
|
+
Sorts defensively by ``(-score, name)`` before slicing so the caller
|
|
50
|
+
needn't pre-sort. AC #5: a tied top-K includes ALL tied items so a
|
|
51
|
+
near-cutoff hit is not missed when comparing against gold.
|
|
52
|
+
|
|
53
|
+
Scores are rounded to :data:`_SCORE_ROUNDING_DECIMALS` decimals before
|
|
54
|
+
comparison so that floating-point noise (e.g. SQLite FTS5's ``-1e-06``
|
|
55
|
+
vs ``-0.0`` for functionally-tied BM25 hits) doesn't make the cutoff
|
|
56
|
+
flip between runs. The returned tuples carry the rounded score; the
|
|
57
|
+
metric only consumes the table-name field, so this loss of precision
|
|
58
|
+
is invisible downstream.
|
|
59
|
+
"""
|
|
60
|
+
if k <= 0 or not ranked:
|
|
61
|
+
return []
|
|
62
|
+
rounded: list[RankedHit] = [
|
|
63
|
+
(round(score, _SCORE_ROUNDING_DECIMALS), name) for score, name in ranked
|
|
64
|
+
]
|
|
65
|
+
ordered = sorted(rounded, key=lambda h: (-h[0], h[1]))
|
|
66
|
+
if len(ordered) <= k:
|
|
67
|
+
return ordered
|
|
68
|
+
cutoff_score = ordered[k - 1][0]
|
|
69
|
+
return [h for h in ordered if h[0] >= cutoff_score]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def query_recall_at_k(
|
|
73
|
+
observations: Iterable[tuple[Iterable[str], Sequence[RankedHit]]],
|
|
74
|
+
k: int = 5,
|
|
75
|
+
) -> float:
|
|
76
|
+
"""Mean Recall@K across observations.
|
|
77
|
+
|
|
78
|
+
Each observation is ``(gold_tables, ranked_results)``. ``ranked_results``
|
|
79
|
+
may arrive in any order — :func:`top_k_with_ties` re-sorts it. Recall
|
|
80
|
+
per observation is ``|gold ∩ retrieved_top_k| / |gold|``. Observations
|
|
81
|
+
with empty ``gold_tables`` are skipped (recall undefined). Returns
|
|
82
|
+
``0.0`` when no observation has gold data.
|
|
83
|
+
"""
|
|
84
|
+
total = 0.0
|
|
85
|
+
n = 0
|
|
86
|
+
for gold, ranked in observations:
|
|
87
|
+
gold_set = set(gold)
|
|
88
|
+
if not gold_set:
|
|
89
|
+
continue
|
|
90
|
+
top_k = top_k_with_ties(list(ranked), k)
|
|
91
|
+
retrieved_set = {name for _, name in top_k}
|
|
92
|
+
recall = len(gold_set & retrieved_set) / len(gold_set)
|
|
93
|
+
total += recall
|
|
94
|
+
n += 1
|
|
95
|
+
return total / n if n > 0 else 0.0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def semantic_search_recall_at_k(
|
|
99
|
+
observations: Iterable[tuple[Iterable[str], Sequence[RankedHit]]],
|
|
100
|
+
k: int = 5,
|
|
101
|
+
) -> float:
|
|
102
|
+
"""Mean Recall@K against ``semantic_search`` retrievals.
|
|
103
|
+
|
|
104
|
+
Same shape and semantics as :func:`query_recall_at_k` — kept distinct
|
|
105
|
+
so the two retrieval systems can diverge later (different ranking
|
|
106
|
+
semantics, different post-processing) without forcing one to fit the
|
|
107
|
+
other's API.
|
|
108
|
+
"""
|
|
109
|
+
return query_recall_at_k(observations, k=k)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def traverse_correctness(
|
|
113
|
+
observations: Iterable[tuple[Sequence[JoinPair], Sequence[Sequence[JoinPair]]]],
|
|
114
|
+
) -> float:
|
|
115
|
+
"""Fraction of items whose gold path matches at least one returned path.
|
|
116
|
+
|
|
117
|
+
The ``traverse`` tool emits all top-ranked paths when ties exist;
|
|
118
|
+
this metric counts the item as correct when ANY returned path
|
|
119
|
+
equals the gold sequence (tuple-wise comparison). Returns ``0.0``
|
|
120
|
+
when ``observations`` is empty.
|
|
121
|
+
"""
|
|
122
|
+
obs_list = list(observations)
|
|
123
|
+
if not obs_list:
|
|
124
|
+
return 0.0
|
|
125
|
+
|
|
126
|
+
correct = 0
|
|
127
|
+
for gold_path, returned_paths in obs_list:
|
|
128
|
+
gold_t = tuple(tuple(p) for p in gold_path)
|
|
129
|
+
for path in returned_paths:
|
|
130
|
+
if tuple(tuple(p) for p in path) == gold_t:
|
|
131
|
+
correct += 1
|
|
132
|
+
break
|
|
133
|
+
return correct / len(obs_list)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def compile_metric_correctness(observations: Iterable[bool]) -> float:
|
|
137
|
+
"""Fraction of metric templates that compiled to valid SQL.
|
|
138
|
+
|
|
139
|
+
Each observation is the boolean ``valid`` flag returned by
|
|
140
|
+
``compile_metric_payload``. Returns ``0.0`` when ``observations`` is
|
|
141
|
+
empty.
|
|
142
|
+
"""
|
|
143
|
+
obs_list = list(observations)
|
|
144
|
+
if not obs_list:
|
|
145
|
+
return 0.0
|
|
146
|
+
return sum(1 for ok in obs_list if ok) / len(obs_list)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Per-call graph construction for the L2 benchmark.
|
|
2
|
+
|
|
3
|
+
Each invocation of :func:`run_l2` indexes the fixture's schema YAML into
|
|
4
|
+
a fresh, throwaway Kuzu store + ``registry.json`` under a temp dir,
|
|
5
|
+
then hands that directory to the MCP tool payload functions
|
|
6
|
+
(``query_payload``, ``traverse_payload``, ``compile_metric_payload``).
|
|
7
|
+
This guarantees determinism — no leftover state from previous runs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from pretensor.config import EmbeddingsConfig, PretensorConfig
|
|
16
|
+
from pretensor.connectors.models import SchemaSnapshot
|
|
17
|
+
from pretensor.core.builder import GraphBuilder
|
|
18
|
+
from pretensor.core.registry import GraphRegistry
|
|
19
|
+
from pretensor.core.store import KuzuStore
|
|
20
|
+
|
|
21
|
+
__all__ = ["build_l2_graph_dir"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Pinned so the persisted ``last_indexed_at`` doesn't churn the registry
|
|
25
|
+
# bytes between runs. The benchmark only consults graph contents — the
|
|
26
|
+
# timestamp is metadata that has no semantic effect on tool output, but
|
|
27
|
+
# we still want byte-identical state across invocations for any future
|
|
28
|
+
# code that hashes the registry.
|
|
29
|
+
_DETERMINISTIC_INDEXED_AT = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_l2_graph_dir(
|
|
33
|
+
snapshot: SchemaSnapshot, *, work_dir: Path, embeddings: bool = False
|
|
34
|
+
) -> Path:
|
|
35
|
+
"""Build a Kuzu graph + registry under ``work_dir`` and return the dir.
|
|
36
|
+
|
|
37
|
+
When ``embeddings`` is True, tables are indexed with
|
|
38
|
+
``EmbeddingsConfig(index_tables=True)`` so the ``semantic_search`` /
|
|
39
|
+
hybrid-rerank legs measure real cosine retrieval instead of the
|
|
40
|
+
no-vectors fallback (which would peg semantic recall at 0.0 and make
|
|
41
|
+
the embeddings CI lane indistinguishable from the plain one). The
|
|
42
|
+
embedding model revision is pinned, so vectors are deterministic
|
|
43
|
+
across runs.
|
|
44
|
+
|
|
45
|
+
The returned path is suitable as the ``graph_dir`` argument to every
|
|
46
|
+
MCP tool payload function. ``work_dir`` is created if it doesn't
|
|
47
|
+
exist; the caller (typically ``tempfile.TemporaryDirectory``) is
|
|
48
|
+
responsible for cleanup.
|
|
49
|
+
|
|
50
|
+
Notes:
|
|
51
|
+
``run_relationship_discovery=False`` matches the determinism
|
|
52
|
+
policy — L2 metrics measure the tools' behaviour over the
|
|
53
|
+
declared FK / structural shape, not over heuristic discovery
|
|
54
|
+
output. (L1 covers the discovery quality leg via its own
|
|
55
|
+
:func:`pretensor.benchmark.l1.pipeline.discover_inferred_joins_blind`.)
|
|
56
|
+
|
|
57
|
+
After build, this helper strips :class:`Cluster` nodes (and the
|
|
58
|
+
``IN_CLUSTER`` edges that link them to tables). Reason: the
|
|
59
|
+
intelligence layer that ``GraphBuilder.build`` runs unconditionally
|
|
60
|
+
produces cluster labels via Louvain/Leiden community detection,
|
|
61
|
+
and even with a seeded RNG the labels can differ across
|
|
62
|
+
environments (different igraph builds, different platform RNG
|
|
63
|
+
wiring). Those labels feed into the FTS5 keyword index's
|
|
64
|
+
``cluster_context`` column and shift BM25 scores across machines.
|
|
65
|
+
L2 measures ``query`` / ``semantic_search`` / ``traverse`` /
|
|
66
|
+
``compile_metric`` quality — none of those tools depend on
|
|
67
|
+
clustering being present, so removing it from the graph keeps
|
|
68
|
+
the benchmark cross-environment-deterministic without changing
|
|
69
|
+
what's being measured.
|
|
70
|
+
"""
|
|
71
|
+
work_dir.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
graph_path = work_dir / "graphs" / f"{snapshot.connection_name}.kuzu"
|
|
73
|
+
graph_path.parent.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
|
|
75
|
+
store = KuzuStore(graph_path)
|
|
76
|
+
try:
|
|
77
|
+
cfg = PretensorConfig(
|
|
78
|
+
embeddings=EmbeddingsConfig(index_tables=embeddings),
|
|
79
|
+
)
|
|
80
|
+
GraphBuilder().build(
|
|
81
|
+
snapshot, store, run_relationship_discovery=False, config=cfg
|
|
82
|
+
)
|
|
83
|
+
if embeddings and not store.has_any_table_embeddings():
|
|
84
|
+
# The production embed path tolerates per-run failures (a
|
|
85
|
+
# degraded index beats an aborted one), but a benchmark lane
|
|
86
|
+
# that silently measures the no-vectors fallback would report
|
|
87
|
+
# a bogus regression against the embeddings baseline. Fail
|
|
88
|
+
# loudly instead — the usual culprit is a failed model
|
|
89
|
+
# download (e.g. HF Hub rate-limiting in CI).
|
|
90
|
+
msg = (
|
|
91
|
+
"L2 was invoked with --embeddings but no table vectors were "
|
|
92
|
+
"computed; the embedding model is likely unavailable "
|
|
93
|
+
"(download failure / rate limit). Fix the model fetch or "
|
|
94
|
+
"re-run without --embeddings."
|
|
95
|
+
)
|
|
96
|
+
raise RuntimeError(msg)
|
|
97
|
+
# See docstring: clusters introduce cross-environment nondeterminism
|
|
98
|
+
# via FTS5's cluster_context column. L2 doesn't measure clustering
|
|
99
|
+
# quality (L1 does), so strip them before the runner queries the graph.
|
|
100
|
+
_strip_clusters(store)
|
|
101
|
+
finally:
|
|
102
|
+
store.close()
|
|
103
|
+
|
|
104
|
+
reg = GraphRegistry(work_dir / "registry.json").load()
|
|
105
|
+
reg.upsert(
|
|
106
|
+
connection_name=snapshot.connection_name,
|
|
107
|
+
database=snapshot.database,
|
|
108
|
+
dsn="",
|
|
109
|
+
graph_path=graph_path,
|
|
110
|
+
indexed_at=_DETERMINISTIC_INDEXED_AT,
|
|
111
|
+
)
|
|
112
|
+
reg.save()
|
|
113
|
+
|
|
114
|
+
return work_dir
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _strip_clusters(store: KuzuStore) -> None:
|
|
118
|
+
"""Remove all ``Cluster`` nodes (and ``IN_CLUSTER`` edges) from the graph.
|
|
119
|
+
|
|
120
|
+
Kuzu doesn't expose ``DETACH DELETE`` semantics on every type, so
|
|
121
|
+
we wipe the relationship rows first and then the cluster nodes.
|
|
122
|
+
"""
|
|
123
|
+
store.execute_write("MATCH ()-[r:IN_CLUSTER]->() DELETE r")
|
|
124
|
+
store.execute_write("MATCH (c:Cluster) DELETE c")
|