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.
- sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
- sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
- sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
- sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
- sphynx/__init__.py +469 -0
- sphynx/__init__.pyi +376 -0
- sphynx/_analytical_client.py +77 -0
- sphynx/_analytical_values.py +252 -0
- sphynx/_audit_client.py +173 -0
- sphynx/_comparison_adapters.py +113 -0
- sphynx/_componentized_full_rag.py +34 -0
- sphynx/_composed_pipeline.py +109 -0
- sphynx/_full_rag_grading.py +218 -0
- sphynx/_full_rag_trace.py +221 -0
- sphynx/_local_eval.py +268 -0
- sphynx/_pipeline_core.py +243 -0
- sphynx/_pr_client.py +237 -0
- sphynx/_rag_diagnostics_calibration.py +285 -0
- sphynx/_rag_diagnostics_catalog.py +39 -0
- sphynx/_rag_diagnostics_common.py +26 -0
- sphynx/_rag_diagnostics_contracts.py +186 -0
- sphynx/_rag_diagnostics_engine.py +243 -0
- sphynx/_rag_diagnostics_importers.py +242 -0
- sphynx/_rag_diagnostics_ranking.py +81 -0
- sphynx/_rag_diagnostics_semantic.py +325 -0
- sphynx/_rag_ranking.py +149 -0
- sphynx/_speech_benchmark_scoring.py +140 -0
- sphynx/_speech_benchmark_sources.py +275 -0
- sphynx/_speech_complete_scoring.py +218 -0
- sphynx/_speech_complete_source.py +118 -0
- sphynx/_speech_kido.py +294 -0
- sphynx/_speech_scoring.py +77 -0
- sphynx/_track_capture.py +213 -0
- sphynx/analytical_views.py +515 -0
- sphynx/assessment_contracts.py +378 -0
- sphynx/assessment_selection.py +57 -0
- sphynx/audit_access.py +245 -0
- sphynx/audit_contracts.py +204 -0
- sphynx/audit_requests.py +180 -0
- sphynx/authoring.py +188 -0
- sphynx/benchmarks.py +568 -0
- sphynx/candidates.py +62 -0
- sphynx/catalog.py +272 -0
- sphynx/chunking.py +372 -0
- sphynx/clickhouse_reader.py +169 -0
- sphynx/client.py +486 -0
- sphynx/comparison_core.py +149 -0
- sphynx/comparison_views.py +223 -0
- sphynx/components.py +1661 -0
- sphynx/contracts.py +514 -0
- sphynx/customer_rerank.py +299 -0
- sphynx/customer_retrieval.py +146 -0
- sphynx/data/benchmark-releases.v1.json +164 -0
- sphynx/data/public-benchmark-release.v1.json +354 -0
- sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
- sphynx/data/rag-paper-references.v1.json +176 -0
- sphynx/data/rag-paper-schemas.v1.json +1069 -0
- sphynx/data/rag-rerank-v1.json +507 -0
- sphynx/dataset_catalog_data.py +1214 -0
- sphynx/dataset_readiness.py +138 -0
- sphynx/dataset_v3.py +999 -0
- sphynx/demo.py +130 -0
- sphynx/demo_candidates/__init__.py +1 -0
- sphynx/demo_candidates/dense.py +11 -0
- sphynx/demo_candidates/lexical.py +21 -0
- sphynx/diagnostics.py +207 -0
- sphynx/documentation.py +737 -0
- sphynx/documentation_candidate.py +939 -0
- sphynx/evals.py +538 -0
- sphynx/evaluation_protocols.py +124 -0
- sphynx/evaluator_bindings.py +370 -0
- sphynx/evidence.py +352 -0
- sphynx/execution_capabilities.py +109 -0
- sphynx/fast_search.py +285 -0
- sphynx/full_rag.py +341 -0
- sphynx/hosted/__init__.py +18 -0
- sphynx/hosted/benchmark_intake.py +218 -0
- sphynx/hosted/contracts.py +361 -0
- sphynx/hosted/model_admission.py +130 -0
- sphynx/hosted/model_artifacts.py +298 -0
- sphynx/hosted/model_contracts.py +63 -0
- sphynx/hosted/model_gcs.py +181 -0
- sphynx/hosted/model_history.py +84 -0
- sphynx/hosted/model_session.py +233 -0
- sphynx/hosted/public_suite.py +74 -0
- sphynx/hosted/registry.py +97 -0
- sphynx/hosted/revisions.py +126 -0
- sphynx/hosted/sandbox.py +105 -0
- sphynx/hosted/submission.py +287 -0
- sphynx/hosted_workflow.py +451 -0
- sphynx/http_transport.py +86 -0
- sphynx/integrated_rag.py +352 -0
- sphynx/integrated_workflow.py +552 -0
- sphynx/integration_contracts.py +474 -0
- sphynx/invocation.py +791 -0
- sphynx/invocation_candidates.py +392 -0
- sphynx/invocation_catalog_data.py +40 -0
- sphynx/learned_catalog_data.py +30 -0
- sphynx/learned_rerank.py +267 -0
- sphynx/legacy_full_rag.py +67 -0
- sphynx/lifecycle.py +885 -0
- sphynx/lifecycle_workflow.py +490 -0
- sphynx/metric_views.py +474 -0
- sphynx/metrics.py +282 -0
- sphynx/model_adapters.py +438 -0
- sphynx/model_catalog_data.py +30 -0
- sphynx/official_benchmarks.py +385 -0
- sphynx/packs.py +258 -0
- sphynx/pipeline.py +313 -0
- sphynx/pipeline_workflow.py +551 -0
- sphynx/pr_evaluations.py +560 -0
- sphynx/progress.py +89 -0
- sphynx/public_bundle.py +173 -0
- sphynx/public_eval.py +62 -0
- sphynx/public_rerank.py +355 -0
- sphynx/py.typed +0 -0
- sphynx/pytest_plugin.py +62 -0
- sphynx/question_robustness.py +437 -0
- sphynx/rag.py +329 -0
- sphynx/rag_diagnostics.py +309 -0
- sphynx/rag_papers/__init__.py +29 -0
- sphynx/rag_papers/__main__.py +45 -0
- sphynx/rag_papers/candidates.py +19 -0
- sphynx/rag_papers/contracts.py +282 -0
- sphynx/rag_papers/datasets.py +291 -0
- sphynx/rag_papers/experiments.py +116 -0
- sphynx/rag_papers/judges.py +308 -0
- sphynx/rag_papers/registry.py +33 -0
- sphynx/rag_papers/runner.py +176 -0
- sphynx/rag_papers/sable.py +135 -0
- sphynx/rag_papers/schemas.py +12 -0
- sphynx/rag_papers/scoring.py +249 -0
- sphynx/rag_papers/semantic.py +288 -0
- sphynx/rag_papers/trec.py +80 -0
- sphynx/recorded_scores.py +352 -0
- sphynx/records.py +124 -0
- sphynx/reflex_traces.py +193 -0
- sphynx/report_rendering.py +71 -0
- sphynx/rerank_family_contracts.py +150 -0
- sphynx/rerank_workflow.py +424 -0
- sphynx/research_candidate.py +190 -0
- sphynx/results.py +852 -0
- sphynx/retrieval.py +750 -0
- sphynx/retrieval_candidates.py +295 -0
- sphynx/retrieval_catalog_data.py +19 -0
- sphynx/retrieval_recordings.py +125 -0
- sphynx/reviewer_audit.py +266 -0
- sphynx/routing_policies.py +375 -0
- sphynx/runner.py +544 -0
- sphynx/sable_rerank.py +489 -0
- sphynx/score_reliability.py +393 -0
- sphynx/speech_benchmarks.py +204 -0
- sphynx/speech_campaigns.py +378 -0
- sphynx/speech_complete.py +236 -0
- sphynx/speech_contracts.py +225 -0
- sphynx/speech_evidence.py +429 -0
- sphynx/speech_listening.py +115 -0
- sphynx/speech_multilingual.py +71 -0
- sphynx/speech_tts.py +479 -0
- sphynx/sql/001_sync_v1.sql +30 -0
- sphynx/stage_contracts.py +199 -0
- sphynx/statistical_catalog_data.py +53 -0
- sphynx/statistical_rerank.py +232 -0
- sphynx/sync.py +797 -0
- sphynx/workbench.py +378 -0
- sphynx/workbench_catalog_data.py +13 -0
- sphynx/workbench_tracks.py +78 -0
- sphynx/workflow_async.py +197 -0
- sphynx/workflow_contracts.py +594 -0
- sphynx/workflow_decisions.py +211 -0
- sphynx/workflow_defaults.py +16 -0
- sphynx/workflow_extensions.py +212 -0
- sphynx/workflow_registry.py +513 -0
- 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
|