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.
Files changed (198) hide show
  1. pretensor/__init__.py +50 -0
  2. pretensor/benchmark/__init__.py +54 -0
  3. pretensor/benchmark/cli.py +294 -0
  4. pretensor/benchmark/fixtures.py +84 -0
  5. pretensor/benchmark/l1/__init__.py +23 -0
  6. pretensor/benchmark/l1/metrics.py +141 -0
  7. pretensor/benchmark/l1/pipeline.py +188 -0
  8. pretensor/benchmark/l1/runner.py +245 -0
  9. pretensor/benchmark/l2/__init__.py +27 -0
  10. pretensor/benchmark/l2/gold.py +236 -0
  11. pretensor/benchmark/l2/metrics.py +146 -0
  12. pretensor/benchmark/l2/pipeline.py +124 -0
  13. pretensor/benchmark/l2/runner.py +530 -0
  14. pretensor/benchmark/l3/__init__.py +73 -0
  15. pretensor/benchmark/l3/agent.py +316 -0
  16. pretensor/benchmark/l3/db.py +188 -0
  17. pretensor/benchmark/l3/gold.py +85 -0
  18. pretensor/benchmark/l3/llm_client.py +395 -0
  19. pretensor/benchmark/l3/mcp_client.py +357 -0
  20. pretensor/benchmark/l3/pretensor_runner.py +456 -0
  21. pretensor/benchmark/l3/prompt.py +132 -0
  22. pretensor/benchmark/l3/runner.py +358 -0
  23. pretensor/benchmark/l3/sql_equivalence.py +176 -0
  24. pretensor/benchmark/release_gate.py +448 -0
  25. pretensor/benchmark/results.py +298 -0
  26. pretensor/benchmark/runner.py +109 -0
  27. pretensor/cli/__init__.py +1 -0
  28. pretensor/cli/commands/_source_runner.py +147 -0
  29. pretensor/cli/commands/analyze.py +201 -0
  30. pretensor/cli/commands/connections/__init__.py +7 -0
  31. pretensor/cli/commands/connections/add_remove.py +126 -0
  32. pretensor/cli/commands/connections/register.py +12 -0
  33. pretensor/cli/commands/export.py +131 -0
  34. pretensor/cli/commands/index.py +559 -0
  35. pretensor/cli/commands/list.py +76 -0
  36. pretensor/cli/commands/quickstart.py +207 -0
  37. pretensor/cli/commands/reindex.py +646 -0
  38. pretensor/cli/commands/semantic.py +190 -0
  39. pretensor/cli/commands/serve.py +144 -0
  40. pretensor/cli/commands/sync_grants.py +149 -0
  41. pretensor/cli/commands/validate.py +176 -0
  42. pretensor/cli/config_file.py +442 -0
  43. pretensor/cli/constants.py +10 -0
  44. pretensor/cli/dbt_enrichment.py +96 -0
  45. pretensor/cli/main.py +109 -0
  46. pretensor/cli/paths.py +43 -0
  47. pretensor/cli/plugin.py +52 -0
  48. pretensor/config.py +226 -0
  49. pretensor/connectors/__init__.py +29 -0
  50. pretensor/connectors/base.py +165 -0
  51. pretensor/connectors/bigquery.py +468 -0
  52. pretensor/connectors/inspect.py +321 -0
  53. pretensor/connectors/lineage_sqlglot.py +97 -0
  54. pretensor/connectors/models.py +130 -0
  55. pretensor/connectors/mysql.py +402 -0
  56. pretensor/connectors/pg_array_parse.py +53 -0
  57. pretensor/connectors/postgres.py +938 -0
  58. pretensor/connectors/registry.py +93 -0
  59. pretensor/connectors/snapshot.py +244 -0
  60. pretensor/connectors/snowflake.py +908 -0
  61. pretensor/core/__init__.py +1 -0
  62. pretensor/core/builder.py +307 -0
  63. pretensor/core/dsn_crypto.py +51 -0
  64. pretensor/core/graph_schema_manager.py +246 -0
  65. pretensor/core/graph_store.py +1226 -0
  66. pretensor/core/ids.py +101 -0
  67. pretensor/core/portable_export.py +276 -0
  68. pretensor/core/query_runner.py +67 -0
  69. pretensor/core/registry.py +209 -0
  70. pretensor/core/schema.py +473 -0
  71. pretensor/core/secure_io.py +93 -0
  72. pretensor/core/store.py +469 -0
  73. pretensor/enrichment/__init__.py +1 -0
  74. pretensor/enrichment/analyze/__init__.py +0 -0
  75. pretensor/enrichment/analyze/classify.py +49 -0
  76. pretensor/enrichment/analyze/extract_python.py +196 -0
  77. pretensor/enrichment/analyze/parse.py +141 -0
  78. pretensor/enrichment/analyze/pipeline.py +195 -0
  79. pretensor/enrichment/analyze/summary.py +38 -0
  80. pretensor/enrichment/analyze/walker.py +98 -0
  81. pretensor/enrichment/analyze/writers.py +214 -0
  82. pretensor/enrichment/dbt/__init__.py +30 -0
  83. pretensor/enrichment/dbt/lineage.py +100 -0
  84. pretensor/enrichment/dbt/manifest.py +300 -0
  85. pretensor/enrichment/dbt/metadata.py +263 -0
  86. pretensor/enrichment/dbt/pipeline.py +77 -0
  87. pretensor/enrichment/dbt/resolution.py +101 -0
  88. pretensor/enrichment/dbt/signals.py +305 -0
  89. pretensor/entities/__init__.py +27 -0
  90. pretensor/entities/builder.py +63 -0
  91. pretensor/entities/classifier.py +383 -0
  92. pretensor/entities/llm_extract.py +66 -0
  93. pretensor/errors.py +35 -0
  94. pretensor/graph_models/__init__.py +17 -0
  95. pretensor/graph_models/base.py +11 -0
  96. pretensor/graph_models/consumer.py +71 -0
  97. pretensor/graph_models/edge.py +35 -0
  98. pretensor/graph_models/entity.py +21 -0
  99. pretensor/graph_models/node.py +79 -0
  100. pretensor/graph_models/relationship.py +33 -0
  101. pretensor/integrations/__init__.py +42 -0
  102. pretensor/integrations/_base.py +138 -0
  103. pretensor/integrations/google_adk.py +49 -0
  104. pretensor/integrations/langchain.py +55 -0
  105. pretensor/integrations/llamaindex.py +53 -0
  106. pretensor/intelligence/__init__.py +33 -0
  107. pretensor/intelligence/cluster_labeler.py +425 -0
  108. pretensor/intelligence/clustering.py +168 -0
  109. pretensor/intelligence/combining.py +32 -0
  110. pretensor/intelligence/discovery.py +114 -0
  111. pretensor/intelligence/embeddings.py +317 -0
  112. pretensor/intelligence/graph_export.py +200 -0
  113. pretensor/intelligence/heuristic.py +544 -0
  114. pretensor/intelligence/join_paths/__init__.py +130 -0
  115. pretensor/intelligence/join_paths/on_demand.py +516 -0
  116. pretensor/intelligence/join_paths/storage.py +70 -0
  117. pretensor/intelligence/llm_infer.py +78 -0
  118. pretensor/intelligence/llm_runtime.py +62 -0
  119. pretensor/intelligence/metric_templates.py +193 -0
  120. pretensor/intelligence/pipeline.py +364 -0
  121. pretensor/intelligence/role_exemplars.py +263 -0
  122. pretensor/intelligence/schema_classification.py +360 -0
  123. pretensor/intelligence/scoring.py +76 -0
  124. pretensor/intelligence/semantic.py +240 -0
  125. pretensor/intelligence/shadow_alias.py +101 -0
  126. pretensor/intelligence/statistical.py +50 -0
  127. pretensor/intelligence/steps.py +191 -0
  128. pretensor/intelligence/steps_embedding.py +168 -0
  129. pretensor/introspection/__init__.py +6 -0
  130. pretensor/introspection/inspector.py +5 -0
  131. pretensor/introspection/models/__init__.py +0 -0
  132. pretensor/introspection/models/base.py +5 -0
  133. pretensor/introspection/models/config.py +237 -0
  134. pretensor/introspection/models/dsn.py +550 -0
  135. pretensor/introspection/models/plan.py +116 -0
  136. pretensor/introspection/models/schema.py +10 -0
  137. pretensor/introspection/models/semantic.py +121 -0
  138. pretensor/introspection/models/validation.py +116 -0
  139. pretensor/introspection/snapshot.py +46 -0
  140. pretensor/mcp/__init__.py +16 -0
  141. pretensor/mcp/config_json.py +24 -0
  142. pretensor/mcp/payload_types.py +274 -0
  143. pretensor/mcp/resources/__init__.py +17 -0
  144. pretensor/mcp/resources/markdown.py +314 -0
  145. pretensor/mcp/server.py +285 -0
  146. pretensor/mcp/service.py +49 -0
  147. pretensor/mcp/service_context.py +142 -0
  148. pretensor/mcp/service_registry.py +294 -0
  149. pretensor/mcp/store_cache.py +43 -0
  150. pretensor/mcp/tool_registry.py +136 -0
  151. pretensor/mcp/tools/__init__.py +1 -0
  152. pretensor/mcp/tools/_rank.py +244 -0
  153. pretensor/mcp/tools/_timed.py +26 -0
  154. pretensor/mcp/tools/compile_metric.py +144 -0
  155. pretensor/mcp/tools/consumers.py +161 -0
  156. pretensor/mcp/tools/context.py +1121 -0
  157. pretensor/mcp/tools/cypher.py +509 -0
  158. pretensor/mcp/tools/detect_changes.py +254 -0
  159. pretensor/mcp/tools/impact.py +271 -0
  160. pretensor/mcp/tools/list.py +131 -0
  161. pretensor/mcp/tools/schema.py +170 -0
  162. pretensor/mcp/tools/search.py +316 -0
  163. pretensor/mcp/tools/semantic_search.py +282 -0
  164. pretensor/mcp/tools/traverse.py +1027 -0
  165. pretensor/mcp/tools/validate_sql.py +150 -0
  166. pretensor/observability.py +203 -0
  167. pretensor/py.typed +0 -0
  168. pretensor/quickstart/README.md +29 -0
  169. pretensor/quickstart/__init__.py +6 -0
  170. pretensor/quickstart/docker-compose.yml +18 -0
  171. pretensor/quickstart/pagila_data.sql +63 -0
  172. pretensor/quickstart/pagila_ddl.sql +92 -0
  173. pretensor/search/__init__.py +6 -0
  174. pretensor/search/base.py +80 -0
  175. pretensor/search/index.py +435 -0
  176. pretensor/semantic/__init__.py +24 -0
  177. pretensor/semantic/base.py +123 -0
  178. pretensor/semantic/compiler.py +487 -0
  179. pretensor/semantic/yaml_layer.py +180 -0
  180. pretensor/skills/__init__.py +5 -0
  181. pretensor/skills/generator.py +235 -0
  182. pretensor/staleness/__init__.py +15 -0
  183. pretensor/staleness/graph_patcher.py +355 -0
  184. pretensor/staleness/impact_analyzer.py +162 -0
  185. pretensor/staleness/snapshot_store.py +38 -0
  186. pretensor/validation/__init__.py +9 -0
  187. pretensor/validation/query_validator.py +436 -0
  188. pretensor/visibility/__init__.py +23 -0
  189. pretensor/visibility/config.py +126 -0
  190. pretensor/visibility/filter.py +143 -0
  191. pretensor/visibility/kuzu_helpers.py +32 -0
  192. pretensor/visibility/runtime.py +36 -0
  193. pretensor/visibility/sync_grants.py +188 -0
  194. pretensor-0.1.0.dist-info/METADATA +251 -0
  195. pretensor-0.1.0.dist-info/RECORD +198 -0
  196. pretensor-0.1.0.dist-info/WHEEL +4 -0
  197. pretensor-0.1.0.dist-info/entry_points.txt +2 -0
  198. pretensor-0.1.0.dist-info/licenses/LICENSE +21 -0
