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
sphynx/evals.py ADDED
@@ -0,0 +1,538 @@
1
+ """Versioned evaluation-function contracts, without execution or provider calls.
2
+
3
+ Resolution expands typed defaults and binds operator-registered artifact metadata.
4
+ It does not fetch/verify artifact bytes, authenticate a user, or run an adapter.
5
+ The safe-integer JSON domain gives future Rust implementations explicit hash rules.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import re
12
+ from dataclasses import InitVar, dataclass
13
+ from types import MappingProxyType
14
+ from typing import Any, Collection, Mapping
15
+
16
+ DEFINITION_SCHEMA = "sphynx.eval.definition.v1"
17
+ INVOCATION_SCHEMA = "sphynx.eval.invocation.v1"
18
+ CANONICALIZATION = "sphynx.safe-integer-json.v1"
19
+ _MAX_INTEGER = 2**53 - 1
20
+ _MAX_BYTES = 131_072
21
+ _MISSING = object()
22
+ _ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:@/-]{0,255}\Z")
23
+ _DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
24
+ _RESOLUTION_TOKEN = object()
25
+ _DECIMAL = re.compile(r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]*[1-9])?\Z")
26
+
27
+
28
+ class EvaluationContractError(ValueError):
29
+ """A definition, parameter, artifact binding, or invocation is invalid."""
30
+
31
+
32
+ def canonical_contract_json(value: Any) -> str:
33
+ """Sorted UTF-8 JSON, safe integers, no floats/surrogates/normalization.
34
+
35
+ Decimal quantities can use a string schema with canonical_decimal format.
36
+ This serializer deliberately does not replace the legacy SDK serializer.
37
+ """
38
+ nodes = [0]
39
+ string_bytes = [0]
40
+
41
+ def check(item: Any, depth: int) -> None:
42
+ nodes[0] += 1
43
+ if depth > 24 or nodes[0] > 8192:
44
+ raise EvaluationContractError("contract JSON exceeds structural limits")
45
+ if item is None or type(item) is bool:
46
+ return
47
+ if type(item) is int:
48
+ if abs(item) > _MAX_INTEGER:
49
+ raise EvaluationContractError("integer exceeds the interoperable safe-integer range")
50
+ return
51
+ if type(item) is str:
52
+ if len(item) > _MAX_BYTES:
53
+ raise EvaluationContractError("contract string exceeds the byte limit")
54
+ try:
55
+ string_bytes[0] += len(item.encode("utf-8", errors="strict"))
56
+ except UnicodeEncodeError:
57
+ raise EvaluationContractError("strings must contain valid Unicode scalar values") from None
58
+ if string_bytes[0] > _MAX_BYTES:
59
+ raise EvaluationContractError("contract strings exceed the byte limit")
60
+ return
61
+ if isinstance(item, Mapping):
62
+ for key, child in item.items():
63
+ if type(key) is not str:
64
+ raise EvaluationContractError("JSON object keys must be strings")
65
+ check(key, depth + 1)
66
+ check(child, depth + 1)
67
+ return
68
+ if isinstance(item, (list, tuple)):
69
+ for child in item:
70
+ check(child, depth + 1)
71
+ return
72
+ raise EvaluationContractError("contract JSON permits null, booleans, safe integers, strings, arrays, and objects")
73
+
74
+ check(value, 0)
75
+ text = json.dumps(_thaw(value), ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False)
76
+ if len(text.encode("utf-8")) > _MAX_BYTES:
77
+ raise EvaluationContractError("contract JSON exceeds the byte limit")
78
+ return text
79
+
80
+
81
+ def _thaw(value: Any) -> Any:
82
+ if isinstance(value, Mapping):
83
+ return {key: _thaw(child) for key, child in value.items()}
84
+ if isinstance(value, (tuple, list)):
85
+ return [_thaw(child) for child in value]
86
+ return value
87
+
88
+
89
+ def _freeze(value: Any) -> Any:
90
+ if isinstance(value, Mapping):
91
+ return MappingProxyType({key: _freeze(child) for key, child in value.items()})
92
+ if isinstance(value, (tuple, list)):
93
+ return tuple(_freeze(child) for child in value)
94
+ return value
95
+
96
+
97
+ def _copy(value: Any) -> Any:
98
+ return json.loads(canonical_contract_json(value))
99
+
100
+
101
+ def _digest(value: Any) -> str:
102
+ return "sha256:" + hashlib.sha256(canonical_contract_json(value).encode("utf-8")).hexdigest()
103
+
104
+
105
+ def _identifier(value: str, label: str) -> None:
106
+ if type(value) is not str or not _ID.fullmatch(value):
107
+ raise EvaluationContractError(f"{label} must be a bounded stable identifier")
108
+
109
+
110
+ def _check_pointer(path: str) -> None:
111
+ if (type(path) is not str or not path.startswith("/") or len(path) > 1024
112
+ or re.search(r"~(?![01])", path)):
113
+ raise EvaluationContractError("parameter paths must be bounded RFC 6901 JSON pointers")
114
+
115
+
116
+ def _pointer(value: Mapping[str, Any], path: str) -> Any:
117
+ _check_pointer(path)
118
+ current: Any = value
119
+ for part in path[1:].split("/"):
120
+ key = part.replace("~1", "/").replace("~0", "~")
121
+ if isinstance(current, Mapping) and key in current:
122
+ current = current[key]
123
+ elif isinstance(current, (list, tuple)) and re.fullmatch(r"0|[1-9][0-9]*", key) and int(key) < len(current):
124
+ current = current[int(key)]
125
+ else:
126
+ raise EvaluationContractError(f"parameter path {path!r} does not exist")
127
+ return current
128
+
129
+
130
+ def _schema_check(schema: Mapping[str, Any], depth: int = 0) -> None:
131
+ if not isinstance(schema, Mapping) or depth > 16:
132
+ raise EvaluationContractError("parameter schemas must be bounded objects")
133
+ common = {"type", "title", "description", "default", "enum"}
134
+ if any(key in schema and type(schema[key]) is not str for key in ("title", "description")):
135
+ raise EvaluationContractError("schema title and description must be strings")
136
+ if "oneOf" in schema:
137
+ if set(schema) - {"oneOf", "default", "title", "description"}:
138
+ raise EvaluationContractError("unsupported oneOf schema keyword")
139
+ variants = schema["oneOf"]
140
+ if not isinstance(variants, (tuple, list)) or not 2 <= len(variants) <= 16:
141
+ raise EvaluationContractError("oneOf requires 2 to 16 schemas")
142
+ for variant in variants:
143
+ _schema_check(variant, depth + 1)
144
+ return
145
+ kind = schema.get("type")
146
+ extras = {"object": {"properties", "required", "additionalProperties"},
147
+ "array": {"items", "minItems", "maxItems"},
148
+ "string": {"minLength", "maxLength", "format"},
149
+ "integer": {"minimum", "maximum"}, "boolean": set(), "null": set()}
150
+ if type(kind) is not str or kind not in extras or set(schema) - common - extras[kind]:
151
+ raise EvaluationContractError("unsupported parameter type or schema keyword")
152
+ if "enum" in schema and (not isinstance(schema["enum"], (tuple, list)) or not schema["enum"]):
153
+ raise EvaluationContractError("enum must contain options")
154
+ if "enum" in schema and kind in {"object", "array"}:
155
+ raise EvaluationContractError("enum is supported for primitive parameters only")
156
+ if kind == "object":
157
+ properties = schema.get("properties", {})
158
+ required = schema.get("required", [])
159
+ if (not isinstance(properties, Mapping) or schema.get("additionalProperties") is not False
160
+ or not isinstance(required, (tuple, list))
161
+ or any(type(key) is not str or key not in properties for key in required)
162
+ or len(set(required)) != len(required)):
163
+ raise EvaluationContractError("object schemas require declared properties and additionalProperties=false")
164
+ for child in properties.values():
165
+ _schema_check(child, depth + 1)
166
+ if kind == "array":
167
+ _schema_check(schema.get("items"), depth + 1)
168
+ for lower, upper in (("minimum", "maximum"), ("minLength", "maxLength"), ("minItems", "maxItems")):
169
+ for key in (lower, upper):
170
+ if key in schema and (type(schema[key]) is not int or (key != "minimum" and key != "maximum" and schema[key] < 0)):
171
+ raise EvaluationContractError(f"{key} must be an integer bound")
172
+ if lower in schema and upper in schema and schema[lower] > schema[upper]:
173
+ raise EvaluationContractError("schema lower bound exceeds upper bound")
174
+ if "format" in schema and schema["format"] != "canonical_decimal":
175
+ raise EvaluationContractError("the supported string format is canonical_decimal")
176
+ if "enum" in schema:
177
+ plain = {key: value for key, value in schema.items() if key not in {"enum", "default"}}
178
+ options = schema["enum"]
179
+ normalized = [_normalize(plain, option, "enum") for option in options]
180
+ if len({canonical_contract_json(option) for option in normalized}) != len(normalized):
181
+ raise EvaluationContractError("enum options must be distinct")
182
+
183
+
184
+ def _normalize(schema: Mapping[str, Any], value: Any, path: str) -> Any:
185
+ if "oneOf" in schema:
186
+ matched = []
187
+ for variant in schema["oneOf"]:
188
+ try:
189
+ matched.append(_normalize(variant, value, path))
190
+ except EvaluationContractError:
191
+ pass
192
+ if len(matched) != 1:
193
+ raise EvaluationContractError(f"{path} must match exactly one parameter variant")
194
+ return matched[0]
195
+ kind = schema["type"]
196
+ valid = {"object": isinstance(value, Mapping), "array": isinstance(value, (tuple, list)),
197
+ "string": type(value) is str, "integer": type(value) is int,
198
+ "boolean": type(value) is bool, "null": value is None}[kind]
199
+ if not valid:
200
+ raise EvaluationContractError(f"{path} must be {kind}")
201
+ if kind == "object":
202
+ properties = schema.get("properties", {})
203
+ unknown = set(value) - set(properties)
204
+ if unknown:
205
+ raise EvaluationContractError(f"{path} has unsupported fields: {', '.join(sorted(unknown))}")
206
+ out = {}
207
+ for key, child in properties.items():
208
+ raw = value.get(key, _MISSING)
209
+ if raw is _MISSING:
210
+ raw = child.get("default", _MISSING)
211
+ if raw is _MISSING:
212
+ if key in schema.get("required", ()):
213
+ raise EvaluationContractError(f"{path}.{key} is required")
214
+ continue
215
+ out[key] = _normalize(child, raw, f"{path}.{key}")
216
+ value = out
217
+ elif kind == "array":
218
+ if not schema.get("minItems", 0) <= len(value) <= schema.get("maxItems", 256):
219
+ raise EvaluationContractError(f"{path} has an unsupported item count")
220
+ value = [_normalize(schema["items"], item, f"{path}[{index}]") for index, item in enumerate(value)]
221
+ elif kind == "integer":
222
+ if not schema.get("minimum", -_MAX_INTEGER) <= value <= schema.get("maximum", _MAX_INTEGER):
223
+ raise EvaluationContractError(f"{path} is outside the supported range")
224
+ elif kind == "string":
225
+ if not schema.get("minLength", 0) <= len(value) <= schema.get("maxLength", 16384):
226
+ raise EvaluationContractError(f"{path} has an unsupported length")
227
+ if schema.get("format") == "canonical_decimal" and (_DECIMAL.fullmatch(value) is None or value == "-0"):
228
+ raise EvaluationContractError(f"{path} has an unsupported string format")
229
+ if "enum" in schema and canonical_contract_json(value) not in {canonical_contract_json(item) for item in schema["enum"]}:
230
+ raise EvaluationContractError(f"{path} is not a supported option")
231
+ return value
232
+
233
+
234
+ @dataclass(frozen=True)
235
+ class ArtifactReference:
236
+ """Registered metadata only; contains no URI, bytes, labels, or credentials."""
237
+ ref: str
238
+ kind: str
239
+ version: str
240
+ digest: str
241
+ public: bool = False
242
+
243
+ def __post_init__(self):
244
+ for key in ("ref", "kind", "version"):
245
+ _identifier(getattr(self, key), key)
246
+ if type(self.digest) is not str or not _DIGEST.fullmatch(self.digest) or type(self.public) is not bool:
247
+ raise EvaluationContractError("artifact requires SHA-256 identity and explicit visibility")
248
+
249
+ def to_dict(self) -> dict[str, Any]:
250
+ return {"ref": self.ref, "kind": self.kind, "version": self.version, "digest": self.digest,
251
+ "metadata_only": True, "bytes_verified": False}
252
+
253
+
254
+ @dataclass(frozen=True)
255
+ class ArtifactBinding:
256
+ parameter_path: str
257
+ kind: str
258
+ comparison_kinds: tuple[str, ...] = ("quality", "latency")
259
+
260
+ def __post_init__(self):
261
+ _check_pointer(self.parameter_path)
262
+ _identifier(self.kind, "artifact kind")
263
+ if (not isinstance(self.comparison_kinds, tuple) or any(type(k) is not str or k not in {"quality", "latency"} for k in self.comparison_kinds)
264
+ or len(set(self.comparison_kinds)) != len(self.comparison_kinds)):
265
+ raise EvaluationContractError("artifact comparison kinds must be unique quality/latency entries")
266
+
267
+
268
+ @dataclass(frozen=True)
269
+ class EvaluationDefinition:
270
+ evaluation_id: str
271
+ version: str
272
+ title: str
273
+ status: str
274
+ interface_version: str
275
+ implementation: Mapping[str, Any]
276
+ parameter_schema: Mapping[str, Any]
277
+ quality_parameters: tuple[str, ...]
278
+ latency_parameters: tuple[str, ...]
279
+ artifact_bindings: tuple[ArtifactBinding, ...] = ()
280
+ metric_ids: tuple[str, ...] = ()
281
+
282
+ def __post_init__(self):
283
+ for key in ("evaluation_id", "version", "interface_version"):
284
+ _identifier(getattr(self, key), key)
285
+ if type(self.status) is not str or self.status not in {"runnable", "contract_only"} or type(self.title) is not str or not self.title:
286
+ raise EvaluationContractError("definition requires a title and supported status")
287
+ if (not isinstance(self.implementation, Mapping) or not self.implementation
288
+ or any(type(value) is not str for value in self.implementation.values())):
289
+ raise EvaluationContractError("implementation must declare stable implementation revision strings")
290
+ for key, revision in self.implementation.items():
291
+ _identifier(key, "implementation component")
292
+ _identifier(revision, "implementation revision")
293
+ schema = _copy(self.parameter_schema)
294
+ _schema_check(schema)
295
+ if schema.get("type") != "object":
296
+ raise EvaluationContractError("evaluation parameter root must be an object")
297
+ if "default" in schema:
298
+ raise EvaluationContractError("evaluation defaults belong to parameters, not the root object")
299
+ for paths in (self.quality_parameters, self.latency_parameters):
300
+ if not isinstance(paths, tuple) or any(type(p) is not str for p in paths) or len(set(paths)) != len(paths):
301
+ raise EvaluationContractError("comparison parameter paths must be unique JSON pointers")
302
+ for path in paths:
303
+ _check_pointer(path)
304
+ if not isinstance(self.artifact_bindings, tuple) or any(not isinstance(b, ArtifactBinding) for b in self.artifact_bindings):
305
+ raise EvaluationContractError("artifact bindings must be a tuple of ArtifactBinding")
306
+ if len({b.parameter_path for b in self.artifact_bindings}) != len(self.artifact_bindings):
307
+ raise EvaluationContractError("artifact binding paths must be unique")
308
+ if not isinstance(self.metric_ids, tuple) or any(type(metric) is not str for metric in self.metric_ids) or len(set(self.metric_ids)) != len(self.metric_ids):
309
+ raise EvaluationContractError("metric IDs must be a unique tuple")
310
+ for metric in self.metric_ids:
311
+ _identifier(metric, "metric ID")
312
+ object.__setattr__(self, "parameter_schema", _freeze(schema))
313
+ object.__setattr__(self, "implementation", _freeze(_copy(self.implementation)))
314
+ # Validate all declared defaults, including inactive tagged variants.
315
+ def defaults(node):
316
+ if "default" in node:
317
+ _normalize(node, node["default"], "default")
318
+ for child in node.get("properties", {}).values():
319
+ defaults(child)
320
+ for child in node.get("oneOf", ()):
321
+ defaults(child)
322
+ if "items" in node:
323
+ defaults(node["items"])
324
+ defaults(schema)
325
+ canonical_contract_json(self.to_dict())
326
+
327
+ @property
328
+ def runnable(self) -> bool:
329
+ return self.status == "runnable"
330
+
331
+ @property
332
+ def definition_digest(self) -> str:
333
+ return _digest(self.to_dict())
334
+
335
+ def to_dict(self) -> dict[str, Any]:
336
+ return {"schema_version": DEFINITION_SCHEMA, "canonicalization": CANONICALIZATION,
337
+ "evaluation_id": self.evaluation_id, "version": self.version, "title": self.title,
338
+ "status": self.status, "runnable": self.runnable, "interface_version": self.interface_version,
339
+ "implementation": _thaw(self.implementation), "parameter_schema": _thaw(self.parameter_schema),
340
+ "comparison": {"quality_parameters": list(self.quality_parameters), "latency_parameters": list(self.latency_parameters),
341
+ "candidate_identity_included": False, "reporting_parameters_included": False},
342
+ "artifact_bindings": [{"parameter_path": b.parameter_path, "kind": b.kind,
343
+ "comparison_kinds": list(b.comparison_kinds)} for b in self.artifact_bindings],
344
+ "metric_ids": list(self.metric_ids)}
345
+
346
+
347
+ @dataclass(frozen=True)
348
+ class ResolvedInvocation:
349
+ """Immutable resolved test metadata; execution identities are optional bindings."""
350
+ definition: EvaluationDefinition
351
+ parameters: Mapping[str, Any]
352
+ artifacts: tuple[Mapping[str, Any], ...]
353
+ candidate_identity: Mapping[str, Any] | None = None
354
+ runtime_identity: Mapping[str, Any] | None = None
355
+ _registry_token: InitVar[object] = None
356
+
357
+ def __post_init__(self, _registry_token):
358
+ if _registry_token is not _RESOLUTION_TOKEN:
359
+ raise EvaluationContractError("resolved invocations must be constructed through registry.resolve()")
360
+ object.__setattr__(self, "parameters", _freeze(_copy(self.parameters)))
361
+ object.__setattr__(self, "artifacts", tuple(_freeze(_copy(item)) for item in self.artifacts))
362
+ for key in ("candidate_identity", "runtime_identity"):
363
+ value = getattr(self, key)
364
+ if value is not None:
365
+ if not isinstance(value, Mapping):
366
+ raise EvaluationContractError(f"{key} must be an object")
367
+ object.__setattr__(self, key, _freeze(_copy(value)))
368
+
369
+ @property
370
+ def invocation_digest(self) -> str:
371
+ return _digest(self.to_dict())
372
+
373
+ def to_dict(self) -> dict[str, Any]:
374
+ return {"schema_version": INVOCATION_SCHEMA, "canonicalization": CANONICALIZATION,
375
+ "definition": {"evaluation_id": self.definition.evaluation_id, "version": self.definition.version,
376
+ "digest": self.definition.definition_digest, "implementation": _thaw(self.definition.implementation)},
377
+ "status": "contract_resolved", "runnable": self.definition.runnable,
378
+ "parameters": _thaw(self.parameters), "artifacts": _thaw(self.artifacts),
379
+ "candidate_identity": _thaw(self.candidate_identity), "runtime_identity": _thaw(self.runtime_identity),
380
+ "runtime_identity_status": "bound" if self.runtime_identity is not None else "unbound",
381
+ "artifact_bytes_verified": False}
382
+
383
+ def comparison_key(self, kind: str = "quality") -> str:
384
+ if kind not in {"quality", "latency"}:
385
+ raise EvaluationContractError("comparison kind must be quality or latency")
386
+ paths = self.definition.quality_parameters
387
+ if kind == "latency":
388
+ paths = tuple(dict.fromkeys((*paths, *self.definition.latency_parameters)))
389
+ return _digest({"schema_version": "sphynx.eval.comparison-cohort.v1", "kind": kind,
390
+ "definition_digest": self.definition.definition_digest,
391
+ "parameters": {path: _thaw(_pointer(self.parameters, path)) for path in paths},
392
+ "artifacts": [_thaw(item) for item in self.artifacts if kind in item["comparison_kinds"]],
393
+ "runtime_identity": _thaw(self.runtime_identity) if kind == "latency" else None,
394
+ "runtime_identity_status": ("bound" if self.runtime_identity is not None else "unbound") if kind == "latency" else "not_used"})
395
+
396
+
397
+ @dataclass(frozen=True)
398
+ class EvaluationRegistry:
399
+ """Operator-owned registry. Grants must come from trusted authorization code."""
400
+ definitions: tuple[EvaluationDefinition, ...]
401
+ artifacts: tuple[ArtifactReference, ...] = ()
402
+
403
+ def __post_init__(self):
404
+ for items, cls, key in ((self.definitions, EvaluationDefinition, "evaluation_id"), (self.artifacts, ArtifactReference, "ref")):
405
+ if not isinstance(items, tuple) or any(not isinstance(item, cls) for item in items):
406
+ raise EvaluationContractError("registry entries must be immutable typed tuples")
407
+ if len({getattr(item, key) for item in items}) != len(items):
408
+ raise EvaluationContractError("registry identities must be unique")
409
+
410
+ def get(self, evaluation_id: str) -> EvaluationDefinition:
411
+ match = next((d for d in self.definitions if d.evaluation_id == evaluation_id), None)
412
+ if match is None:
413
+ raise EvaluationContractError("evaluation definition is not registered")
414
+ return match
415
+
416
+ def resolve(self, evaluation_id: str, parameters: Mapping[str, Any] | None = None, *,
417
+ granted_artifacts: Collection[str] = (), candidate_identity: Mapping[str, Any] | None = None,
418
+ runtime_identity: Mapping[str, Any] | None = None) -> ResolvedInvocation:
419
+ definition = self.get(evaluation_id)
420
+ if (isinstance(granted_artifacts, (str, bytes)) or not isinstance(granted_artifacts, Collection)
421
+ or any(type(ref) is not str for ref in granted_artifacts)):
422
+ raise EvaluationContractError("artifact grants must be an explicit collection of reference strings")
423
+ if parameters is not None and not isinstance(parameters, Mapping):
424
+ raise EvaluationContractError("parameters must be an object")
425
+ values = _normalize(definition.parameter_schema, _copy({} if parameters is None else parameters), "parameters")
426
+ records = []
427
+ for binding in sorted(definition.artifact_bindings, key=lambda item: item.parameter_path):
428
+ ref = _pointer(values, binding.parameter_path)
429
+ artifact = next((a for a in self.artifacts if a.ref == ref), None)
430
+ if artifact is None or not (artifact.public or artifact.ref in granted_artifacts):
431
+ raise EvaluationContractError("requested artifact is unavailable or not authorized")
432
+ if artifact.kind != binding.kind:
433
+ raise EvaluationContractError("registered artifact kind differs from parameter binding")
434
+ records.append({"parameter_path": binding.parameter_path, "comparison_kinds": list(binding.comparison_kinds), **artifact.to_dict()})
435
+ # All comparison paths must be present in the fully expanded invocation.
436
+ for path in (*definition.quality_parameters, *definition.latency_parameters):
437
+ _pointer(values, path)
438
+ return ResolvedInvocation(definition, values, tuple(records), candidate_identity, runtime_identity, _registry_token=_RESOLUTION_TOKEN)
439
+
440
+
441
+ def _object(properties, *, required=(), default=_MISSING):
442
+ schema = {"type": "object", "properties": properties, "required": list(required), "additionalProperties": False}
443
+ if default is not _MISSING:
444
+ schema["default"] = default
445
+ return schema
446
+
447
+
448
+ def _integer(default, minimum, maximum):
449
+ return {"type": "integer", "default": default, "minimum": minimum, "maximum": maximum}
450
+
451
+
452
+ def _reporting():
453
+ return _object({"format": {"type": "string", "enum": ["markdown", "json"], "default": "markdown"}}, default={})
454
+
455
+
456
+ def default_evaluation_registry() -> EvaluationRegistry:
457
+ # Lazy imports keep this contract layer independent of runner/candidate code.
458
+ from .metrics import GRADER_RELEASE, METRIC_RELEASE, metric_definitions
459
+ from .rag import FIXTURE_SHA256
460
+ bank = "help-center-evidence-mini@2.0.0"
461
+ legacy = _object({
462
+ "question_bank": {"type": "string", "enum": [bank], "default": bank},
463
+ "split": {"type": "string", "enum": ["all"], "default": "all"},
464
+ "top_k": {"type": "integer", "enum": [5], "default": 5},
465
+ "relevance_threshold": {"type": "integer", "enum": [2], "default": 2},
466
+ "runtime": _object({"trials": {"type": "integer", "enum": [1], "default": 1},
467
+ "per_case_timeout_seconds": {"type": "integer", "enum": [30], "default": 30}}, default={}),
468
+ "reporting": _reporting(),
469
+ })
470
+ configurable = _copy(legacy)
471
+ props = configurable["properties"]
472
+ props["question_bank"] = {"type": "string", "minLength": 1, "maxLength": 256, "default": bank}
473
+ props["split"] = {"type": "string", "enum": ["all", "dev", "test", "holdout"], "default": "all"}
474
+ props["top_k"] = _integer(5, 1, 100)
475
+ props["relevance_threshold"] = _integer(2, 0, 3)
476
+ props["runtime"] = _object({"trials": _integer(1, 1, 10), "per_case_timeout_seconds": _integer(30, 1, 300)}, default={})
477
+ quality = ("/question_bank", "/split", "/top_k", "/relevance_threshold", "/runtime")
478
+ bindings = (ArtifactBinding("/question_bank", "question_bank"),)
479
+ definitions = (
480
+ EvaluationDefinition("rag.rerank.v1", "v1", "Six-case illustrative reranking", "runnable",
481
+ "sphynx.rag.rerank.v1", {"adapter": "legacy-rag-rerank.v1", "grader": GRADER_RELEASE, "metrics": METRIC_RELEASE},
482
+ legacy, quality, ("/runtime",), bindings, tuple(d.metric_id for d in metric_definitions())),
483
+ EvaluationDefinition("rag.rerank.v2", "v2", "Parameterized reranking contract", "contract_only",
484
+ "sphynx.rag.rerank.v1", {"adapter": "not_implemented", "grader": "parameterized-rerank.pending"},
485
+ configurable, quality, ("/runtime",), bindings),
486
+ EvaluationDefinition("audio.similarity.v1", "v1", "Fourier or wavelet audio comparison contract", "contract_only",
487
+ "sphynx.audio.similarity.v1", {"adapter": "not_implemented", "grader": "audio-similarity.pending"},
488
+ _object({"audio_bank": {"type": "string", "minLength": 1, "maxLength": 256},
489
+ "comparison": {"oneOf": [
490
+ _object({"method": {"type": "string", "enum": ["fourier"]},
491
+ "fft_size": _integer(1024, 16, 65536),
492
+ "window": {"type": "string", "enum": ["hann", "rectangular"], "default": "hann"}}, required=("method",)),
493
+ _object({"method": {"type": "string", "enum": ["wavelet"]},
494
+ "family": {"type": "string", "enum": ["haar", "db2"], "default": "haar"},
495
+ "levels": _integer(4, 1, 12)}, required=("method",))],
496
+ "default": {"method": "fourier"}},
497
+ "runtime": _object({"trials": _integer(1, 1, 10), "per_case_timeout_seconds": _integer(30, 1, 300)}, default={}),
498
+ "reporting": _reporting()}, required=("audio_bank",)),
499
+ ("/audio_bank", "/comparison", "/runtime"), ("/runtime",), (ArtifactBinding("/audio_bank", "audio_bank"),)),
500
+ )
501
+ from .public_eval import public_definition, public_artifacts
502
+ return EvaluationRegistry((*definitions, public_definition()),
503
+ (ArtifactReference(bank, "question_bank", "2.0.0", "sha256:" + FIXTURE_SHA256, public=True), *public_artifacts()))
504
+
505
+
506
+ def list_evals(*, registry: EvaluationRegistry | None = None) -> tuple[EvaluationDefinition, ...]:
507
+ """Discover installed definitions; does not assert hosted availability."""
508
+ return (registry or default_evaluation_registry()).definitions
509
+
510
+
511
+ def describe_eval(evaluation_id: str, *, registry: EvaluationRegistry | None = None) -> dict[str, Any]:
512
+ definition = (registry or default_evaluation_registry()).get(evaluation_id)
513
+ return {**definition.to_dict(), "definition_digest": definition.definition_digest}
514
+
515
+
516
+ def resolve_eval(evaluation_id: str, parameters: Mapping[str, Any] | None = None, *,
517
+ registry: EvaluationRegistry | None = None, granted_artifacts: Collection[str] = (),
518
+ candidate_identity: Mapping[str, Any] | None = None,
519
+ runtime_identity: Mapping[str, Any] | None = None) -> ResolvedInvocation:
520
+ return (registry or default_evaluation_registry()).resolve(
521
+ evaluation_id, parameters, granted_artifacts=granted_artifacts,
522
+ candidate_identity=candidate_identity, runtime_identity=runtime_identity)
523
+
524
+
525
+ def require_legacy_rerank(evaluation_id: str, parameters: Mapping[str, Any] | None = None) -> ResolvedInvocation:
526
+ """Fail before process/filesystem work unless the existing adapter supports it."""
527
+ definition = default_evaluation_registry().get(evaluation_id)
528
+ if not definition.runnable or definition.evaluation_id != "rag.rerank.v1":
529
+ raise EvaluationContractError("evaluation has a contract only; its execution adapter is not implemented")
530
+ resolved = resolve_eval(evaluation_id, parameters)
531
+ # Keep the legacy record and comparison hashes intact. Admission uses
532
+ # separate, operator-owned benchmark and label metadata.
533
+ from .benchmarks import BenchmarkContractError, default_registry
534
+ try:
535
+ default_registry().validate_legacy(resolved.to_dict())
536
+ except BenchmarkContractError as exc:
537
+ raise EvaluationContractError(str(exc)) from None
538
+ return resolved
@@ -0,0 +1,124 @@
1
+ """Optional domain-neutral protocol identities, never grants or attestations."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass, field, fields
6
+ from types import MappingProxyType
7
+ from typing import Any
8
+ import re
9
+
10
+ from .workflow_contracts import (
11
+ WorkflowContractError, _object, _optional_text, _text, _texts, _thaw,
12
+ contract_digest,
13
+ )
14
+
15
+ IDENTITY_SCHEMA = "sphynx.versioned-identity.v1"
16
+ PROTOCOL_SCHEMA = "sphynx.evaluation-protocol.v1"
17
+ _DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
18
+
19
+
20
+ def _envelope(value: Mapping[str, Any], schema: str, cls: type) -> dict[str, Any]:
21
+ if not isinstance(value, Mapping):
22
+ raise WorkflowContractError("versioned envelope must be a mapping")
23
+ result = dict(value)
24
+ if result.pop("schema_version", None) != schema:
25
+ raise WorkflowContractError("unsupported " + schema + " envelope version")
26
+ if set(result) - {item.name for item in fields(cls)}:
27
+ raise WorkflowContractError("unknown " + schema + " envelope fields")
28
+ return result
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class VersionedIdentity:
33
+ """A declared identity. A digest does not authenticate the referenced source."""
34
+
35
+ identity_id: str
36
+ version: str
37
+ digest: str | None = None
38
+
39
+ def __post_init__(self) -> None:
40
+ _text(self.identity_id, "identity_id")
41
+ _text(self.version, "version")
42
+ if self.digest is not None and (
43
+ type(self.digest) is not str or not _DIGEST.fullmatch(self.digest)
44
+ ):
45
+ raise WorkflowContractError("identity digest must be canonical sha256 text")
46
+
47
+ def to_dict(self) -> dict[str, Any]:
48
+ return {"schema_version": IDENTITY_SCHEMA, "identity_id": self.identity_id,
49
+ "version": self.version, "digest": self.digest}
50
+
51
+ @classmethod
52
+ def from_dict(cls, value: Mapping[str, Any]) -> VersionedIdentity:
53
+ try:
54
+ return cls(**_envelope(value, IDENTITY_SCHEMA, cls))
55
+ except TypeError as exc:
56
+ raise WorkflowContractError("invalid versioned identity fields") from exc
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class EvaluationProtocol:
61
+ """Frozen conditions for an opt-in driver, with an open evaluation unit.
62
+
63
+ Population and grouping semantics belong to the protocol author. Bindings
64
+ identify source/bank/labels/rubric/evaluator/runtime as needed by that driver.
65
+ They contain permitted identifiers only, never protected questions or grants.
66
+ PayloadSchema and Dataset still validate and authorize the actual payloads.
67
+ """
68
+
69
+ protocol_id: str
70
+ version: str
71
+ implementation_id: str
72
+ unit: str
73
+ input_schema_id: str
74
+ output_schema_id: str
75
+ population: Mapping[str, Any]
76
+ bindings: Mapping[str, VersionedIdentity] = field(default_factory=dict)
77
+ grouping_keys: tuple[str, ...] = ()
78
+ reset_policy_id: str | None = None
79
+
80
+ def __post_init__(self) -> None:
81
+ for name in ("protocol_id", "version", "implementation_id", "unit",
82
+ "input_schema_id", "output_schema_id"):
83
+ _text(getattr(self, name), name)
84
+ population = _object(self.population, "protocol population")
85
+ if not population:
86
+ raise WorkflowContractError("protocol requires an explicit population rule")
87
+ object.__setattr__(self, "population", population)
88
+ if not isinstance(self.bindings, Mapping):
89
+ raise WorkflowContractError("protocol bindings must be a mapping")
90
+ bindings = {}
91
+ for role, identity in self.bindings.items():
92
+ _text(role, "identity role")
93
+ if not isinstance(identity, VersionedIdentity):
94
+ raise WorkflowContractError("protocol bindings require VersionedIdentity values")
95
+ bindings[role] = identity
96
+ object.__setattr__(self, "bindings", MappingProxyType(bindings))
97
+ object.__setattr__(self, "grouping_keys", _texts(self.grouping_keys, "grouping_keys"))
98
+ _optional_text(self.reset_policy_id, "reset_policy_id")
99
+
100
+ @property
101
+ def protocol_digest(self) -> str:
102
+ return contract_digest(self.to_dict())
103
+
104
+ def to_dict(self) -> dict[str, Any]:
105
+ return {"schema_version": PROTOCOL_SCHEMA, "protocol_id": self.protocol_id,
106
+ "version": self.version, "implementation_id": self.implementation_id,
107
+ "unit": self.unit, "input_schema_id": self.input_schema_id,
108
+ "output_schema_id": self.output_schema_id,
109
+ "population": _thaw(self.population),
110
+ "bindings": {role: identity.to_dict() for role, identity in self.bindings.items()},
111
+ "grouping_keys": list(self.grouping_keys), "reset_policy_id": self.reset_policy_id}
112
+
113
+ @classmethod
114
+ def from_dict(cls, value: Mapping[str, Any]) -> EvaluationProtocol:
115
+ values = _envelope(value, PROTOCOL_SCHEMA, cls)
116
+ bindings = values.get("bindings", {})
117
+ if not isinstance(bindings, Mapping):
118
+ raise WorkflowContractError("protocol bindings must be a mapping")
119
+ values["bindings"] = {role: VersionedIdentity.from_dict(identity)
120
+ for role, identity in bindings.items()}
121
+ try:
122
+ return cls(**values)
123
+ except TypeError as exc:
124
+ raise WorkflowContractError("invalid evaluation protocol fields") from exc