sable-sphynx 0.7.0.dev0__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 (174) hide show
  1. sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
  2. sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
  3. sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
  4. sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
  5. sphynx/__init__.py +469 -0
  6. sphynx/__init__.pyi +376 -0
  7. sphynx/_analytical_client.py +77 -0
  8. sphynx/_analytical_values.py +252 -0
  9. sphynx/_audit_client.py +173 -0
  10. sphynx/_comparison_adapters.py +113 -0
  11. sphynx/_componentized_full_rag.py +34 -0
  12. sphynx/_composed_pipeline.py +109 -0
  13. sphynx/_full_rag_grading.py +218 -0
  14. sphynx/_full_rag_trace.py +221 -0
  15. sphynx/_local_eval.py +268 -0
  16. sphynx/_pipeline_core.py +243 -0
  17. sphynx/_pr_client.py +237 -0
  18. sphynx/_rag_diagnostics_calibration.py +285 -0
  19. sphynx/_rag_diagnostics_catalog.py +39 -0
  20. sphynx/_rag_diagnostics_common.py +26 -0
  21. sphynx/_rag_diagnostics_contracts.py +186 -0
  22. sphynx/_rag_diagnostics_engine.py +243 -0
  23. sphynx/_rag_diagnostics_importers.py +242 -0
  24. sphynx/_rag_diagnostics_ranking.py +81 -0
  25. sphynx/_rag_diagnostics_semantic.py +325 -0
  26. sphynx/_rag_ranking.py +149 -0
  27. sphynx/_speech_benchmark_scoring.py +140 -0
  28. sphynx/_speech_benchmark_sources.py +275 -0
  29. sphynx/_speech_complete_scoring.py +218 -0
  30. sphynx/_speech_complete_source.py +118 -0
  31. sphynx/_speech_kido.py +294 -0
  32. sphynx/_speech_scoring.py +77 -0
  33. sphynx/_track_capture.py +213 -0
  34. sphynx/analytical_views.py +515 -0
  35. sphynx/assessment_contracts.py +378 -0
  36. sphynx/assessment_selection.py +57 -0
  37. sphynx/audit_access.py +245 -0
  38. sphynx/audit_contracts.py +204 -0
  39. sphynx/audit_requests.py +180 -0
  40. sphynx/authoring.py +188 -0
  41. sphynx/benchmarks.py +568 -0
  42. sphynx/candidates.py +62 -0
  43. sphynx/catalog.py +272 -0
  44. sphynx/chunking.py +372 -0
  45. sphynx/clickhouse_reader.py +169 -0
  46. sphynx/client.py +486 -0
  47. sphynx/comparison_core.py +149 -0
  48. sphynx/comparison_views.py +223 -0
  49. sphynx/components.py +1661 -0
  50. sphynx/contracts.py +514 -0
  51. sphynx/customer_rerank.py +299 -0
  52. sphynx/customer_retrieval.py +146 -0
  53. sphynx/data/benchmark-releases.v1.json +164 -0
  54. sphynx/data/public-benchmark-release.v1.json +354 -0
  55. sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
  56. sphynx/data/rag-paper-references.v1.json +176 -0
  57. sphynx/data/rag-paper-schemas.v1.json +1069 -0
  58. sphynx/data/rag-rerank-v1.json +507 -0
  59. sphynx/dataset_catalog_data.py +1214 -0
  60. sphynx/dataset_readiness.py +138 -0
  61. sphynx/dataset_v3.py +999 -0
  62. sphynx/demo.py +130 -0
  63. sphynx/demo_candidates/__init__.py +1 -0
  64. sphynx/demo_candidates/dense.py +11 -0
  65. sphynx/demo_candidates/lexical.py +21 -0
  66. sphynx/diagnostics.py +207 -0
  67. sphynx/documentation.py +737 -0
  68. sphynx/documentation_candidate.py +939 -0
  69. sphynx/evals.py +538 -0
  70. sphynx/evaluation_protocols.py +124 -0
  71. sphynx/evaluator_bindings.py +370 -0
  72. sphynx/evidence.py +352 -0
  73. sphynx/execution_capabilities.py +109 -0
  74. sphynx/fast_search.py +285 -0
  75. sphynx/full_rag.py +341 -0
  76. sphynx/hosted/__init__.py +18 -0
  77. sphynx/hosted/benchmark_intake.py +218 -0
  78. sphynx/hosted/contracts.py +361 -0
  79. sphynx/hosted/model_admission.py +130 -0
  80. sphynx/hosted/model_artifacts.py +298 -0
  81. sphynx/hosted/model_contracts.py +63 -0
  82. sphynx/hosted/model_gcs.py +181 -0
  83. sphynx/hosted/model_history.py +84 -0
  84. sphynx/hosted/model_session.py +233 -0
  85. sphynx/hosted/public_suite.py +74 -0
  86. sphynx/hosted/registry.py +97 -0
  87. sphynx/hosted/revisions.py +126 -0
  88. sphynx/hosted/sandbox.py +105 -0
  89. sphynx/hosted/submission.py +287 -0
  90. sphynx/hosted_workflow.py +451 -0
  91. sphynx/http_transport.py +86 -0
  92. sphynx/integrated_rag.py +352 -0
  93. sphynx/integrated_workflow.py +552 -0
  94. sphynx/integration_contracts.py +474 -0
  95. sphynx/invocation.py +791 -0
  96. sphynx/invocation_candidates.py +392 -0
  97. sphynx/invocation_catalog_data.py +40 -0
  98. sphynx/learned_catalog_data.py +30 -0
  99. sphynx/learned_rerank.py +267 -0
  100. sphynx/legacy_full_rag.py +67 -0
  101. sphynx/lifecycle.py +885 -0
  102. sphynx/lifecycle_workflow.py +490 -0
  103. sphynx/metric_views.py +474 -0
  104. sphynx/metrics.py +282 -0
  105. sphynx/model_adapters.py +438 -0
  106. sphynx/model_catalog_data.py +30 -0
  107. sphynx/official_benchmarks.py +385 -0
  108. sphynx/packs.py +258 -0
  109. sphynx/pipeline.py +313 -0
  110. sphynx/pipeline_workflow.py +551 -0
  111. sphynx/pr_evaluations.py +560 -0
  112. sphynx/progress.py +89 -0
  113. sphynx/public_bundle.py +173 -0
  114. sphynx/public_eval.py +62 -0
  115. sphynx/public_rerank.py +355 -0
  116. sphynx/py.typed +0 -0
  117. sphynx/pytest_plugin.py +62 -0
  118. sphynx/question_robustness.py +437 -0
  119. sphynx/rag.py +329 -0
  120. sphynx/rag_diagnostics.py +309 -0
  121. sphynx/rag_papers/__init__.py +29 -0
  122. sphynx/rag_papers/__main__.py +45 -0
  123. sphynx/rag_papers/candidates.py +19 -0
  124. sphynx/rag_papers/contracts.py +282 -0
  125. sphynx/rag_papers/datasets.py +291 -0
  126. sphynx/rag_papers/experiments.py +116 -0
  127. sphynx/rag_papers/judges.py +308 -0
  128. sphynx/rag_papers/registry.py +33 -0
  129. sphynx/rag_papers/runner.py +176 -0
  130. sphynx/rag_papers/sable.py +135 -0
  131. sphynx/rag_papers/schemas.py +12 -0
  132. sphynx/rag_papers/scoring.py +249 -0
  133. sphynx/rag_papers/semantic.py +288 -0
  134. sphynx/rag_papers/trec.py +80 -0
  135. sphynx/recorded_scores.py +352 -0
  136. sphynx/records.py +124 -0
  137. sphynx/reflex_traces.py +193 -0
  138. sphynx/report_rendering.py +71 -0
  139. sphynx/rerank_family_contracts.py +150 -0
  140. sphynx/rerank_workflow.py +424 -0
  141. sphynx/research_candidate.py +190 -0
  142. sphynx/results.py +852 -0
  143. sphynx/retrieval.py +750 -0
  144. sphynx/retrieval_candidates.py +295 -0
  145. sphynx/retrieval_catalog_data.py +19 -0
  146. sphynx/retrieval_recordings.py +125 -0
  147. sphynx/reviewer_audit.py +266 -0
  148. sphynx/routing_policies.py +375 -0
  149. sphynx/runner.py +544 -0
  150. sphynx/sable_rerank.py +489 -0
  151. sphynx/score_reliability.py +393 -0
  152. sphynx/speech_benchmarks.py +204 -0
  153. sphynx/speech_campaigns.py +378 -0
  154. sphynx/speech_complete.py +236 -0
  155. sphynx/speech_contracts.py +225 -0
  156. sphynx/speech_evidence.py +429 -0
  157. sphynx/speech_listening.py +115 -0
  158. sphynx/speech_multilingual.py +71 -0
  159. sphynx/speech_tts.py +479 -0
  160. sphynx/sql/001_sync_v1.sql +30 -0
  161. sphynx/stage_contracts.py +199 -0
  162. sphynx/statistical_catalog_data.py +53 -0
  163. sphynx/statistical_rerank.py +232 -0
  164. sphynx/sync.py +797 -0
  165. sphynx/workbench.py +378 -0
  166. sphynx/workbench_catalog_data.py +13 -0
  167. sphynx/workbench_tracks.py +78 -0
  168. sphynx/workflow_async.py +197 -0
  169. sphynx/workflow_contracts.py +594 -0
  170. sphynx/workflow_decisions.py +211 -0
  171. sphynx/workflow_defaults.py +16 -0
  172. sphynx/workflow_extensions.py +212 -0
  173. sphynx/workflow_registry.py +513 -0
  174. sphynx/workflows.py +792 -0