pretensor/__init__.py ADDED
@@ -0,0 +1,50 @@
1
+ """Pretensor Graph — schema knowledge graph backed by Kuzu.
2
+
3
+ Heavy imports (e.g. :class:`GraphBuilder`) are lazy so ``import pretensor``
4
+ works in tests that only touch the registry.
5
+ """
6
+
7
+ # pyright: reportUnsupportedDunderAll=false
8
+ # Names in __all__ are provided via __getattr__; Pyright does not model that.
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ __all__ = [
15
+ "GraphBuilder",
16
+ "GraphEdge",
17
+ "GraphNode",
18
+ "GraphRegistry",
19
+ "KuzuStore",
20
+ "RelationshipCandidate",
21
+ ]
22
+
23
+
24
+ def __getattr__(name: str) -> Any:
25
+ if name == "GraphBuilder":
26
+ from pretensor.core.builder import GraphBuilder
27
+
28
+ return GraphBuilder
29
+ if name == "GraphRegistry":
30
+ from pretensor.core.registry import GraphRegistry
31
+
32
+ return GraphRegistry
33
+ if name == "KuzuStore":
34
+ from pretensor.core.store import KuzuStore
35
+
36
+ return KuzuStore
37
+ if name == "GraphNode":
38
+ from pretensor.graph_models.node import GraphNode
39
+
40
+ return GraphNode
41
+ if name == "GraphEdge":
42
+ from pretensor.graph_models.edge import GraphEdge
43
+
44
+ return GraphEdge
45
+ if name == "RelationshipCandidate":
46
+ from pretensor.graph_models.relationship import RelationshipCandidate
47
+
48
+ return RelationshipCandidate
49
+ msg = f"module {__name__!r} has no attribute {name!r}"
50
+ raise AttributeError(msg)
@@ -0,0 +1,54 @@
1
+ """``pretensor benchmark`` subsystem — 3-level benchmark harness.
2
+
3
+ The CLI contract and metric definitions are specified in
4
+ ``pretensor-ai/pretensor-specs`` at ``docs/specs/benchmark/spec.md``. This
5
+ package contains the CLI dispatch / I/O wiring, the results aggregator
6
+ (``BenchmarkResult``, ``compare``, JSON / CSV I/O), and the ``compare``
7
+ regression-gate. L1 / L2 metric computation and the L3 agent-task runners
8
+ land in follow-up changes.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from pretensor.benchmark.cli import register_benchmark_command
14
+ from pretensor.benchmark.fixtures import Fixture, load_dataset
15
+ from pretensor.benchmark.results import (
16
+ BenchmarkResult,
17
+ ComparisonError,
18
+ ComparisonReport,
19
+ Direction,
20
+ Metric,
21
+ MetricDiff,
22
+ compare,
23
+ read_json,
24
+ write_csv,
25
+ write_json,
26
+ )
27
+ from pretensor.benchmark.runner import (
28
+ Dataset,
29
+ RunnerKind,
30
+ run_l1,
31
+ run_l2,
32
+ run_l3,
33
+ )
34
+
35
+ __all__ = [
36
+ "BenchmarkResult",
37
+ "ComparisonError",
38
+ "ComparisonReport",
39
+ "Dataset",
40
+ "Direction",
41
+ "Fixture",
42
+ "Metric",
43
+ "MetricDiff",
44
+ "RunnerKind",
45
+ "compare",
46
+ "load_dataset",
47
+ "read_json",
48
+ "register_benchmark_command",
49
+ "run_l1",
50
+ "run_l2",
51
+ "run_l3",
52
+ "write_csv",
53
+ "write_json",
54
+ ]
@@ -0,0 +1,294 @@
1
+ """``pretensor benchmark`` command group — L1 / L2 / L3 dispatch.
2
+
3
+ Contract: see ``docs/specs/benchmark/spec.md`` §CLI contract. stdout carries
4
+ only the JSON document when ``--out`` is absent; all human-facing text goes
5
+ to stderr.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+ from typing import TYPE_CHECKING
13
+
14
+ import typer
15
+ from rich.console import Console
16
+
17
+ if TYPE_CHECKING:
18
+ # Type-only import — runtime cost is paid lazily inside l3_command.
19
+ from pretensor.benchmark.l3.mcp_client import McpClientError
20
+
21
+ from pretensor.benchmark.results import (
22
+ ComparisonError,
23
+ compare,
24
+ read_json,
25
+ )
26
+ from pretensor.benchmark.runner import (
27
+ Dataset,
28
+ RunnerKind,
29
+ run_l1,
30
+ run_l2,
31
+ run_l3,
32
+ )
33
+
34
+ __all__ = ["register_benchmark_command"]
35
+
36
+
37
+ # Exit 1 covers both stub run failures and detected regressions — the spec
38
+ # §CLI contract collapses them under a single "release-gating assertion
39
+ # failed" code; downstream scripts inspect stderr to disambiguate.
40
+ _EXIT_FAILED = 1
41
+ _EXIT_BAD_INPUT = 2
42
+ _DEFAULT_GRAPH_DIR = Path(".pretensor")
43
+ _DATASET_HELP = (
44
+ "Fixture key (pagila, tpch, analytics_dwh, adversarial, "
45
+ "saas_multitenant, adventureworks)."
46
+ )
47
+ _OUT_HELP = "Write JSON output to this path; stdout when omitted."
48
+ _GRAPH_DIR_HELP = "Existing indexed graph directory. Defaults to .pretensor."
49
+ _BASELINE_HELP = "Path to the baseline benchmark result JSON."
50
+ _CURRENT_HELP = (
51
+ "Path to the current benchmark result JSON to compare against the baseline."
52
+ )
53
+
54
+
55
+ def register_benchmark_command(app: typer.Typer) -> None:
56
+ """Register ``pretensor benchmark`` onto ``app``.
57
+
58
+ The benchmark commands route all human output to a dedicated stderr
59
+ console (the spec reserves stdout for the JSON document). Unlike most
60
+ ``register_*`` callers, this function therefore does not accept the
61
+ shared stdout console from ``cli.main``.
62
+ """
63
+ err_console = Console(stderr=True)
64
+
65
+ benchmark_app = typer.Typer(
66
+ name="benchmark",
67
+ help=(
68
+ "Run the 3-level benchmark harness (L1 graph quality, "
69
+ "L2 MCP tool quality, L3 agent task success)."
70
+ ),
71
+ no_args_is_help=True,
72
+ )
73
+
74
+ def _handle_not_implemented(exc: NotImplementedError) -> None:
75
+ err_console.print(f"[yellow]{exc}[/yellow]")
76
+ raise typer.Exit(_EXIT_FAILED) from exc
77
+
78
+ def _handle_input_error(
79
+ exc: LookupError | FileNotFoundError | McpClientError,
80
+ ) -> None:
81
+ """Friendly exit for the L3 runner's user-recoverable errors.
82
+
83
+ Three families land here:
84
+
85
+ * ``LookupError`` — missing per-dataset DB URL env var or the
86
+ LLM-provider key (the L3 runner converts the constructor's
87
+ ``LlmCallError`` to ``LookupError`` so this handler picks it
88
+ up too).
89
+ * ``FileNotFoundError`` — missing DDL bundle, missing gold
90
+ file, or the pretensor runner's missing-indexed-graph guard.
91
+ * ``McpClientError`` — the pretensor runner failed to spawn or
92
+ handshake with ``pretensor serve`` (broken binary, version
93
+ mismatch, transport fault). Distinct from missing-graph-dir
94
+ so an operator can tell apart "I forgot to index" from "the
95
+ serve binary is broken" in the output.
96
+
97
+ All three are user-recoverable misconfigurations — print the
98
+ message as-is (the exception text is already actionable) and
99
+ exit with the same release-gate code as a stub failure so
100
+ wrappers don't need to special-case L3 vs L1/L2.
101
+ """
102
+ err_console.print(f"[red]{exc}[/red]")
103
+ raise typer.Exit(_EXIT_FAILED) from exc
104
+
105
+ @benchmark_app.command("l1")
106
+ def l1_command(
107
+ dataset: Dataset = typer.Option(
108
+ ...,
109
+ "--dataset",
110
+ help=_DATASET_HELP,
111
+ case_sensitive=False,
112
+ ),
113
+ out: Path | None = typer.Option(
114
+ None,
115
+ "--out",
116
+ help=_OUT_HELP,
117
+ file_okay=True,
118
+ dir_okay=False,
119
+ writable=True,
120
+ resolve_path=True,
121
+ ),
122
+ graph_dir: Path = typer.Option(
123
+ _DEFAULT_GRAPH_DIR,
124
+ "--graph-dir",
125
+ help=_GRAPH_DIR_HELP,
126
+ file_okay=False,
127
+ dir_okay=True,
128
+ resolve_path=True,
129
+ ),
130
+ embeddings: bool = typer.Option(
131
+ False,
132
+ "--embeddings/--no-embeddings",
133
+ help="Run the Layer-A embeddings path (requires the "
134
+ "'embeddings' optional extra).",
135
+ ),
136
+ ) -> None:
137
+ """Run L1 graph-quality metrics against a fixture."""
138
+ try:
139
+ run_l1(dataset, out, graph_dir, embeddings=embeddings)
140
+ except NotImplementedError as exc:
141
+ _handle_not_implemented(exc)
142
+
143
+ @benchmark_app.command("l2")
144
+ def l2_command(
145
+ dataset: Dataset = typer.Option(
146
+ ...,
147
+ "--dataset",
148
+ help=_DATASET_HELP,
149
+ case_sensitive=False,
150
+ ),
151
+ out: Path | None = typer.Option(
152
+ None,
153
+ "--out",
154
+ help=_OUT_HELP,
155
+ file_okay=True,
156
+ dir_okay=False,
157
+ writable=True,
158
+ resolve_path=True,
159
+ ),
160
+ graph_dir: Path = typer.Option(
161
+ _DEFAULT_GRAPH_DIR,
162
+ "--graph-dir",
163
+ help=_GRAPH_DIR_HELP,
164
+ file_okay=False,
165
+ dir_okay=True,
166
+ resolve_path=True,
167
+ ),
168
+ embeddings: bool = typer.Option(
169
+ False,
170
+ "--embeddings/--no-embeddings",
171
+ help="Include the semantic_search Recall@K metric "
172
+ "(requires the 'embeddings' optional extra).",
173
+ ),
174
+ ) -> None:
175
+ """Run L2 MCP-tool-quality metrics against a fixture."""
176
+ try:
177
+ run_l2(dataset, out, graph_dir, embeddings=embeddings)
178
+ except NotImplementedError as exc:
179
+ _handle_not_implemented(exc)
180
+
181
+ @benchmark_app.command("l3")
182
+ def l3_command(
183
+ dataset: Dataset = typer.Option(
184
+ ...,
185
+ "--dataset",
186
+ help=_DATASET_HELP,
187
+ case_sensitive=False,
188
+ ),
189
+ out: Path | None = typer.Option(
190
+ None,
191
+ "--out",
192
+ help=_OUT_HELP,
193
+ file_okay=True,
194
+ dir_okay=False,
195
+ writable=True,
196
+ resolve_path=True,
197
+ ),
198
+ graph_dir: Path = typer.Option(
199
+ _DEFAULT_GRAPH_DIR,
200
+ "--graph-dir",
201
+ help=_GRAPH_DIR_HELP,
202
+ file_okay=False,
203
+ dir_okay=True,
204
+ resolve_path=True,
205
+ ),
206
+ runner: RunnerKind = typer.Option(
207
+ ...,
208
+ "--runner",
209
+ help="Which L3 runner to execute: baseline (agent + raw "
210
+ "schema) or pretensor (agent + MCP).",
211
+ case_sensitive=False,
212
+ ),
213
+ model: str = typer.Option(
214
+ ...,
215
+ "--model",
216
+ help="LLM model identifier (e.g. claude-haiku-4-5).",
217
+ ),
218
+ seed: int | None = typer.Option(
219
+ None,
220
+ "--seed",
221
+ help="Optional integer seed for reproducible agent runs.",
222
+ ),
223
+ ) -> None:
224
+ """Run L3 agent-task-success evaluation against a fixture."""
225
+ # Lazy import: ``McpClientError`` lives in the L3 MCP-client
226
+ # module, which transitively pulls in the MCP SDK. Importing it
227
+ # at CLI module load would defeat the lazy-import strategy used
228
+ # by the run_l3 dispatcher (and slow `pretensor benchmark
229
+ # --help` for everyone).
230
+ from pretensor.benchmark.l3.mcp_client import McpClientError
231
+
232
+ try:
233
+ run_l3(
234
+ dataset,
235
+ out,
236
+ graph_dir,
237
+ runner=runner,
238
+ model=model,
239
+ seed=seed,
240
+ )
241
+ except NotImplementedError as exc:
242
+ _handle_not_implemented(exc)
243
+ except (LookupError, FileNotFoundError, McpClientError) as exc:
244
+ _handle_input_error(exc)
245
+
246
+ @benchmark_app.command("compare")
247
+ def compare_command(
248
+ baseline: Path = typer.Option(
249
+ ...,
250
+ "--baseline",
251
+ help=_BASELINE_HELP,
252
+ exists=True,
253
+ file_okay=True,
254
+ dir_okay=False,
255
+ readable=True,
256
+ resolve_path=True,
257
+ ),
258
+ current: Path = typer.Option(
259
+ ...,
260
+ "--current",
261
+ help=_CURRENT_HELP,
262
+ exists=True,
263
+ file_okay=True,
264
+ dir_okay=False,
265
+ readable=True,
266
+ resolve_path=True,
267
+ ),
268
+ tolerance: float = typer.Option(
269
+ 1e-6,
270
+ "--tolerance",
271
+ help="Numeric tolerance below which metric drift is treated as noise.",
272
+ ),
273
+ ) -> None:
274
+ """Compare two benchmark result JSONs and exit non-zero on regression."""
275
+ try:
276
+ baseline_result = read_json(baseline)
277
+ except (json.JSONDecodeError, KeyError) as exc:
278
+ err_console.print(f"[red]Failed to read baseline: {exc}[/red]")
279
+ raise typer.Exit(_EXIT_BAD_INPUT) from exc
280
+ try:
281
+ current_result = read_json(current)
282
+ except (json.JSONDecodeError, KeyError) as exc:
283
+ err_console.print(f"[red]Failed to read current: {exc}[/red]")
284
+ raise typer.Exit(_EXIT_BAD_INPUT) from exc
285
+ try:
286
+ report = compare(baseline_result, current_result, tolerance=tolerance)
287
+ except ComparisonError as exc:
288
+ err_console.print(f"[red]{exc}[/red]")
289
+ raise typer.Exit(_EXIT_BAD_INPUT) from exc
290
+ err_console.print(report.format_diff())
291
+ if report.has_regression:
292
+ raise typer.Exit(_EXIT_FAILED)
293
+
294
+ app.add_typer(benchmark_app)
@@ -0,0 +1,84 @@
1
+ """Benchmark-fixture loader.
2
+
3
+ Every L1 / L2 / L3 runner resolves datasets through :func:`load_dataset`, so
4
+ the set of supported keys and the physical file layout lives in one place.
5
+ Fixtures are resolved relative to the repo root (this module's grandparent of
6
+ ``src``) — callers never need to know where the files live.
7
+
8
+ The returned :class:`Fixture` names the dataset, points to its schema-snapshot
9
+ YAML (always present), and optionally to a DDL-only ``schema.sql`` and a gold
10
+ NL-to-SQL question set. Datasets without DDL or a question set (``adversarial``,
11
+ ``analytics_dwh``, ``saas_multitenant``, ``messy_warehouse``) return ``None`` for
12
+ those fields.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+
20
+ from pretensor.benchmark.runner import Dataset
21
+
22
+ __all__ = ["Fixture", "load_dataset"]
23
+
24
+ # Walk up from src/pretensor/benchmark/fixtures.py to the repo root.
25
+ _REPO_ROOT = Path(__file__).resolve().parents[3]
26
+ _SCHEMAS_DIR = _REPO_ROOT / "tests" / "fixtures" / "schemas"
27
+ _DATA_DIR = _REPO_ROOT / "scripts" / "data"
28
+
29
+
30
+ @dataclass(frozen=True, slots=True)
31
+ class Fixture:
32
+ """One benchmark dataset's on-disk layout.
33
+
34
+ ``name`` is the canonical :class:`Dataset` enum key. ``schema_yaml_path``
35
+ is always set and points to a file that :meth:`SchemaSnapshot.from_yaml`
36
+ can parse. The remaining fields are ``None`` for datasets that have no
37
+ DDL dump, no gold question set, or no metric-templates file checked in.
38
+ """
39
+
40
+ name: Dataset
41
+ schema_yaml_path: Path
42
+ ddl_sql_path: Path | None
43
+ questions_path: Path | None
44
+ metric_templates_path: Path | None
45
+
46
+
47
+ def load_dataset(name: str | Dataset) -> Fixture:
48
+ """Resolve a benchmark dataset's fixture paths.
49
+
50
+ Accepts a :class:`Dataset` enum value or its string form. Unknown keys
51
+ raise :class:`ValueError`. The schema YAML must exist — if missing,
52
+ :class:`FileNotFoundError` is raised. DDL and question-set paths are
53
+ returned as ``None`` when the dataset has no such file on disk.
54
+ """
55
+ if isinstance(name, Dataset):
56
+ key = name
57
+ else:
58
+ try:
59
+ key = Dataset(name)
60
+ except ValueError as exc:
61
+ raise ValueError(
62
+ f"Unknown benchmark dataset: {name!r}. "
63
+ f"Known: {[d.value for d in Dataset]}"
64
+ ) from exc
65
+
66
+ schema_path = _SCHEMAS_DIR / f"{key.value}.yaml"
67
+ if not schema_path.exists():
68
+ raise FileNotFoundError(
69
+ f"Schema YAML missing for dataset {key.value!r}: {schema_path}"
70
+ )
71
+
72
+ ddl_path = _DATA_DIR / key.value / "schema.sql"
73
+ questions_path = _DATA_DIR / f"{key.value}_nl2sql_bench.json"
74
+ metric_templates_path = _DATA_DIR / f"{key.value}_metric_templates.yaml"
75
+
76
+ return Fixture(
77
+ name=key,
78
+ schema_yaml_path=schema_path,
79
+ ddl_sql_path=ddl_path if ddl_path.exists() else None,
80
+ questions_path=questions_path if questions_path.exists() else None,
81
+ metric_templates_path=(
82
+ metric_templates_path if metric_templates_path.exists() else None
83
+ ),
84
+ )
@@ -0,0 +1,23 @@
1
+ """L1 graph-quality benchmark — metric implementations and runner.
2
+
3
+ See ``docs/specs/benchmark/spec.md`` §L1 metric definitions for the
4
+ contract this module fulfils. Metric values are wrapped in the
5
+ ``{value, direction}`` envelope from
6
+ :mod:`pretensor.benchmark.results`.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from pretensor.benchmark.l1.metrics import (
12
+ cluster_stability_jaccard,
13
+ inferred_join_pr,
14
+ role_f1,
15
+ )
16
+ from pretensor.benchmark.l1.runner import run_l1
17
+
18
+ __all__ = [
19
+ "cluster_stability_jaccard",
20
+ "inferred_join_pr",
21
+ "role_f1",
22
+ "run_l1",
23
+ ]
@@ -0,0 +1,141 @@
1
+ """Pure L1 metric functions.
2
+
3
+ Each function takes only data — no graph store, no I/O. The runner
4
+ collects predictions from the intelligence pipeline, then hands them
5
+ to these helpers. Boundary cases (empty ground truth, identical
6
+ inputs, etc.) are handled here so the runner never divides by zero.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Iterable, Mapping
12
+
13
+ __all__ = [
14
+ "canonicalise_join_key",
15
+ "cluster_stability_jaccard",
16
+ "inferred_join_pr",
17
+ "role_f1",
18
+ ]
19
+
20
+
21
+ JoinKey = tuple[str, str, str, str]
22
+ """``(src_table, src_column, dst_table, dst_column)`` — bare names.
23
+
24
+ Schema is folded into the table name (``schema.table``) so callers can
25
+ key on either form consistently.
26
+ """
27
+
28
+
29
+ def inferred_join_pr(
30
+ inferred: Iterable[JoinKey],
31
+ declared_fks: Iterable[JoinKey],
32
+ ) -> tuple[float, float]:
33
+ """Precision and recall of inferred joins against declared FK ground truth.
34
+
35
+ Both inputs are sets of ``(src_table, src_column, dst_table, dst_column)``
36
+ tuples. Keys are canonicalised to unordered pairs before comparison —
37
+ declared FKs are directional, but the heuristic discovery layer may
38
+ emit either orientation for the same physical relationship, and an
39
+ inferred edge ``A.x ↔ B.y`` should match the declared FK ``A.x → B.y``
40
+ regardless of orientation.
41
+
42
+ Returns:
43
+ ``(precision, recall)``. Both are ``0.0`` when the corresponding
44
+ denominator is zero (no candidates ⇒ precision 0; no ground truth
45
+ ⇒ recall 0). Identical sets ⇒ ``(1.0, 1.0)``.
46
+ """
47
+ inferred_set = {canonicalise_join_key(k) for k in inferred}
48
+ declared_set = {canonicalise_join_key(k) for k in declared_fks}
49
+
50
+ true_positives = inferred_set & declared_set
51
+ precision = len(true_positives) / len(inferred_set) if inferred_set else 0.0
52
+ recall = len(true_positives) / len(declared_set) if declared_set else 0.0
53
+ return precision, recall
54
+
55
+
56
+ def cluster_stability_jaccard(
57
+ run_a: Iterable[Iterable[str]],
58
+ run_b: Iterable[Iterable[str]],
59
+ ) -> float:
60
+ """Mean pairwise Jaccard between two cluster partitions of the same input.
61
+
62
+ Each cluster is a collection of table node IDs. A score of ``1.0``
63
+ means the two partitions are identical (modulo cluster ordering); a
64
+ score of ``0.0`` means no cluster from ``run_a`` matches any cluster
65
+ in ``run_b``.
66
+
67
+ For each cluster in ``run_a``, this picks the best-matching cluster
68
+ in ``run_b`` (highest Jaccard) and averages those scores. Empty
69
+ inputs on both sides ⇒ ``1.0`` (vacuously stable). Empty input on
70
+ one side only ⇒ ``0.0``.
71
+ """
72
+ a_sets = [frozenset(c) for c in run_a if c]
73
+ b_sets = [frozenset(c) for c in run_b if c]
74
+
75
+ if not a_sets and not b_sets:
76
+ return 1.0
77
+ if not a_sets or not b_sets:
78
+ return 0.0
79
+
80
+ total = 0.0
81
+ for cluster_a in a_sets:
82
+ best = 0.0
83
+ for cluster_b in b_sets:
84
+ # Both sides are filtered to non-empty frozensets above, so
85
+ # the union is always non-empty — no zero-divide guard needed.
86
+ jacc = len(cluster_a & cluster_b) / len(cluster_a | cluster_b)
87
+ if jacc > best:
88
+ best = jacc
89
+ total += best
90
+ return total / len(a_sets)
91
+
92
+
93
+ def role_f1(
94
+ predicted: Mapping[str, str],
95
+ gold: Mapping[str, str],
96
+ ) -> float:
97
+ """Macro-averaged F1 across the gold role labels.
98
+
99
+ For each role appearing in ``gold``, computes precision and recall
100
+ against ``predicted`` and averages the per-class F1 scores. Tables
101
+ in ``gold`` that the predictor did not classify count as misses;
102
+ tables in ``predicted`` whose gold role is absent are ignored
103
+ (they're outside the evaluation set).
104
+
105
+ Returns ``1.0`` when ``gold`` and ``predicted`` are both empty
106
+ (vacuously correct). Returns ``0.0`` when ``gold`` is non-empty but
107
+ ``predicted`` is empty, and vice versa.
108
+ """
109
+ if not gold and not predicted:
110
+ return 1.0
111
+ if not gold or not predicted:
112
+ return 0.0
113
+
114
+ gold_roles = sorted(set(gold.values()))
115
+ f1_sum = 0.0
116
+ for role in gold_roles:
117
+ gold_for_role = {t for t, r in gold.items() if r == role}
118
+ pred_for_role = {t for t, r in predicted.items() if r == role}
119
+ true_positives = gold_for_role & pred_for_role
120
+ precision = len(true_positives) / len(pred_for_role) if pred_for_role else 0.0
121
+ recall = len(true_positives) / len(gold_for_role) if gold_for_role else 0.0
122
+ if precision + recall == 0.0:
123
+ f1 = 0.0
124
+ else:
125
+ f1 = 2 * precision * recall / (precision + recall)
126
+ f1_sum += f1
127
+ return f1_sum / len(gold_roles)
128
+
129
+
130
+ def canonicalise_join_key(key: JoinKey) -> tuple[tuple[str, str], tuple[str, str]]:
131
+ """Collapse a directional ``JoinKey`` to an unordered pair.
132
+
133
+ Treats ``(A.x → B.y)`` and ``(B.y → A.x)`` as the same edge by
134
+ sorting the two endpoint ``(table, column)`` tuples. Exposed so
135
+ other modules (e.g. the runner's per-item collector) can canonicalise
136
+ keys against the same convention without duplicating the logic.
137
+ """
138
+ src_t, src_c, dst_t, dst_c = key
139
+ a = (src_t, src_c)
140
+ b = (dst_t, dst_c)
141
+ return (a, b) if a <= b else (b, a)