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,352 @@
1
+ """Replay a hash-bound snapshot of local reranker outputs.
2
+
3
+ This adapter measures replay runtime, not model inference latency. Model timing
4
+ belongs to the owner-side score snapshot retained as separate evidence.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import json
10
+ import math
11
+ from pathlib import Path
12
+ from typing import Any, Mapping
13
+
14
+ from .contracts import ContractError, canonical_json
15
+
16
+ RECORDED_SCORE_SCHEMA = "sphynx.recorded.rerank.scores.v1"
17
+
18
+
19
+ def replay_rerank_scores(request: Mapping[str, Any], score_path: str | Path,
20
+ expected_sha256: str) -> dict[str, Any]:
21
+ """Return recorded scores only for an exactly matching label-free request."""
22
+ payload = Path(score_path).read_bytes()
23
+ if hashlib.sha256(payload).hexdigest() != expected_sha256.removeprefix("sha256:"):
24
+ raise ContractError("recorded model score snapshot hash differs")
25
+ try:
26
+ artifact = json.loads(payload)
27
+ except (ValueError, UnicodeError) as exc:
28
+ raise ContractError("invalid recorded model score snapshot") from exc
29
+ if not isinstance(artifact, dict) or artifact.get("schema_version") != RECORDED_SCORE_SCHEMA:
30
+ raise ContractError("unsupported recorded model score snapshot")
31
+ if request.get("schema_version") != "sphynx.rag.rerank.request.v1":
32
+ raise ContractError("unsupported recorded-score request")
33
+ rows = artifact.get("cases")
34
+ if not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows):
35
+ raise ContractError("recorded model cases are invalid")
36
+ identifiers = [row.get("case_id") for row in rows]
37
+ if len(set(identifiers)) != len(identifiers):
38
+ raise ContractError("recorded model cases contain duplicate IDs")
39
+ matches = [row for row in rows if row.get("case_id") == request.get("case_id")]
40
+ if len(matches) != 1:
41
+ raise ContractError("recorded model case not found")
42
+ row = matches[0]
43
+ request_digest = "sha256:" + hashlib.sha256(canonical_json(dict(request)).encode("utf-8")).hexdigest()
44
+ if row.get("request_sha256") != request_digest:
45
+ raise ContractError("recorded model request identity differs")
46
+ scores = row.get("scores")
47
+ if not isinstance(scores, list) or len(scores) != len(request.get("candidates", [])):
48
+ raise ContractError("recorded model score coverage differs")
49
+ expected_ids = [candidate["candidate_id"] for candidate in request["candidates"]]
50
+ if [item.get("candidate_id") for item in scores] != expected_ids:
51
+ raise ContractError("recorded model score order or IDs differ")
52
+ for item in scores:
53
+ value = item.get("ranking_score")
54
+ if set(item) != {"candidate_id", "ranking_score"} or isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
55
+ raise ContractError("recorded model scores must be finite numbers")
56
+ return {
57
+ "schema_version": "sphynx.rag.rerank.response.v1",
58
+ "query_id": request["query_id"], "pool_digest": request["pool_digest"],
59
+ "scores": scores,
60
+ }
61
+ # The strict additive interface retains the historical v1 replay API unchanged.
62
+ BOUND_RECORDED_SCORE_SCHEMA = "sphynx.recorded.rerank.scores.v2"
63
+
64
+
65
+ def _strict_json(payload: bytes) -> dict[str, Any]:
66
+ def unique(pairs):
67
+ result = {}
68
+ for key, value in pairs:
69
+ if key in result:
70
+ raise ContractError("duplicate recorded JSON field")
71
+ result[key] = value
72
+ return result
73
+ def invalid(value):
74
+ raise ContractError("nonfinite recorded JSON number")
75
+ try:
76
+ value = json.loads(payload, object_pairs_hook=unique, parse_constant=invalid)
77
+ except ContractError:
78
+ raise
79
+ except (ValueError, UnicodeError) as exc:
80
+ raise ContractError("invalid recorded score JSON") from exc
81
+ if not isinstance(value, dict):
82
+ raise ContractError("recorded artifact must be an object")
83
+ return value
84
+
85
+
86
+ def _read_bound(path, digest):
87
+ from .integration_contracts import require_digest
88
+ require_digest(digest, "recording_sha256")
89
+ payload = Path(path).read_bytes()
90
+ if "sha256:" + hashlib.sha256(payload).hexdigest() != digest:
91
+ raise ContractError("recorded snapshot hash differs")
92
+ return _strict_json(payload)
93
+
94
+
95
+ def _recorded_rows(requests):
96
+ from .model_adapters import request_pairs
97
+ result = {}
98
+ for request in requests:
99
+ # Detach the entire JSON request, including candidate lists and metadata.
100
+ request = json.loads(canonical_json(dict(request)))
101
+ request_pairs(request)
102
+ case_id = request["case_id"]
103
+ if case_id in result:
104
+ raise ContractError("duplicate requested recorded case IDs")
105
+ result[case_id] = request
106
+ if not result:
107
+ raise ContractError("recorded cohort must be nonempty")
108
+ return result
109
+
110
+
111
+ def _validated_scores(scores, request):
112
+ if not isinstance(scores, list) or len(scores) != len(request["candidates"]):
113
+ raise ContractError("recorded score coverage differs")
114
+ expected = [item["candidate_id"] for item in request["candidates"]]
115
+ if any(not isinstance(item, dict) or set(item) != {"candidate_id", "ranking_score"} for item in scores):
116
+ raise ContractError("invalid recorded score fields")
117
+ if [item["candidate_id"] for item in scores] != expected:
118
+ raise ContractError("recorded candidate order differs")
119
+ for item in scores:
120
+ value = item["ranking_score"]
121
+ if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
122
+ raise ContractError("recorded scores must be finite scalar utilities")
123
+ return [dict(item) for item in scores]
124
+
125
+
126
+ class DatasetBoundRecordedScorer:
127
+ """Strict complete-cohort replay; no model/provider is imported or executed.
128
+
129
+ Construction pins exact score bytes and every ordered label-free request.
130
+ Each call rechecks bytes against that pin, including after construction.
131
+ Original inference timing remains provenance; replay timing is lookup only.
132
+ """
133
+ def __init__(self, score_path, expected_sha256, *, identity, requests, expected_model):
134
+ from .integration_contracts import ComparisonIdentity, value_digest, CandidateConfiguration
135
+ if not isinstance(identity, ComparisonIdentity):
136
+ raise ContractError("recorded scorer requires ComparisonIdentity")
137
+ if identity.track != "rerank":
138
+ raise ContractError("recorded scorer requires the rerank track")
139
+ self.path = Path(score_path)
140
+ self.digest = expected_sha256
141
+ self.identity = identity
142
+ self._requests = _recorded_rows(requests)
143
+ self._request_digests = {case_id: value_digest(request) for case_id, request in self._requests.items()}
144
+ self._expected_model = dict(expected_model)
145
+ artifact = self._load()
146
+ self.configuration = CandidateConfiguration("recorded-" + artifact["model"]["id"] + "-v2", "sphynx.recorded.rerank.v2",
147
+ "sha256:" + hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
148
+ {"recording_sha256": expected_sha256, "identity": identity.to_dict(), "model": artifact["model"], "provenance": artifact["provenance"], "request_digests": list(self._request_digests.values()), "timing_scope": "recorded_lookup_only_not_model_inference"}, "recorded")
149
+
150
+ def _load(self):
151
+ from .integration_contracts import value_digest
152
+ artifact = _read_bound(self.path, self.digest)
153
+ if set(artifact) != {"schema_version", "identity", "model", "provenance", "cases"} or artifact["schema_version"] != BOUND_RECORDED_SCORE_SCHEMA:
154
+ raise ContractError("unsupported strict recorded score schema")
155
+ if artifact["identity"] != self.identity.to_dict():
156
+ raise ContractError("recorded dataset/cohort/reference/policy identity differs")
157
+ if artifact["model"] != self._expected_model:
158
+ raise ContractError("recorded model/checkpoint/tokenizer/truncation identity differs")
159
+ _validate_model_identity(artifact["model"])
160
+ provenance = artifact["provenance"]
161
+ fields = {"source_recording_sha256", "source_pool_sha256", "source_schema", "runtime", "timing_scope"}
162
+ mapped = isinstance(provenance, dict) and "request_order_mapping" in provenance
163
+ if mapped:
164
+ fields.add("request_order_mapping")
165
+ if not isinstance(provenance, dict) or set(provenance) != fields:
166
+ raise ContractError("recorded provenance fields differ")
167
+ if mapped:
168
+ from .public_rerank import PUBLIC_POOL_RELEASES
169
+ if (provenance["request_order_mapping"] != "verified_source_vector_to_first_stage_rank_by_exact_candidate_id"
170
+ or provenance["source_schema"] != "expanded-reranker.model-scores.v2"
171
+ or provenance["source_pool_sha256"] not in {"sha256:" + release.source_sha256 for release in PUBLIC_POOL_RELEASES}
172
+ or self.identity.dataset_digest != provenance["source_pool_sha256"]):
173
+ raise ContractError("invalid recorded release-specific ordering provenance")
174
+ from .integration_contracts import require_digest
175
+ require_digest(provenance["source_recording_sha256"], "source_recording_sha256")
176
+ if provenance["source_pool_sha256"] is not None:
177
+ require_digest(provenance["source_pool_sha256"], "source_pool_sha256")
178
+ if not isinstance(provenance["source_schema"], str) or not isinstance(provenance["timing_scope"], str) or not isinstance(provenance["runtime"], dict):
179
+ raise ContractError("invalid recording timing/runtime provenance")
180
+ rows = artifact["cases"]
181
+ if not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows):
182
+ raise ContractError("invalid recorded cohort rows")
183
+ if [row.get("case_id") for row in rows] != list(self._requests):
184
+ raise ContractError("recorded cohort coverage or order differs")
185
+ for row, request in zip(rows, self._requests.values()):
186
+ if set(row) != {"case_id", "request_sha256", "scores", "model_inference_ms"} or row["request_sha256"] != value_digest(request):
187
+ raise ContractError("recorded request identity differs")
188
+ _validated_scores(row["scores"], request)
189
+ latency = row["model_inference_ms"]
190
+ if latency is not None and (isinstance(latency, bool) or not isinstance(latency, (int, float)) or not math.isfinite(latency) or latency < 0):
191
+ raise ContractError("invalid recorded inference latency")
192
+ return artifact
193
+
194
+ def __call__(self, request):
195
+ from .integration_contracts import value_digest
196
+ case_id = request.get("case_id")
197
+ if case_id not in self._requests or value_digest(request) != self._request_digests[case_id]:
198
+ raise ContractError("recorded request identity differs")
199
+ # Exact bytes were fully validated during construction. Rechecking the
200
+ # pinned bytes is sufficient; repeating cohort validation adds no proof.
201
+ artifact = _read_bound(self.path, self.digest)
202
+ row = next(row for row in artifact["cases"] if row["case_id"] == case_id)
203
+ return {"schema_version": "sphynx.rag.rerank.response.v1", "query_id": request["query_id"], "pool_digest": request["pool_digest"], "scores": _validated_scores(row["scores"], request)}
204
+
205
+
206
+ def _validate_model_identity(model):
207
+ from .model_adapters import get_model_spec
208
+ from .integration_contracts import require_digest
209
+ fields = {"id", "checkpoint", "revision", "max_length", "batch_size", "tokenizer_revision", "truncation", "activation", "snapshot_digest", "onnx_sha256"}
210
+ if not isinstance(model, dict) or set(model) != fields:
211
+ raise ContractError("recorded model identity fields differ")
212
+ spec = get_model_spec(model["id"])
213
+ for name in ("checkpoint", "revision", "max_length", "batch_size"):
214
+ if model[name] != getattr(spec, name) or (name in ("max_length", "batch_size") and type(model[name]) is not int):
215
+ raise ContractError("recorded model differs from frozen parameters")
216
+ if model["tokenizer_revision"] != spec.revision or model["truncation"] != "longest_first" or model["activation"] not in ("identity", "legacy_cross_encoder_default"):
217
+ raise ContractError("recorded tokenizer/truncation/activation identity differs")
218
+ for name in ("snapshot_digest", "onnx_sha256"):
219
+ if model[name] is not None:
220
+ require_digest(model[name], name)
221
+ if spec.backend == "onnx_int8" and model["onnx_sha256"] is None:
222
+ raise ContractError("recorded INT8 graph digest required")
223
+
224
+
225
+ def import_recorded_scores(score_path, expected_sha256, *, identity, requests, expected_model, source_pool_path=None, source_pool_sha256=None, source_task_ids=None, source_request_order="source"):
226
+ """Return a strict v2 artifact from an exact owner-held completed recording.
227
+
228
+ Supports the existing SDK v1 and expanded-reranker.model-scores.v2. Expanded
229
+ inputs require exact original pool bytes and explicit task-ID mapping. No
230
+ join on text or guessed IDs is performed; every query and ordered passage is
231
+ checked. Caller saves the result outside source control and pins its bytes.
232
+ ``snapshot_digest=None`` preserves unknown historical snapshot provenance.
233
+ ``source_request_order='first_stage_rank'`` is available only for registered
234
+ revised public pools. It verifies the original score-vector order first,
235
+ then maps unchanged values by exact candidate ID to the dense request order.
236
+ """
237
+ from .integration_contracts import ComparisonIdentity, value_digest
238
+ if not isinstance(identity, ComparisonIdentity) or identity.track != "rerank":
239
+ raise ContractError("import requires rerank ComparisonIdentity")
240
+ if source_request_order not in ("source", "first_stage_rank"):
241
+ raise ContractError("unsupported recorded source request order")
242
+ model = dict(expected_model)
243
+ _validate_model_identity(model)
244
+ artifact = _read_bound(score_path, expected_sha256)
245
+ requests = _recorded_rows(requests)
246
+ source_model = artifact.get("model")
247
+ if not isinstance(source_model, dict):
248
+ raise ContractError("source recording model identity missing")
249
+ for name in ("id", "checkpoint", "revision", "max_length", "batch_size"):
250
+ if source_model.get(name) != model[name]:
251
+ raise ContractError("source recorded model identity differs")
252
+ if model["snapshot_digest"] != source_model.get("snapshot_digest"):
253
+ raise ContractError("source snapshot digest is absent or differs; historical unknowns must remain null")
254
+ if model["onnx_sha256"] is not None and source_model.get("onnx_sha256", "").removeprefix("sha256:") != model["onnx_sha256"].removeprefix("sha256:"):
255
+ raise ContractError("source recorded ONNX graph identity differs")
256
+ cases = []
257
+ if artifact.get("schema_version") == RECORDED_SCORE_SCHEMA:
258
+ if source_request_order != "source":
259
+ raise ContractError("legacy recording requires unchanged source order")
260
+ if model["activation"] != "legacy_cross_encoder_default":
261
+ raise ContractError("SDK v1 recording requires its historical default activation identity")
262
+ if artifact.get("dataset_digest") != identity.dataset_digest:
263
+ raise ContractError("source recorded dataset identity differs")
264
+ rows = artifact.get("cases")
265
+ if not isinstance(rows, list) or any(not isinstance(row, dict) for row in rows):
266
+ raise ContractError("invalid source recording rows")
267
+ if [row.get("case_id") for row in rows] != list(requests) or artifact.get("case_count") != len(rows):
268
+ raise ContractError("source recording cohort coverage differs")
269
+ for row, request in zip(rows, requests.values()):
270
+ if row.get("request_sha256") != value_digest(request):
271
+ raise ContractError("source recording request identity differs")
272
+ cases.append({"case_id": request["case_id"], "request_sha256": value_digest(request), "scores": _validated_scores(row.get("scores"), request), "model_inference_ms": row.get("model_inference_ms")})
273
+ pool_digest = None
274
+ scope = artifact.get("timing_scope")
275
+ elif artifact.get("schema_version") == "expanded-reranker.model-scores.v2":
276
+ if model["activation"] != "identity":
277
+ raise ContractError("expanded source recording requires raw identity activation")
278
+ if source_pool_path is None or source_pool_sha256 is None or source_task_ids is None:
279
+ raise ContractError("expanded recordings require pinned source pool and explicit task-ID mapping")
280
+ pools = _read_bound(source_pool_path, source_pool_sha256)
281
+ if source_request_order == "first_stage_rank":
282
+ from .public_rerank import PUBLIC_POOL_RELEASES, load_public_pool_release
283
+ release = next((item for item in PUBLIC_POOL_RELEASES
284
+ if item.source_sha256 == source_pool_sha256.removeprefix("sha256:")), None)
285
+ if release is None or identity.dataset_digest != "sha256:" + release.source_sha256:
286
+ raise ContractError("dense request ordering requires an exact registered public release")
287
+ load_public_pool_release(source_pool_path, release_id=release.release_id)
288
+ if artifact.get("pools_sha256") != source_pool_sha256.removeprefix("sha256:"):
289
+ raise ContractError("source recording pool identity differs")
290
+ if set(source_task_ids) != set(requests) or len(set(source_task_ids.values())) != len(requests):
291
+ raise ContractError("source task mapping coverage differs")
292
+ def unique_rows(rows, field):
293
+ if not isinstance(rows, list) or any(not isinstance(row, dict) or not isinstance(row.get(field), str) for row in rows):
294
+ raise ContractError("source rows invalid")
295
+ result = {row[field]: row for row in rows}
296
+ if len(result) != len(rows):
297
+ raise ContractError("source rows contain duplicate IDs")
298
+ return result
299
+ pool_rows = unique_rows(pools.get("queries"), "task_id")
300
+ score_rows = unique_rows(artifact.get("queries"), "task_id")
301
+ for case_id, request in requests.items():
302
+ task_id = source_task_ids[case_id]
303
+ if task_id not in pool_rows or task_id not in score_rows:
304
+ raise ContractError("source recorded cohort incomplete")
305
+ pool, row = pool_rows[task_id], score_rows[task_id]
306
+ source_candidates = pool.get("candidates", [])
307
+ ordered_candidates = sorted(source_candidates, key=lambda item: item["first_stage_rank"]) if source_request_order == "first_stage_rank" else source_candidates
308
+ if pool.get("query") != request["query"] or [{"candidate_id": p.get("candidate_id"), "input": p.get("input")} for p in ordered_candidates] != [{"candidate_id": p["candidate_id"], "input": p["input"]} for p in request["candidates"]]:
309
+ raise ContractError("source pool query/passages differ from request")
310
+ if source_request_order == "first_stage_rank" and any(
311
+ candidate["first_stage_rank"] != requested["dense_rank"] or
312
+ candidate["first_stage_score"] != requested["dense_score"]
313
+ for candidate, requested in zip(ordered_candidates, request["candidates"])
314
+ ):
315
+ raise ContractError("source dense ranks or scores differ from request")
316
+ split = pool.get("split")
317
+ completion = artifact.get("completion", {}).get(split, {})
318
+ split_count = sum(p.get("split") == split for p in pool_rows.values())
319
+ actual_scored = sum(key in score_rows for key, p in pool_rows.items() if p.get("split") == split)
320
+ if completion.get("complete") is not True or completion.get("pool_query_count") != split_count or completion.get("scored_query_count") != actual_scored or actual_scored != split_count:
321
+ raise ContractError("source recording split remains prescreen-only or incomplete")
322
+ ids, values = row.get("candidate_ids"), row.get("scores")
323
+ if not isinstance(ids, list) or not isinstance(values, list) or len(ids) != len(values):
324
+ raise ContractError("source recording candidate coverage differs")
325
+ if source_request_order == "first_stage_rank":
326
+ if ids != [candidate["candidate_id"] for candidate in source_candidates]:
327
+ raise ContractError("recorded original score-vector order differs from source pool")
328
+ by_id = dict(zip(ids, values))
329
+ ids = [candidate["candidate_id"] for candidate in request["candidates"]]
330
+ values = [by_id[cid] for cid in ids]
331
+ scores = [{"candidate_id": cid, "ranking_score": value} for cid, value in zip(ids, values)]
332
+ cases.append({"case_id": case_id, "request_sha256": value_digest(request), "scores": _validated_scores(scores, request), "model_inference_ms": row.get("latency_ms")})
333
+ pool_digest = source_pool_sha256
334
+ scope = "source_sequential_per_query_inference_including_tokenization_excluding_model_load; historical_warmup_unknown"
335
+ else:
336
+ raise ContractError("unsupported source recorded schema")
337
+ output = {"schema_version": BOUND_RECORDED_SCORE_SCHEMA, "identity": identity.to_dict(), "model": model,
338
+ "provenance": {"source_recording_sha256": expected_sha256, "source_pool_sha256": pool_digest, "source_schema": artifact["schema_version"], "runtime": artifact.get("runtime", {}), "timing_scope": scope}, "cases": cases}
339
+ if source_request_order != "source":
340
+ output["provenance"]["request_order_mapping"] = "verified_source_vector_to_first_stage_rank_by_exact_candidate_id"
341
+ # Validate all output values without needing a saved temporary artifact.
342
+ for row in cases:
343
+ latency = row["model_inference_ms"]
344
+ if latency is not None and (isinstance(latency, bool) or not isinstance(latency, (int, float)) or not math.isfinite(latency) or latency < 0):
345
+ raise ContractError("invalid source recorded inference latency")
346
+ if not isinstance(scope, str) or not isinstance(output["provenance"]["runtime"], dict):
347
+ raise ContractError("source timing/runtime provenance missing")
348
+ return output
349
+
350
+
351
+ def create_recorded_scorer(score_path, expected_sha256, *, identity, requests, expected_model):
352
+ return DatasetBoundRecordedScorer(score_path, expected_sha256, identity=identity, requests=requests, expected_model=expected_model)
sphynx/records.py ADDED
@@ -0,0 +1,124 @@
1
+ """Stable evidence and grading identity contracts for later SDK stages.
2
+
3
+ These are immutable value types only. Recording, grading, and persistence are
4
+ implemented by separate subsystems, which must preserve these identities.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+
12
+ from .contracts import ContractError, _id, _text, _tuple_of_ids
13
+
14
+ EVIDENCE_SCHEMA_VERSION = "sphynx.evidence.v1"
15
+ EVALUATION_SCHEMA_VERSION = "sphynx.evaluation.v1"
16
+ METRIC_DEFINITION_SCHEMA_VERSION = "sphynx.metric-definition.v1"
17
+
18
+ _DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
19
+ _COVERAGE_STATES = frozenset(
20
+ {"available", "not_captured", "truncated", "redacted", "corrupt", "expired", "unsupported_schema"}
21
+ )
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class CaptureIdentity:
26
+ """One attempt at one case; its run and capture IDs must stay distinct."""
27
+
28
+ run_id: str
29
+ capture_id: str
30
+ case_id: str
31
+ trial_index: int
32
+ attempt_index: int
33
+ pool_digest: str
34
+ trust_tier: str = "local_unverified"
35
+ schema_version: str = EVIDENCE_SCHEMA_VERSION
36
+
37
+ def __post_init__(self) -> None:
38
+ for field in ("run_id", "capture_id", "case_id"):
39
+ _id(getattr(self, field), field)
40
+ if self.run_id == self.capture_id:
41
+ raise ContractError("run_id and capture_id must be distinct")
42
+ for field in ("trial_index", "attempt_index"):
43
+ value = getattr(self, field)
44
+ if type(value) is not int or value < 1:
45
+ raise ContractError(f"{field} must be a positive integer")
46
+ if not isinstance(self.pool_digest, str) or not _DIGEST.fullmatch(self.pool_digest):
47
+ raise ContractError("pool_digest must be a sha256: lowercase-hex digest")
48
+ if self.trust_tier != "local_unverified":
49
+ raise ContractError("this SDK stage supports only local_unverified trust tier")
50
+ if self.schema_version != EVIDENCE_SCHEMA_VERSION:
51
+ raise ContractError(f"capture schema_version must be {EVIDENCE_SCHEMA_VERSION}")
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class EvaluationIdentity:
56
+ """A grading pass over retained evidence; regrading creates a new ID."""
57
+
58
+ run_id: str
59
+ evaluation_id: str
60
+ grader_release: str
61
+ metric_release: str
62
+ schema_version: str = EVALUATION_SCHEMA_VERSION
63
+
64
+ def __post_init__(self) -> None:
65
+ _id(self.run_id, "run_id")
66
+ _id(self.evaluation_id, "evaluation_id")
67
+ if self.run_id == self.evaluation_id:
68
+ raise ContractError("run_id and evaluation_id must be distinct")
69
+ _id(self.grader_release, "grader_release")
70
+ _id(self.metric_release, "metric_release")
71
+ if self.schema_version != EVALUATION_SCHEMA_VERSION:
72
+ raise ContractError(f"evaluation schema_version must be {EVALUATION_SCHEMA_VERSION}")
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class EvidenceCapability:
77
+ """Observed availability, distinct from a metric result or a zero value."""
78
+
79
+ capability_id: str
80
+ capability_schema_version: str
81
+ state: str
82
+ observed_count: int
83
+ reason: str | None = None
84
+
85
+ def __post_init__(self) -> None:
86
+ _id(self.capability_id, "capability_id")
87
+ _id(self.capability_schema_version, "capability_schema_version")
88
+ if self.state not in _COVERAGE_STATES:
89
+ raise ContractError(f"state must be one of {', '.join(sorted(_COVERAGE_STATES))}")
90
+ if type(self.observed_count) is not int or self.observed_count < 0:
91
+ raise ContractError("observed_count must be a nonnegative integer")
92
+ if self.reason is not None:
93
+ _text(self.reason, "reason")
94
+ if self.state != "available" and self.reason is None:
95
+ raise ContractError("unavailable evidence requires a reason")
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class MetricDefinition:
100
+ """Versioned metric declaration; computation belongs to the grader."""
101
+
102
+ metric_id: str
103
+ release: str
104
+ unit: str
105
+ direction: str
106
+ required_capabilities: tuple[str, ...]
107
+ primary: bool = False
108
+ schema_version: str = METRIC_DEFINITION_SCHEMA_VERSION
109
+
110
+ def __post_init__(self) -> None:
111
+ _id(self.metric_id, "metric_id")
112
+ _id(self.release, "release")
113
+ _id(self.unit, "unit")
114
+ if self.direction not in ("higher", "lower"):
115
+ raise ContractError("direction must be higher or lower")
116
+ object.__setattr__(
117
+ self,
118
+ "required_capabilities",
119
+ _tuple_of_ids(self.required_capabilities, "required_capabilities"),
120
+ )
121
+ if type(self.primary) is not bool:
122
+ raise ContractError("primary must be a boolean")
123
+ if self.schema_version != METRIC_DEFINITION_SCHEMA_VERSION:
124
+ raise ContractError(f"metric definition schema_version must be {METRIC_DEFINITION_SCHEMA_VERSION}")