@@ -0,0 +1,513 @@
1
+ """Operator-owned extension registration for the shared workflow.
2
+
3
+ Registration and discovery inspect declared metadata only. They do not import
4
+ extensions, read assets, validate payloads, execute candidates, or run evaluator
5
+ and policy callbacks. The operator supplies objects explicitly. Their declared
6
+ implementation labels identify a binding; they do not authenticate source bytes.
7
+
8
+ Each role has an independent ID space. Native catalogs remain authoritative for
9
+ native records; this registry contains only explicitly enabled common extensions.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from collections.abc import Callable, Collection, Mapping
15
+ from dataclasses import dataclass, field
16
+ from typing import Any
17
+
18
+ from .workflow_contracts import (
19
+ BackendDescriptor, CaseProvider, Criterion, Dataset, DatasetDefinition,
20
+ EvaluatorDefinition, MetricSpec, PayloadSchema, SuiteDefinition,
21
+ WorkflowBackend, WorkflowContractError, _object, _text, _thaw,
22
+ contract_digest,
23
+ )
24
+
25
+ _IDENTIFIER = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/@+\-]*\Z")
26
+ _BACKEND_METHODS = (
27
+ "describe", "cases", "resolve", "admit", "recheck", "execute",
28
+ "accepts_reference", "reference", "assess", "report", "assessments", "compare",
29
+ )
30
+ _POLICY_KINDS = frozenset({"aggregation", "comparison", "report", "decision"})
31
+
32
+
33
+ class WorkflowRegistryError(WorkflowContractError):
34
+ """An extension is missing, incompatible, duplicated, or has drifted."""
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class CallbackDefinition:
39
+ """Declared policy/projection identity; lookup never invokes its callback."""
40
+
41
+ callback_id: str
42
+ version: str
43
+ implementation_id: str
44
+ kind: str
45
+ metadata: Mapping[str, Any] = field(default_factory=dict)
46
+
47
+ def __post_init__(self) -> None:
48
+ for name in ("callback_id", "version", "implementation_id", "kind"):
49
+ _text(getattr(self, name), name)
50
+ object.__setattr__(self, "metadata", _object(self.metadata, "metadata"))
51
+
52
+ def to_dict(self) -> dict[str, Any]:
53
+ return {"callback_id": self.callback_id, "version": self.version,
54
+ "implementation_id": self.implementation_id, "kind": self.kind,
55
+ "metadata": _thaw(self.metadata)}
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class _ProviderSchemaBinding:
60
+ role: str
61
+ digest: str
62
+ validator: Callable[..., Any] = field(repr=False, compare=False)
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class _Registration:
67
+ value: Any
68
+ definition: Any
69
+ digest: str
70
+ callback: Callable[..., Any] | None = None
71
+ schema_bindings: tuple[_ProviderSchemaBinding, ...] | None = None
72
+
73
+
74
+ class WorkflowRegistry:
75
+ """Explicit, mutable operator registry with pinned registration metadata.
76
+
77
+ IDs are namespaced by default. ``legacy_ids`` permits an operator's exact,
78
+ known opaque native IDs without relaxing the convention for new extensions.
79
+ Duplicate IDs within a role are rejected, even for another version. Use a
80
+ separate registry to select a different binding. Criterion and metric IDs
81
+ may have the same spelling without implying a one-to-one relationship.
82
+
83
+ Backend ``descriptor``/``describe`` and provider ``definition`` must be pure
84
+ metadata access. Registration/discovery do not call asset or runtime methods.
85
+ ``resolve_suite`` checks declared cross-references and descriptor drift; it
86
+ is not execution admission and does not grant reference access.
87
+
88
+ Register a concrete Dataset's input, reference and selection schemas first.
89
+ Registration and provider lookup require their exact registered metadata
90
+ and validator bindings. Custom CaseProvider implementations remain subject
91
+ to their backend's validation contract; the registry does not inspect their
92
+ private schema implementation or invoke provider methods to discover it.
93
+ """
94
+
95
+ def __init__(self, *, legacy_ids: Collection[str] = ()) -> None:
96
+ if isinstance(legacy_ids, (str, bytes)) or not isinstance(legacy_ids, Collection):
97
+ raise WorkflowRegistryError("legacy_ids must be an explicit collection of native IDs")
98
+ self._legacy_ids = frozenset(_text(value, "legacy ID") for value in legacy_ids)
99
+ self._stores: dict[str, dict[str, _Registration]] = {
100
+ role: {} for role in ("backend", "schema", "dataset", "provider", "criterion",
101
+ "evaluator", "metric", "suite", "policy", "projection")
102
+ }
103
+
104
+ def _id(self, value: str, role: str) -> str:
105
+ _text(value, role + " ID")
106
+ if not _IDENTIFIER.fullmatch(value):
107
+ raise WorkflowRegistryError(f"{role} ID must be a stable identifier")
108
+ if "." not in value and ":" not in value and value not in self._legacy_ids:
109
+ raise WorkflowRegistryError(f"{role} ID must be namespaced or an explicit legacy ID")
110
+ return value
111
+
112
+ def fork(self) -> WorkflowRegistry:
113
+ """Stage trusted registrations without changing this registry's stores.
114
+
115
+ Bindings keep their exact validator, callback and implementation objects.
116
+ This isolates registration membership on factory failure. It does not
117
+ sandbox trusted code or copy mutable state inside an implementation.
118
+ Lookup still checks the original registration pins for drift.
119
+ """
120
+ for identifier in self._stores["backend"]:
121
+ self.get_backend(identifier)
122
+ for identifier in self._stores["schema"]:
123
+ self.get_schema(identifier)
124
+ for identifier in self._stores["provider"]:
125
+ self.get_provider(identifier)
126
+ selected = WorkflowRegistry(legacy_ids=self._legacy_ids)
127
+ selected._stores = {role: dict(rows) for role, rows in self._stores.items()}
128
+ return selected
129
+
130
+ def _register(self, role: str, identifier: str, value: Any, definition: Any,
131
+ callback: Callable[..., Any] | None = None, *,
132
+ schema_bindings: tuple[_ProviderSchemaBinding, ...] | None = None) -> None:
133
+ identifier = self._id(identifier, role)
134
+ if identifier in self._stores[role]:
135
+ raise WorkflowRegistryError(f"duplicate {role} ID: {identifier}")
136
+ self._stores[role][identifier] = _Registration(
137
+ value, definition, contract_digest(definition.to_dict()), callback, schema_bindings,
138
+ )
139
+
140
+ def _get(self, role: str, identifier: str) -> _Registration:
141
+ self._id(identifier, role)
142
+ try:
143
+ return self._stores[role][identifier]
144
+ except KeyError:
145
+ raise WorkflowRegistryError(f"unregistered {role}: {identifier}") from None
146
+
147
+ def _definitions(self, role: str) -> tuple[Any, ...]:
148
+ # These are frozen snapshots captured during registration, not live
149
+ # backend/provider properties or runtime callbacks.
150
+ return tuple(self._stores[role][key].definition for key in sorted(self._stores[role]))
151
+
152
+ @staticmethod
153
+ def _unchanged(registration: _Registration, definition: Any, role: str) -> None:
154
+ if contract_digest(definition.to_dict()) != registration.digest:
155
+ raise WorkflowRegistryError(f"registered {role} metadata has changed")
156
+
157
+ def register_backend(self, backend: WorkflowBackend) -> BackendDescriptor:
158
+ descriptor = getattr(backend, "descriptor", None)
159
+ if not isinstance(descriptor, BackendDescriptor):
160
+ raise WorkflowRegistryError("backend requires a BackendDescriptor")
161
+ if any(not callable(getattr(backend, name, None)) for name in _BACKEND_METHODS):
162
+ raise WorkflowRegistryError("backend does not implement the workflow protocol")
163
+ from .execution_capabilities import execution_paths
164
+ try:
165
+ execution_paths(descriptor)
166
+ except WorkflowContractError as exc:
167
+ raise WorkflowRegistryError(str(exc)) from None
168
+ for suite_id in descriptor.suite_ids:
169
+ self._id(suite_id, "suite")
170
+ self._register("backend", descriptor.backend_id, backend, descriptor)
171
+ return descriptor
172
+
173
+ def get_backend(self, backend_id: str) -> WorkflowBackend:
174
+ registration = self._get("backend", backend_id)
175
+ descriptor = getattr(registration.value, "descriptor", None)
176
+ if not isinstance(descriptor, BackendDescriptor):
177
+ raise WorkflowRegistryError("registered backend descriptor is invalid")
178
+ self._unchanged(registration, descriptor, "backend")
179
+ return registration.value
180
+
181
+ def list_backends(self) -> tuple[BackendDescriptor, ...]:
182
+ return self._definitions("backend")
183
+
184
+ def register_schema(self, schema: PayloadSchema) -> PayloadSchema:
185
+ if not isinstance(schema, PayloadSchema):
186
+ raise WorkflowRegistryError("schema must be PayloadSchema")
187
+ self._register("schema", schema.schema_id, schema, schema, schema.validator)
188
+ return schema
189
+
190
+ def get_schema(self, schema_id: str) -> PayloadSchema:
191
+ registration = self._get("schema", schema_id)
192
+ self._unchanged(registration, registration.value, "schema")
193
+ if registration.value.validator is not registration.callback:
194
+ raise WorkflowRegistryError("registered schema validator has changed")
195
+ return registration.value
196
+
197
+ def list_schemas(self) -> tuple[PayloadSchema, ...]:
198
+ return self._definitions("schema")
199
+
200
+ def validate_payload(self, schema_id: str, value: Any) -> Any:
201
+ """Explicitly execute the registered validator on a detached snapshot."""
202
+ return self.get_schema(schema_id).validate(value)
203
+
204
+ def register_dataset(self, definition: DatasetDefinition) -> DatasetDefinition:
205
+ if not isinstance(definition, DatasetDefinition):
206
+ raise WorkflowRegistryError("dataset must be DatasetDefinition")
207
+ for role, identifier in (("provider", definition.provider_id),
208
+ ("schema", definition.input_schema_id)):
209
+ self._id(identifier, role)
210
+ if definition.reference_schema_id is not None:
211
+ self._id(definition.reference_schema_id, "schema")
212
+ self._register("dataset", definition.dataset_id, definition, definition)
213
+ return definition
214
+
215
+ def get_dataset(self, dataset_id: str) -> DatasetDefinition:
216
+ registration = self._get("dataset", dataset_id)
217
+ self._unchanged(registration, registration.value, "dataset")
218
+ return registration.value
219
+
220
+ def list_datasets(self) -> tuple[DatasetDefinition, ...]:
221
+ return self._definitions("dataset")
222
+
223
+ def _provider_schema_bindings(self, provider: CaseProvider,
224
+ definition: DatasetDefinition) -> tuple[_ProviderSchemaBinding, ...] | None:
225
+ if not isinstance(provider, Dataset):
226
+ return None
227
+ bindings = []
228
+ for role, schema, expected_id in (
229
+ ("input", provider.input_schema, definition.input_schema_id),
230
+ ("reference", provider.reference_schema, definition.reference_schema_id),
231
+ ("selection", provider.selection_schema, None),
232
+ ):
233
+ if schema is None and role != "input":
234
+ continue
235
+ if not isinstance(schema, PayloadSchema):
236
+ raise WorkflowRegistryError(f"concrete dataset {role} schema is invalid")
237
+ if role != "selection" and schema.schema_id != expected_id:
238
+ raise WorkflowRegistryError(f"concrete dataset {role} schema differs from its definition")
239
+ registered = self.get_schema(schema.schema_id)
240
+ digest = contract_digest(schema.to_dict())
241
+ if (digest != contract_digest(registered.to_dict())
242
+ or schema.validator is not registered.validator):
243
+ raise WorkflowRegistryError(f"concrete dataset {role} schema differs from registered binding")
244
+ bindings.append(_ProviderSchemaBinding(role, digest, schema.validator))
245
+ return tuple(bindings)
246
+
247
+ def register_provider(self, provider: CaseProvider) -> DatasetDefinition:
248
+ definition = getattr(provider, "definition", None)
249
+ if not isinstance(definition, DatasetDefinition):
250
+ raise WorkflowRegistryError("provider requires a DatasetDefinition")
251
+ if not callable(getattr(provider, "cases", None)) or not callable(getattr(provider, "reference_for", None)):
252
+ raise WorkflowRegistryError("provider does not implement the case-provider protocol")
253
+ self._id(definition.provider_id, "provider")
254
+ if definition.provider_id in self._stores["provider"]:
255
+ raise WorkflowRegistryError(f"duplicate provider ID: {definition.provider_id}")
256
+ existing = self._stores["dataset"].get(definition.dataset_id)
257
+ if existing is not None and existing.digest != contract_digest(definition.to_dict()):
258
+ raise WorkflowRegistryError("provider definition differs from registered dataset")
259
+ bindings = self._provider_schema_bindings(provider, definition)
260
+ if existing is None:
261
+ self.register_dataset(definition)
262
+ self._register("provider", definition.provider_id, provider, definition,
263
+ schema_bindings=bindings)
264
+ return definition
265
+
266
+ def get_provider(self, provider_id: str) -> CaseProvider:
267
+ registration = self._get("provider", provider_id)
268
+ definition = getattr(registration.value, "definition", None)
269
+ if not isinstance(definition, DatasetDefinition):
270
+ raise WorkflowRegistryError("registered provider definition is invalid")
271
+ self._unchanged(registration, definition, "provider")
272
+ current = self._provider_schema_bindings(registration.value, definition)
273
+ expected = registration.schema_bindings
274
+ if ((current is None) != (expected is None)
275
+ or (current is not None and expected is not None
276
+ and (len(current) != len(expected)
277
+ or any(a.role != b.role or a.digest != b.digest or a.validator is not b.validator
278
+ for a, b in zip(current, expected))))):
279
+ raise WorkflowRegistryError("registered concrete dataset schema bindings have changed")
280
+ return registration.value
281
+
282
+ def list_providers(self) -> tuple[DatasetDefinition, ...]:
283
+ return self._definitions("provider")
284
+
285
+ def register_criterion(self, criterion: Criterion) -> Criterion:
286
+ if not isinstance(criterion, Criterion):
287
+ raise WorkflowRegistryError("criterion must be Criterion")
288
+ self._register("criterion", criterion.criterion_id, criterion, criterion)
289
+ return criterion
290
+
291
+ def get_criterion(self, criterion_id: str) -> Criterion:
292
+ registration = self._get("criterion", criterion_id)
293
+ self._unchanged(registration, registration.value, "criterion")
294
+ return registration.value
295
+
296
+ def list_criteria(self) -> tuple[Criterion, ...]:
297
+ return self._definitions("criterion")
298
+
299
+ def register_evaluator(self, definition: EvaluatorDefinition,
300
+ evaluator: Callable[..., Any] | None = None) -> EvaluatorDefinition:
301
+ if not isinstance(definition, EvaluatorDefinition):
302
+ raise WorkflowRegistryError("evaluator must be EvaluatorDefinition")
303
+ if evaluator is not None and not callable(evaluator):
304
+ raise WorkflowRegistryError("evaluator callback must be callable")
305
+ for role, identifiers in (("criterion", definition.criterion_ids),
306
+ ("schema", definition.assessment_schema_ids),
307
+ ("metric", definition.metric_ids)):
308
+ for identifier in identifiers:
309
+ self._id(identifier, role)
310
+ self._register("evaluator", definition.evaluator_id, definition, definition, evaluator)
311
+ return definition
312
+
313
+ def get_evaluator(self, evaluator_id: str) -> EvaluatorDefinition:
314
+ registration = self._get("evaluator", evaluator_id)
315
+ self._unchanged(registration, registration.value, "evaluator")
316
+ return registration.value
317
+
318
+ def get_evaluator_callable(self, evaluator_id: str) -> Callable[..., Any]:
319
+ self.get_evaluator(evaluator_id)
320
+ callback = self._get("evaluator", evaluator_id).callback
321
+ if callback is None:
322
+ raise WorkflowRegistryError("evaluator execution is backend-owned; no callback is registered")
323
+ return callback
324
+
325
+ def list_evaluators(self) -> tuple[EvaluatorDefinition, ...]:
326
+ return self._definitions("evaluator")
327
+
328
+ def register_metric(self, metric: MetricSpec) -> MetricSpec:
329
+ if not isinstance(metric, MetricSpec):
330
+ raise WorkflowRegistryError("metric must be MetricSpec")
331
+ for policy_id in (metric.aggregation_policy, metric.comparison_policy):
332
+ if policy_id is not None:
333
+ self._id(policy_id, "policy")
334
+ self._register("metric", metric.metric_id, metric, metric)
335
+ return metric
336
+
337
+ def get_metric(self, metric_id: str) -> MetricSpec:
338
+ registration = self._get("metric", metric_id)
339
+ self._unchanged(registration, registration.value, "metric")
340
+ return registration.value
341
+
342
+ def list_metrics(self) -> tuple[MetricSpec, ...]:
343
+ return self._definitions("metric")
344
+
345
+ def register_suite(self, suite: SuiteDefinition) -> SuiteDefinition:
346
+ if not isinstance(suite, SuiteDefinition):
347
+ raise WorkflowRegistryError("suite must be SuiteDefinition")
348
+ self._id(suite.backend_id, "backend")
349
+ for role, identifiers in (("criterion", suite.criterion_ids),
350
+ ("evaluator", suite.evaluator_ids), ("metric", suite.metric_ids)):
351
+ for identifier in identifiers:
352
+ self._id(identifier, role)
353
+ for dataset in suite.datasets:
354
+ self._id(dataset.dataset_id, "dataset")
355
+ self._register("suite", suite.suite_id, suite, suite)
356
+ return suite
357
+
358
+ def get_suite(self, suite_id: str) -> SuiteDefinition:
359
+ registration = self._get("suite", suite_id)
360
+ self._unchanged(registration, registration.value, "suite")
361
+ return registration.value
362
+
363
+ def list_suites(self) -> tuple[SuiteDefinition, ...]:
364
+ return self._definitions("suite")
365
+
366
+ def register_policy(self, policy_id: str, *, version: str, implementation_id: str,
367
+ kind: str, callback: Callable[..., Any] | None = None,
368
+ metadata: Mapping[str, Any] | None = None) -> CallbackDefinition:
369
+ if type(kind) is not str or kind not in _POLICY_KINDS:
370
+ raise WorkflowRegistryError("policy kind must be aggregation, comparison, report, or decision")
371
+ if callback is not None and not callable(callback):
372
+ raise WorkflowRegistryError("policy callback must be callable")
373
+ definition = CallbackDefinition(policy_id, version, implementation_id, kind,
374
+ metadata if metadata is not None else {})
375
+ self._register("policy", policy_id, definition, definition, callback)
376
+ return definition
377
+
378
+ def get_policy(self, policy_id: str, *, kind: str | None = None) -> CallbackDefinition:
379
+ registration = self._get("policy", policy_id)
380
+ self._unchanged(registration, registration.value, "policy")
381
+ if kind is not None and registration.definition.kind != kind:
382
+ raise WorkflowRegistryError(f"policy {policy_id} has incompatible kind")
383
+ return registration.value
384
+
385
+ def get_policy_callable(self, policy_id: str, *, kind: str | None = None) -> Callable[..., Any]:
386
+ self.get_policy(policy_id, kind=kind)
387
+ callback = self._get("policy", policy_id).callback
388
+ if callback is None:
389
+ raise WorkflowRegistryError("policy execution is backend-owned; no callback is registered")
390
+ return callback
391
+
392
+ def list_policies(self) -> tuple[CallbackDefinition, ...]:
393
+ return self._definitions("policy")
394
+
395
+ def register_projection(self, schema_id: str, projector: Callable[..., Any], *,
396
+ version: str, implementation_id: str,
397
+ metadata: Mapping[str, Any] | None = None) -> CallbackDefinition:
398
+ self.get_schema(schema_id)
399
+ if not callable(projector):
400
+ raise WorkflowRegistryError("projection callback must be callable")
401
+ definition = CallbackDefinition(schema_id, version, implementation_id, "projection",
402
+ metadata if metadata is not None else {})
403
+ self._register("projection", schema_id, definition, definition, projector)
404
+ return definition
405
+
406
+ def get_projection(self, schema_id: str) -> CallbackDefinition:
407
+ registration = self._get("projection", schema_id)
408
+ self._unchanged(registration, registration.value, "projection")
409
+ self.get_schema(schema_id)
410
+ return registration.value
411
+
412
+ def get_projection_callable(self, schema_id: str) -> Callable[..., Any]:
413
+ self.get_projection(schema_id)
414
+ callback = self._get("projection", schema_id).callback
415
+ assert callback is not None
416
+ return callback
417
+
418
+ def list_projections(self) -> tuple[CallbackDefinition, ...]:
419
+ return self._definitions("projection")
420
+
421
+ def resolve_suite(self, suite_id: str, *, scope: str | None = None,
422
+ mode: str | None = None) -> tuple[SuiteDefinition, WorkflowBackend]:
423
+ """Bind pure metadata and check capabilities; do not admit execution."""
424
+ suite = self.get_suite(suite_id)
425
+ backend = self.get_backend(suite.backend_id)
426
+ descriptor = self._get("backend", suite.backend_id).definition
427
+ all_required_capabilities = set(suite.required_capabilities)
428
+ if descriptor.suite_ids and suite.suite_id not in descriptor.suite_ids:
429
+ raise WorkflowRegistryError("backend does not declare the selected suite")
430
+ if scope is not None and scope not in descriptor.supported_scopes:
431
+ raise WorkflowRegistryError(f"backend does not support scope {scope!r}")
432
+ if mode is not None and mode not in descriptor.supported_modes:
433
+ raise WorkflowRegistryError(f"backend does not support mode {mode!r}")
434
+ if not set(suite.required_capabilities).issubset(descriptor.capabilities):
435
+ raise WorkflowRegistryError("backend lacks required suite capabilities")
436
+ from .execution_capabilities import validate_execution_requirement
437
+ try:
438
+ validate_execution_requirement(descriptor, scope=scope, mode=mode,
439
+ capabilities=suite.required_capabilities)
440
+ if "evaluation_protocol" in suite.metadata:
441
+ from .evaluation_protocols import EvaluationProtocol, PROTOCOL_SCHEMA
442
+ protocol = EvaluationProtocol.from_dict(suite.metadata["evaluation_protocol"])
443
+ all_required_capabilities.add(PROTOCOL_SCHEMA)
444
+ validate_execution_requirement(descriptor, scope=scope, mode=mode,
445
+ capabilities=(PROTOCOL_SCHEMA,))
446
+ self.get_schema(protocol.input_schema_id)
447
+ self.get_schema(protocol.output_schema_id)
448
+ if any(item.input_schema_id != protocol.input_schema_id for item in suite.datasets):
449
+ raise WorkflowRegistryError("protocol input schema differs from suite datasets")
450
+ except WorkflowContractError as exc:
451
+ raise WorkflowRegistryError(str(exc)) from None
452
+ for criterion_id in suite.criterion_ids:
453
+ self.get_criterion(criterion_id)
454
+ for metric_id in suite.metric_ids:
455
+ metric = self.get_metric(metric_id)
456
+ if metric.aggregation_policy is not None:
457
+ self.get_policy(metric.aggregation_policy, kind="aggregation")
458
+ if metric.comparison_policy is not None:
459
+ self.get_policy(metric.comparison_policy, kind="comparison")
460
+ for evaluator_id in suite.evaluator_ids:
461
+ evaluator = self.get_evaluator(evaluator_id)
462
+ all_required_capabilities.update(evaluator.required_capabilities)
463
+ if not set(evaluator.required_capabilities).issubset(descriptor.capabilities):
464
+ raise WorkflowRegistryError("backend lacks required evaluator capabilities")
465
+ try:
466
+ validate_execution_requirement(descriptor, scope=scope, mode=mode,
467
+ capabilities=evaluator.required_capabilities)
468
+ except WorkflowContractError as exc:
469
+ raise WorkflowRegistryError(str(exc)) from None
470
+ if not set(evaluator.criterion_ids).issubset(suite.criterion_ids):
471
+ raise WorkflowRegistryError("evaluator refers to criteria outside the suite")
472
+ if not set(evaluator.metric_ids).issubset(suite.metric_ids):
473
+ raise WorkflowRegistryError("evaluator refers to metrics outside the suite")
474
+ for schema_id in evaluator.assessment_schema_ids:
475
+ self.get_schema(schema_id)
476
+ for dataset in suite.datasets:
477
+ registered = self.get_dataset(dataset.dataset_id)
478
+ if contract_digest(registered.to_dict()) != contract_digest(dataset.to_dict()):
479
+ raise WorkflowRegistryError("suite dataset differs from registered definition")
480
+ provider = self.get_provider(dataset.provider_id)
481
+ if contract_digest(provider.definition.to_dict()) != contract_digest(dataset.to_dict()):
482
+ raise WorkflowRegistryError("case provider differs from suite dataset")
483
+ self.get_schema(dataset.input_schema_id)
484
+ if dataset.reference_schema_id is not None:
485
+ self.get_schema(dataset.reference_schema_id)
486
+ try:
487
+ validate_execution_requirement(descriptor, scope=scope, mode=mode,
488
+ capabilities=tuple(sorted(all_required_capabilities)))
489
+ except WorkflowContractError as exc:
490
+ raise WorkflowRegistryError(str(exc)) from None
491
+ described = backend.describe(suite_id)
492
+ if not isinstance(described, SuiteDefinition) or described.definition_digest != suite.definition_digest:
493
+ raise WorkflowRegistryError("backend suite description differs from registered definition")
494
+ self.get_backend(suite.backend_id)
495
+ return suite, backend
496
+
497
+ def verify_suite(self, suite: str | SuiteDefinition, *, scope: str | None = None,
498
+ mode: str | None = None) -> tuple[SuiteDefinition, WorkflowBackend]:
499
+ """Check a registered suite or require an exact supplied metadata match."""
500
+ if isinstance(suite, SuiteDefinition):
501
+ registered = self.get_suite(suite.suite_id)
502
+ if registered.definition_digest != suite.definition_digest:
503
+ raise WorkflowRegistryError("supplied suite differs from registered definition")
504
+ suite_id = suite.suite_id
505
+ else:
506
+ suite_id = suite
507
+ return self.resolve_suite(suite_id, scope=scope, mode=mode)
508
+
509
+ def to_dict(self) -> dict[str, Any]:
510
+ """Return registration metadata only; do not call backend/provider code."""
511
+ return {"schema_version": "sphynx.workflow-registry.v1",
512
+ "roles": {role: [item.to_dict() for item in self._definitions(role)]
513
+ for role in self._stores}}