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,474 @@
1
+ """Additive integration value contracts; this module never executes candidates.
2
+
3
+ All digests are canonical ``sha256:`` lowercase hex. Raw-file digests bind exact
4
+ bytes; value digests use the existing canonical_json encoding. Legacy candidate
5
+ responses and pack identities retain their original semantics.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import re
11
+ from collections.abc import Mapping, Sequence
12
+ from dataclasses import dataclass, fields, is_dataclass
13
+ from pathlib import Path, PurePosixPath
14
+ from types import MappingProxyType
15
+ from typing import Any
16
+
17
+ from .contracts import ContractError, _finite_number, _id, _text, _tuple_of_ids, canonical_json
18
+
19
+ CONFIGURATION_SCHEMA = "sphynx.candidate-configuration.v1"
20
+ SCORE_SEMANTICS_SCHEMA = "sphynx.score-semantics.v1"
21
+ RICH_SCORES_SCHEMA = "sphynx.rich-score-capture.v1"
22
+ COMPARISON_SCHEMA = "sphynx.track-comparison-identity.v1"
23
+ TRACK_CAPTURE_SCHEMA = "sphynx.track-capture.v1"
24
+ TRACK_EVALUATION_SCHEMA = "sphynx.track-evaluation.v1"
25
+ _DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
26
+ _SECRET_KEYS = frozenset({"api_key", "apikey", "access_token", "refresh_token", "password", "secret", "authorization", "credential", "credentials", "private_key", "credential_value", "bearer_token", "token"})
27
+
28
+
29
+ def require_digest(value: object, field: str) -> str:
30
+ if not isinstance(value, str) or not _DIGEST.fullmatch(value):
31
+ raise ContractError(f"{field} must be a sha256: lowercase-hex digest")
32
+ return value
33
+
34
+
35
+ def _optional_digest(value: object, field: str) -> None:
36
+ if value is not None:
37
+ require_digest(value, field)
38
+
39
+
40
+ def _schema(actual: str, expected: str) -> None:
41
+ if actual != expected:
42
+ raise ContractError(f"schema_version must be {expected}")
43
+
44
+
45
+ def _choice(value: object, choices: tuple[str, ...], field: str) -> None:
46
+ if not isinstance(value, str) or value not in choices:
47
+ raise ContractError(f"{field} must be one of {', '.join(choices)}")
48
+
49
+
50
+ def _count(value: object, field: str, minimum: int = 0) -> None:
51
+ if type(value) is not int or value < minimum:
52
+ raise ContractError(f"{field} must be an integer >= {minimum}")
53
+
54
+
55
+ def _sequence(value: object, item_type: type, field: str) -> tuple:
56
+ if isinstance(value, (str, bytes)) or not isinstance(value, Sequence):
57
+ raise ContractError(f"{field} must be a sequence")
58
+ result = tuple(value)
59
+ if any(not isinstance(item, item_type) for item in result):
60
+ raise ContractError(f"{field} must contain {item_type.__name__} values")
61
+ return result
62
+
63
+
64
+ def _freeze(value: Any, *, reject_secrets: bool = False) -> Any:
65
+ if isinstance(value, Mapping):
66
+ result = {}
67
+ for key, item in value.items():
68
+ if not isinstance(key, str):
69
+ raise ContractError("JSON object keys must be strings")
70
+ normalized = key.lower().replace('-', '_')
71
+ if reject_secrets and (normalized in _SECRET_KEYS or normalized.endswith(("_api_key", "_access_token", "_password", "_secret", "_private_key"))):
72
+ raise ContractError("credential values are forbidden; use credential_refs")
73
+ result[key] = _freeze(item, reject_secrets=reject_secrets)
74
+ return MappingProxyType(result)
75
+ if isinstance(value, (list, tuple)):
76
+ return tuple(_freeze(item, reject_secrets=reject_secrets) for item in value)
77
+ if value is None or type(value) in (str, bool, int):
78
+ return value
79
+ if type(value) is float:
80
+ _finite_number(value, "JSON value")
81
+ return value
82
+ raise ContractError("value must contain only finite JSON data")
83
+
84
+
85
+ def _wire(value: Any) -> Any:
86
+ if is_dataclass(value):
87
+ return {field.name: _wire(getattr(value, field.name)) for field in fields(value)}
88
+ if isinstance(value, Mapping):
89
+ return {key: _wire(item) for key, item in value.items()}
90
+ if isinstance(value, (tuple, list)):
91
+ return [_wire(item) for item in value]
92
+ return value
93
+
94
+
95
+ def value_digest(value: Any) -> str:
96
+ return "sha256:" + hashlib.sha256(canonical_json(_wire(value)).encode("utf-8")).hexdigest()
97
+
98
+
99
+ class WireValue:
100
+ def to_dict(self) -> dict[str, Any]:
101
+ return _wire(self)
102
+
103
+ @classmethod
104
+ def _checked_fields(cls, value: Mapping[str, Any]) -> dict[str, Any]:
105
+ if not isinstance(value, Mapping) or set(value) != {field.name for field in fields(cls)}:
106
+ raise ContractError(f"{cls.__name__} fields differ from the exact schema")
107
+ return dict(value)
108
+
109
+ @classmethod
110
+ def from_dict(cls, value: Mapping[str, Any]):
111
+ return cls(**cls._checked_fields(value))
112
+
113
+ @property
114
+ def digest(self) -> str:
115
+ return value_digest(self)
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class CandidateConfiguration(WireValue):
120
+ configuration_id: str
121
+ interface_version: str
122
+ source_digest: str
123
+ parameters: Mapping[str, Any]
124
+ execution_mode: str
125
+ model_id: str | None = None
126
+ model_revision: str | None = None
127
+ model_snapshot_digest: str | None = None
128
+ credential_refs: tuple[str, ...] = ()
129
+ schema_version: str = CONFIGURATION_SCHEMA
130
+
131
+ def __post_init__(self) -> None:
132
+ _schema(self.schema_version, CONFIGURATION_SCHEMA)
133
+ _id(self.configuration_id, "configuration_id")
134
+ _id(self.interface_version, "interface_version")
135
+ require_digest(self.source_digest, "source_digest")
136
+ _choice(self.execution_mode, ("recorded", "trusted_local", "external_command", "live_provider"), "execution_mode")
137
+ if not isinstance(self.parameters, Mapping):
138
+ raise ContractError("parameters must be a JSON object")
139
+ object.__setattr__(self, "parameters", _freeze(self.parameters, reject_secrets=True))
140
+ refs = _tuple_of_ids(self.credential_refs, "credential_refs")
141
+ if any(not ref.startswith(("env:", "keyring:", "credential:")) or not ref.split(":", 1)[1] for ref in refs):
142
+ raise ContractError("credential_refs must use env:, keyring:, or credential: references")
143
+ object.__setattr__(self, "credential_refs", refs)
144
+ model = (self.model_id, self.model_revision, self.model_snapshot_digest)
145
+ if any(item is not None for item in model):
146
+ if any(item is None for item in model):
147
+ raise ContractError("model ID, revision and snapshot digest must be supplied together")
148
+ _text(self.model_id, "model_id")
149
+ _text(self.model_revision, "model_revision")
150
+ require_digest(self.model_snapshot_digest, "model_snapshot_digest")
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class ScoreField(WireValue):
155
+ field_id: str
156
+ kind: str
157
+ direction: str
158
+ unit: str
159
+ probability_target: str | None = None
160
+
161
+ def __post_init__(self) -> None:
162
+ _id(self.field_id, "field_id")
163
+ _choice(self.kind, ("utility", "pool_relative", "diagnostic", "probability"), "score kind")
164
+ _choice(self.direction, ("higher", "lower"), "direction")
165
+ _id(self.unit, "unit")
166
+ if self.kind == "probability":
167
+ _id(self.probability_target, "probability_target")
168
+ if self.unit != "probability" or self.direction != "higher":
169
+ raise ContractError("probability requires unit=probability and direction=higher")
170
+ elif self.probability_target is not None or self.unit == "probability":
171
+ raise ContractError("only a probability field may declare probability semantics")
172
+
173
+
174
+ @dataclass(frozen=True)
175
+ class ScoreSemantics(WireValue):
176
+ subject_digest: str
177
+ fields: tuple[ScoreField, ...]
178
+ fitted_state_digest: str | None = None
179
+ schema_version: str = SCORE_SEMANTICS_SCHEMA
180
+
181
+ def __post_init__(self) -> None:
182
+ _schema(self.schema_version, SCORE_SEMANTICS_SCHEMA)
183
+ require_digest(self.subject_digest, "subject_digest")
184
+ _optional_digest(self.fitted_state_digest, "fitted_state_digest")
185
+ items = _sequence(self.fields, ScoreField, "fields")
186
+ if not items or len({item.field_id for item in items}) != len(items):
187
+ raise ContractError("score fields must be nonempty and unique")
188
+ object.__setattr__(self, "fields", items)
189
+
190
+ @classmethod
191
+ def from_dict(cls, value):
192
+ checked = cls._checked_fields(value)
193
+ if isinstance(checked, dict) and "fields" in checked:
194
+ checked["fields"] = tuple(ScoreField.from_dict(item) for item in _sequence(checked["fields"], Mapping, "fields"))
195
+ return super().from_dict(checked)
196
+
197
+
198
+ @dataclass(frozen=True)
199
+ class RichScoreRecord(WireValue):
200
+ case_id: str
201
+ request_digest: str
202
+ pool_digest: str
203
+ candidate_id: str
204
+ field_id: str
205
+ value: float | None
206
+
207
+ def __post_init__(self) -> None:
208
+ for name in ("case_id", "candidate_id", "field_id"):
209
+ _id(getattr(self, name), name)
210
+ for name in ("request_digest", "pool_digest"):
211
+ require_digest(getattr(self, name), name)
212
+ if self.value is not None:
213
+ object.__setattr__(self, "value", _finite_number(self.value, "score"))
214
+
215
+
216
+ @dataclass(frozen=True)
217
+ class RichScoreCapture(WireValue):
218
+ capture_manifest_digest: str
219
+ cohort_digest: str
220
+ reference_digest: str
221
+ semantics: ScoreSemantics
222
+ records: tuple[RichScoreRecord, ...]
223
+ schema_version: str = RICH_SCORES_SCHEMA
224
+
225
+ def __post_init__(self) -> None:
226
+ _schema(self.schema_version, RICH_SCORES_SCHEMA)
227
+ for name in ("capture_manifest_digest", "cohort_digest", "reference_digest"):
228
+ require_digest(getattr(self, name), name)
229
+ if not isinstance(self.semantics, ScoreSemantics):
230
+ raise ContractError("semantics must be ScoreSemantics")
231
+ records = _sequence(self.records, RichScoreRecord, "records")
232
+ definitions = {item.field_id: item for item in self.semantics.fields}
233
+ seen = set()
234
+ cases = {}
235
+ for record in records:
236
+ key = (record.case_id, record.candidate_id, record.field_id)
237
+ if key in seen or record.field_id not in definitions:
238
+ raise ContractError("duplicate score record or undeclared score field")
239
+ seen.add(key)
240
+ binding = (record.request_digest, record.pool_digest)
241
+ if record.case_id in cases and cases[record.case_id] != binding:
242
+ raise ContractError("scores for one case must bind the same request and pool")
243
+ cases[record.case_id] = binding
244
+ if definitions[record.field_id].kind == "probability" and record.value is not None and not 0 <= record.value <= 1:
245
+ raise ContractError("probability values must lie in [0, 1]")
246
+ object.__setattr__(self, "records", records)
247
+
248
+ @classmethod
249
+ def from_dict(cls, value):
250
+ checked = cls._checked_fields(value)
251
+ if isinstance(checked, dict):
252
+ if "semantics" in checked:
253
+ checked["semantics"] = ScoreSemantics.from_dict(checked["semantics"])
254
+ if "records" in checked:
255
+ checked["records"] = tuple(RichScoreRecord.from_dict(item) for item in _sequence(checked["records"], Mapping, "records"))
256
+ return super().from_dict(checked)
257
+
258
+
259
+ @dataclass(frozen=True)
260
+ class ComparisonIdentity(WireValue):
261
+ track: str
262
+ pack_id: str
263
+ pack_release: str
264
+ dataset_schema_version: str
265
+ dataset_digest: str
266
+ cohort_digest: str
267
+ reference_digest: str
268
+ policy_digest: str
269
+ metric_release: str
270
+ grader_release: str
271
+ subject_digest: str
272
+ context_condition: str = "none"
273
+ generator_digest: str | None = None
274
+ score_semantics_digest: str | None = None
275
+ fitted_state_digest: str | None = None
276
+ judgment_policy_digest: str | None = None
277
+ schema_version: str = COMPARISON_SCHEMA
278
+
279
+ def __post_init__(self) -> None:
280
+ _schema(self.schema_version, COMPARISON_SCHEMA)
281
+ for name in ("track", "pack_id", "pack_release", "dataset_schema_version", "metric_release", "grader_release"):
282
+ _id(getattr(self, name), name)
283
+ for name in ("dataset_digest", "cohort_digest", "reference_digest", "policy_digest", "subject_digest"):
284
+ require_digest(getattr(self, name), name)
285
+ for name in ("generator_digest", "score_semantics_digest", "fitted_state_digest", "judgment_policy_digest"):
286
+ _optional_digest(getattr(self, name), name)
287
+ _choice(self.context_condition, ("none", "retrieved", "oracle", "empty"), "context_condition")
288
+ if self.context_condition != "none" and self.generator_digest is None:
289
+ raise ContractError("context conditions require a pinned generator digest")
290
+ if self.fitted_state_digest is not None and self.score_semantics_digest is None:
291
+ raise ContractError("fitted state requires score semantics identity")
292
+
293
+
294
+ def require_comparable(baseline: ComparisonIdentity, candidate: ComparisonIdentity, *, context_conditions: bool = False) -> None:
295
+ """Reject mismatched fixed boundaries; only explicit context diagnostics vary context.
296
+
297
+ subject_digest pins the subject boundary and frozen unchanged components.
298
+ The changed candidate/configuration is captured separately, never silently
299
+ substituted into a recording. It is not a requirement that two candidate
300
+ implementations under comparison have identical source code.
301
+ """
302
+ if not isinstance(baseline, ComparisonIdentity) or not isinstance(candidate, ComparisonIdentity):
303
+ raise ContractError("comparison requires ComparisonIdentity values")
304
+ left, right = baseline.to_dict(), candidate.to_dict()
305
+ if context_conditions:
306
+ if baseline.context_condition == "none" or candidate.context_condition == "none":
307
+ raise ContractError("context diagnostics require explicit generator conditions")
308
+ left.pop("context_condition")
309
+ right.pop("context_condition")
310
+ different = [name for name in left if left[name] != right[name]]
311
+ if different:
312
+ raise ContractError("incompatible comparison identity: " + ", ".join(different))
313
+
314
+
315
+ @dataclass(frozen=True)
316
+ class ArtifactBinding(WireValue):
317
+ path: str
318
+ sha256: str
319
+ size_bytes: int
320
+
321
+ def __post_init__(self) -> None:
322
+ _text(self.path, "artifact path")
323
+ path = PurePosixPath(self.path)
324
+ if path.is_absolute() or "\\" in self.path or ":" in self.path or any(part in ("", ".", "..") for part in self.path.split("/")):
325
+ raise ContractError("artifact path must be a safe relative POSIX path")
326
+ require_digest(self.sha256, "artifact sha256")
327
+ _count(self.size_bytes, "size_bytes")
328
+
329
+
330
+ @dataclass(frozen=True)
331
+ class TrackCaseCapture(WireValue):
332
+ case_id: str
333
+ trial_index: int
334
+ attempt_index: int
335
+ request: ArtifactBinding
336
+ response: ArtifactBinding | None
337
+ observed: ArtifactBinding | None
338
+ failure: str | None
339
+ runner_latency_ms: float | None
340
+
341
+ def __post_init__(self) -> None:
342
+ _id(self.case_id, "case_id")
343
+ _count(self.trial_index, "trial_index", 1)
344
+ _count(self.attempt_index, "attempt_index", 1)
345
+ if not isinstance(self.request, ArtifactBinding):
346
+ raise ContractError("request must be ArtifactBinding")
347
+ for name in ("response", "observed"):
348
+ item = getattr(self, name)
349
+ if item is not None and not isinstance(item, ArtifactBinding):
350
+ raise ContractError(f"{name} must be ArtifactBinding or null")
351
+ if self.failure is not None:
352
+ _id(self.failure, "failure")
353
+ if self.response is None and self.failure is None:
354
+ raise ContractError("missing response requires an explicit failure state")
355
+ if self.runner_latency_ms is not None:
356
+ latency = _finite_number(self.runner_latency_ms, "runner_latency_ms")
357
+ if latency < 0:
358
+ raise ContractError("runner latency must be nonnegative")
359
+ object.__setattr__(self, "runner_latency_ms", latency)
360
+
361
+ @classmethod
362
+ def from_dict(cls, value):
363
+ checked = cls._checked_fields(value)
364
+ if isinstance(checked, dict):
365
+ for name in ("request", "response", "observed"):
366
+ if name in checked and checked[name] is not None:
367
+ checked[name] = ArtifactBinding.from_dict(checked[name])
368
+ return super().from_dict(checked)
369
+
370
+
371
+ @dataclass(frozen=True)
372
+ class TrackCaptureManifest(WireValue):
373
+ run_id: str
374
+ identity: ComparisonIdentity
375
+ candidate: CandidateConfiguration
376
+ case_ids: tuple[str, ...]
377
+ captures: tuple[TrackCaseCapture, ...]
378
+ trust_tier: str = "local_unverified"
379
+ schema_version: str = TRACK_CAPTURE_SCHEMA
380
+
381
+ def __post_init__(self) -> None:
382
+ _schema(self.schema_version, TRACK_CAPTURE_SCHEMA)
383
+ _id(self.run_id, "run_id")
384
+ if not isinstance(self.identity, ComparisonIdentity) or not isinstance(self.candidate, CandidateConfiguration):
385
+ raise ContractError("capture requires comparison identity and candidate configuration")
386
+ if self.trust_tier != "local_unverified":
387
+ raise ContractError("only local_unverified captures are supported")
388
+ case_ids = _tuple_of_ids(self.case_ids, "case_ids", empty=False)
389
+ captures = _sequence(self.captures, TrackCaseCapture, "captures")
390
+ keys = [(item.case_id, item.trial_index, item.attempt_index) for item in captures]
391
+ if len(set(keys)) != len(keys) or {item.case_id for item in captures} != set(case_ids):
392
+ raise ContractError("capture coverage must match cohort with unique case/trial/attempt identities")
393
+ paths = [binding.path for item in captures for binding in (item.request, item.response, item.observed) if binding is not None]
394
+ if len(set(paths)) != len(paths):
395
+ raise ContractError("capture artifacts must have unique paths")
396
+ object.__setattr__(self, "case_ids", case_ids)
397
+ object.__setattr__(self, "captures", captures)
398
+
399
+ @classmethod
400
+ def from_dict(cls, value):
401
+ checked = cls._checked_fields(value)
402
+ if isinstance(checked, dict):
403
+ if "identity" in checked:
404
+ checked["identity"] = ComparisonIdentity.from_dict(checked["identity"])
405
+ if "candidate" in checked:
406
+ checked["candidate"] = CandidateConfiguration.from_dict(checked["candidate"])
407
+ if "captures" in checked:
408
+ checked["captures"] = tuple(TrackCaseCapture.from_dict(item) for item in _sequence(checked["captures"], Mapping, "captures"))
409
+ return super().from_dict(checked)
410
+
411
+
412
+ @dataclass(frozen=True)
413
+ class TrackEvaluationManifest(WireValue):
414
+ evaluation_id: str
415
+ run_id: str
416
+ capture_manifest_digest: str
417
+ identity: ComparisonIdentity
418
+ metrics: Mapping[str, Any]
419
+ judgment_artifact_digest: str | None = None
420
+ schema_version: str = TRACK_EVALUATION_SCHEMA
421
+
422
+ def __post_init__(self) -> None:
423
+ _schema(self.schema_version, TRACK_EVALUATION_SCHEMA)
424
+ _id(self.evaluation_id, "evaluation_id")
425
+ _id(self.run_id, "run_id")
426
+ if self.evaluation_id == self.run_id:
427
+ raise ContractError("evaluation_id and run_id must be distinct")
428
+ require_digest(self.capture_manifest_digest, "capture_manifest_digest")
429
+ _optional_digest(self.judgment_artifact_digest, "judgment_artifact_digest")
430
+ if not isinstance(self.identity, ComparisonIdentity) or not isinstance(self.metrics, Mapping):
431
+ raise ContractError("evaluation requires comparison identity and a metric mapping")
432
+ object.__setattr__(self, "metrics", _freeze(self.metrics))
433
+
434
+ @classmethod
435
+ def from_dict(cls, value):
436
+ checked = cls._checked_fields(value)
437
+ if isinstance(checked, dict) and "identity" in checked:
438
+ checked["identity"] = ComparisonIdentity.from_dict(checked["identity"])
439
+ return super().from_dict(checked)
440
+
441
+
442
+ @dataclass(frozen=True)
443
+ class TrackRunRef:
444
+ path: Path
445
+ manifest_sha256: str
446
+
447
+ def __post_init__(self) -> None:
448
+ object.__setattr__(self, "path", Path(self.path))
449
+ require_digest(self.manifest_sha256, "manifest_sha256")
450
+ _id(self.path.name, "run_id")
451
+
452
+ @property
453
+ def run_id(self) -> str:
454
+ return self.path.name
455
+
456
+
457
+ @dataclass(frozen=True)
458
+ class TrackEvaluationRef:
459
+ path: Path
460
+ scorecard_sha256: str
461
+ run: TrackRunRef
462
+
463
+ def __post_init__(self) -> None:
464
+ object.__setattr__(self, "path", Path(self.path))
465
+ require_digest(self.scorecard_sha256, "scorecard_sha256")
466
+ _id(self.path.name, "evaluation_id")
467
+ if not isinstance(self.run, TrackRunRef):
468
+ raise ContractError("run must be TrackRunRef")
469
+ if self.path.parent.name != "evaluations" or self.path.parent.parent != self.run.path:
470
+ raise ContractError("evaluation must be retained inside its original run/evaluations directory")
471
+
472
+ @property
473
+ def evaluation_id(self) -> str:
474
+ return self.path.name