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,378 @@
1
+ """Domain-neutral read views for cases, evidence, and assessments.
2
+
3
+ These views do not replace native capture, grading, or comparison records. A
4
+ schema name identifies a payload contract; the domain backend must validate that
5
+ contract before it creates a view. JSON validation here checks the envelope only.
6
+
7
+ An evaluator context belongs inside the trusted grader. Its private builder is
8
+ called after the backend verifies native records and reference access. Python
9
+ objects, private functions, and hashes do not establish authentication or process
10
+ isolation. Local records retain ``local_unverified`` authority.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import math
15
+ import re
16
+ from dataclasses import InitVar, dataclass, field
17
+ from types import MappingProxyType
18
+ from typing import TYPE_CHECKING, Any, Mapping, Sequence
19
+
20
+ if TYPE_CHECKING:
21
+ from .workflow_contracts import PayloadSchema
22
+
23
+
24
+ ASSESSMENT_VIEW_SCHEMA = "sphynx.assessment-view.v1"
25
+ STRUCTURED_ASSESSMENT_VIEW_SCHEMA = "sphynx.assessment-view.v2"
26
+ _DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
27
+ _CONTEXT_TOKEN = object()
28
+ _STRUCTURED_ASSESSMENT_TOKEN = object()
29
+ _CUSTOM_KIND = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:/@+\-]*\Z")
30
+ _ASSESSMENT_AVAILABILITY = frozenset({
31
+ "available", "missing", "unavailable", "undefined", "error", "ineligible",
32
+ "pending", "invalid", "redacted", "truncated",
33
+ })
34
+ _EVIDENCE_AVAILABILITY = frozenset({
35
+ "available", "missing", "unavailable", "invalid", "redacted", "truncated",
36
+ })
37
+
38
+
39
+ class AssessmentContractError(ValueError):
40
+ """A shared view is malformed or has inconsistent availability."""
41
+
42
+
43
+ def _text(value: Any, name: str) -> str:
44
+ if type(value) is not str or not value.strip() or "\x00" in value:
45
+ raise AssessmentContractError(f"{name} must be nonempty text without NUL bytes")
46
+ try:
47
+ value.encode("utf-8", errors="strict")
48
+ except UnicodeEncodeError:
49
+ raise AssessmentContractError(f"{name} must contain valid Unicode") from None
50
+ return value
51
+
52
+
53
+ def _optional_text(value: Any, name: str) -> None:
54
+ if value is not None:
55
+ _text(value, name)
56
+
57
+
58
+ def _freeze_json(value: Any, name: str, depth: int = 0) -> Any:
59
+ if depth > 32:
60
+ raise AssessmentContractError(f"{name} exceeds the JSON nesting limit")
61
+ if value is None or type(value) is bool or type(value) is int:
62
+ return value
63
+ if type(value) is float:
64
+ if not math.isfinite(value):
65
+ raise AssessmentContractError(f"{name} must contain finite numbers")
66
+ return value
67
+ if type(value) is str:
68
+ # JSON strings may be empty; NUL is legal within a JSON payload.
69
+ try:
70
+ value.encode("utf-8", errors="strict")
71
+ except UnicodeEncodeError:
72
+ raise AssessmentContractError(f"{name} must contain valid Unicode") from None
73
+ return value
74
+ if isinstance(value, Mapping):
75
+ if any(type(key) is not str for key in value):
76
+ raise AssessmentContractError(f"{name} must use string object keys")
77
+ for key in value:
78
+ _freeze_json(key, name, depth + 1)
79
+ return MappingProxyType({
80
+ key: _freeze_json(child, name, depth + 1) for key, child in value.items()
81
+ })
82
+ if isinstance(value, (list, tuple)):
83
+ return tuple(_freeze_json(child, name, depth + 1) for child in value)
84
+ raise AssessmentContractError(f"{name} must contain only JSON values")
85
+
86
+
87
+ def _mapping(value: Any, name: str) -> Mapping[str, Any]:
88
+ if not isinstance(value, Mapping):
89
+ raise AssessmentContractError(f"{name} must be a JSON object")
90
+ return _freeze_json(value, name)
91
+
92
+
93
+ def _thaw(value: Any) -> Any:
94
+ if isinstance(value, Mapping):
95
+ return {key: _thaw(child) for key, child in value.items()}
96
+ if isinstance(value, tuple):
97
+ return [_thaw(child) for child in value]
98
+ return value
99
+
100
+
101
+ def _texts(values: Sequence[str], name: str) -> tuple[str, ...]:
102
+ if isinstance(values, (str, bytes)) or not isinstance(values, Sequence):
103
+ raise AssessmentContractError(f"{name} must be a sequence of text values")
104
+ result = tuple(_text(value, name) for value in values)
105
+ if len(set(result)) != len(result):
106
+ raise AssessmentContractError(f"{name} must not contain duplicates")
107
+ return result
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class CaseView:
112
+ """Candidate-permitted case inputs, without grading references.
113
+
114
+ The backend allowlists the fields copied into ``inputs`` and ``metadata``.
115
+ The plain constructor validates the JSON envelope only. Use ``validated``
116
+ with a registered PayloadSchema to validate domain content. Neither path can
117
+ infer whether arbitrary caller-supplied data is protected. Grouping, split,
118
+ session, and scenario identities can use domain metadata.
119
+ """
120
+
121
+ case_id: str
122
+ schema_version: str
123
+ inputs: Mapping[str, Any]
124
+ metadata: Mapping[str, Any] = field(default_factory=dict)
125
+
126
+ def __post_init__(self) -> None:
127
+ _text(self.case_id, "case_id")
128
+ _text(self.schema_version, "schema_version")
129
+ object.__setattr__(self, "inputs", _mapping(self.inputs, "inputs"))
130
+ object.__setattr__(self, "metadata", _mapping(self.metadata, "metadata"))
131
+
132
+ @classmethod
133
+ def validated(cls, case_id: str, inputs: Mapping[str, Any], *,
134
+ schema: PayloadSchema, metadata: Mapping[str, Any] | None = None) -> CaseView:
135
+ """Validate domain inputs and retain their exact schema identity.
136
+
137
+ Import the schema contract only at this explicit operation, after module
138
+ initialization. Native adapters that already validate their own typed
139
+ payloads can continue using the compatible envelope constructor.
140
+ """
141
+ from .workflow_contracts import PayloadSchema
142
+
143
+ if not isinstance(schema, PayloadSchema):
144
+ raise AssessmentContractError("validated cases require a PayloadSchema")
145
+ validated = schema.validate(inputs)
146
+ return cls(case_id=case_id, schema_version=schema.schema_id, inputs=validated,
147
+ metadata=metadata if metadata is not None else {})
148
+
149
+ def to_dict(self) -> dict[str, Any]:
150
+ return {"case_id": self.case_id, "schema_version": self.schema_version,
151
+ "inputs": _thaw(self.inputs), "metadata": _thaw(self.metadata)}
152
+
153
+
154
+ @dataclass(frozen=True)
155
+ class EvidenceView:
156
+ """A typed reference to native evidence, without loading its bytes.
157
+
158
+ ``source`` names the observer, not a trust decision. ``visibility`` retains
159
+ the backend's native access label; this read view does not grant access.
160
+ Metadata can bind episode order, state lineage, reviewers, measurement
161
+ boundaries, and environment identities under the named domain schema.
162
+ """
163
+
164
+ evidence_id: str
165
+ sha256: str | None
166
+ media_type: str
167
+ role: str
168
+ source: str
169
+ visibility: str
170
+ schema_version: str | None = None
171
+ case_id: str | None = None
172
+ step_id: str | None = None
173
+ availability: str = "available"
174
+ metadata: Mapping[str, Any] = field(default_factory=dict)
175
+
176
+ def __post_init__(self) -> None:
177
+ for name in ("evidence_id", "media_type", "role", "source", "visibility"):
178
+ _text(getattr(self, name), name)
179
+ for name in ("schema_version", "case_id", "step_id"):
180
+ _optional_text(getattr(self, name), name)
181
+ if type(self.availability) is not str or self.availability not in _EVIDENCE_AVAILABILITY:
182
+ raise AssessmentContractError("unsupported evidence availability")
183
+ if self.sha256 is not None and (type(self.sha256) is not str or not _DIGEST.fullmatch(self.sha256)):
184
+ raise AssessmentContractError("sha256 must use sha256: lowercase hex")
185
+ if self.availability == "available" and self.sha256 is None:
186
+ raise AssessmentContractError("available evidence requires its native byte digest")
187
+ object.__setattr__(self, "metadata", _mapping(self.metadata, "metadata"))
188
+
189
+ def to_dict(self) -> dict[str, Any]:
190
+ return {"evidence_id": self.evidence_id, "sha256": self.sha256,
191
+ "media_type": self.media_type, "role": self.role, "source": self.source,
192
+ "visibility": self.visibility, "schema_version": self.schema_version,
193
+ "case_id": self.case_id, "step_id": self.step_id,
194
+ "availability": self.availability, "metadata": _thaw(self.metadata)}
195
+
196
+
197
+ @dataclass(frozen=True)
198
+ class Assessment:
199
+ """One evaluator's assessment of a criterion against retained evidence.
200
+
201
+ Available scores can have any finite scale. Missing evidence is never a
202
+ measured zero. Native run/grading IDs identify the authoritative records;
203
+ this view does not create a new capture or saved grading identity. Use
204
+ ``structured`` for a schema-validated bespoke JSON payload. Its schema
205
+ identity remains explicit; reporting must use an explicitly selected
206
+ projection instead of coercing the payload to a number or string.
207
+ """
208
+
209
+ criterion_id: str
210
+ evaluator_id: str
211
+ case_id: str
212
+ kind: str
213
+ availability: str
214
+ value: Any = None
215
+ reason: str | None = None
216
+ evidence_ids: tuple[str, ...] = ()
217
+ run_id: str | None = None
218
+ grading_id: str | None = None
219
+ trial: int = 1
220
+ attempt: int = 1
221
+ payload_schema: Mapping[str, Any] | None = None
222
+ _schema_token: InitVar[object] = None
223
+
224
+ def __post_init__(self, _schema_token: object) -> None:
225
+ for name in ("criterion_id", "evaluator_id", "case_id"):
226
+ _text(getattr(self, name), name)
227
+ for name in ("reason", "run_id", "grading_id"):
228
+ _optional_text(getattr(self, name), name)
229
+ for name in ("trial", "attempt"):
230
+ if type(getattr(self, name)) is not int or getattr(self, name) < 1:
231
+ raise AssessmentContractError(f"{name} must be a positive integer")
232
+ if type(self.kind) is not str:
233
+ raise AssessmentContractError("assessment kind must be text")
234
+ builtin = self.kind in {"assertion", "score", "label"}
235
+ if not builtin:
236
+ if _schema_token is not _STRUCTURED_ASSESSMENT_TOKEN or self.payload_schema is None:
237
+ raise AssessmentContractError("custom assessment kinds require a validated payload schema")
238
+ if self.kind != "structured" and (not _CUSTOM_KIND.fullmatch(self.kind)
239
+ or ("." not in self.kind and ":" not in self.kind)):
240
+ raise AssessmentContractError("custom assessment kind must be structured or namespaced")
241
+ schema = _mapping(self.payload_schema, "payload_schema")
242
+ if set(schema) != {"schema_id", "version", "implementation_id"}:
243
+ raise AssessmentContractError("payload schema requires its exact identity and implementation")
244
+ for name in ("schema_id", "version", "implementation_id"):
245
+ _text(schema[name], "payload_schema." + name)
246
+ object.__setattr__(self, "payload_schema", schema)
247
+ elif self.payload_schema is not None or _schema_token is not None:
248
+ raise AssessmentContractError("builtin assessments do not accept a custom payload schema")
249
+ if type(self.availability) is not str or self.availability not in _ASSESSMENT_AVAILABILITY:
250
+ raise AssessmentContractError("unsupported assessment availability")
251
+ if self.availability != "available":
252
+ if self.value is not None or self.reason is None:
253
+ raise AssessmentContractError("an unavailable assessment requires a reason and no value")
254
+ elif self.kind == "assertion" and type(self.value) is not bool:
255
+ raise AssessmentContractError("an available assertion requires a boolean")
256
+ elif self.kind == "score":
257
+ if type(self.value) not in (int, float):
258
+ raise AssessmentContractError("an available score requires a finite number")
259
+ try:
260
+ finite = math.isfinite(self.value)
261
+ except OverflowError:
262
+ finite = False
263
+ if not finite:
264
+ raise AssessmentContractError("an available score requires a finite number")
265
+ elif self.kind == "label":
266
+ _text(self.value, "label value")
267
+ if not builtin:
268
+ object.__setattr__(self, "value", _freeze_json(self.value, "assessment payload"))
269
+ object.__setattr__(self, "evidence_ids", _texts(self.evidence_ids, "evidence_ids"))
270
+
271
+ @classmethod
272
+ def structured(cls, criterion_id: str, evaluator_id: str, case_id: str, value: Any, *,
273
+ schema: PayloadSchema, kind: str = "structured", availability: str = "available",
274
+ reason: str | None = None, evidence_ids: tuple[str, ...] = (),
275
+ run_id: str | None = None, grading_id: str | None = None,
276
+ trial: int = 1, attempt: int = 1) -> Assessment:
277
+ """Create a custom assessment after explicit domain-payload validation.
278
+
279
+ Missing or pending values retain the schema declaration but have no
280
+ payload to validate. Their reason remains required. The factory marker
281
+ records this construction path; it is not evidence authentication.
282
+ Projection callbacks remain backend/registry-owned and are not executed
283
+ by construction or serialization.
284
+ """
285
+ from .workflow_contracts import PayloadSchema
286
+
287
+ if not isinstance(schema, PayloadSchema):
288
+ raise AssessmentContractError("structured assessments require a PayloadSchema")
289
+ validated = schema.validate(value) if availability == "available" else value
290
+ return cls(
291
+ criterion_id=criterion_id, evaluator_id=evaluator_id, case_id=case_id,
292
+ kind=kind, availability=availability, value=validated, reason=reason,
293
+ evidence_ids=evidence_ids, run_id=run_id, grading_id=grading_id,
294
+ trial=trial, attempt=attempt,
295
+ payload_schema={"schema_id": schema.schema_id, "version": schema.version,
296
+ "implementation_id": schema.implementation_id},
297
+ _schema_token=_STRUCTURED_ASSESSMENT_TOKEN,
298
+ )
299
+
300
+ def to_dict(self) -> dict[str, Any]:
301
+ data = {"schema_version": ASSESSMENT_VIEW_SCHEMA,
302
+ "criterion_id": self.criterion_id, "evaluator_id": self.evaluator_id,
303
+ "case_id": self.case_id, "kind": self.kind,
304
+ "availability": self.availability, "value": _thaw(self.value), "reason": self.reason,
305
+ "evidence_ids": list(self.evidence_ids), "run_id": self.run_id,
306
+ "grading_id": self.grading_id, "trial": self.trial, "attempt": self.attempt}
307
+ if self.payload_schema is not None:
308
+ data["schema_version"] = STRUCTURED_ASSESSMENT_VIEW_SCHEMA
309
+ data["payload_schema"] = _thaw(self.payload_schema)
310
+ return data
311
+
312
+
313
+ @dataclass(frozen=True, init=False)
314
+ class EvaluatorContext:
315
+ """Grader-owned access to verified data and permitted references.
316
+
317
+ Do not pass this object to a candidate or include it in a public report.
318
+ ``candidate_case`` returns only the backend's candidate-permitted projection.
319
+ No general serializer is provided because references can be protected.
320
+ """
321
+
322
+ case: CaseView
323
+ outputs: Mapping[str, Any] = field(repr=False)
324
+ references: Mapping[str, Any] = field(repr=False)
325
+ evidence: tuple[EvidenceView, ...] = field(repr=False)
326
+ run_id: str
327
+ grading_id: str | None
328
+ trust_tier: str
329
+ verified_by: str
330
+ metadata: Mapping[str, Any] = field(repr=False)
331
+
332
+ def __init__(self, *, case: CaseView, outputs: Mapping[str, Any],
333
+ references: Mapping[str, Any], evidence: Sequence[EvidenceView] = (),
334
+ run_id: str, grading_id: str | None = None, trust_tier: str,
335
+ verified_by: str, metadata: Mapping[str, Any] | None = None,
336
+ _token: object = None) -> None:
337
+ if _token is not _CONTEXT_TOKEN:
338
+ raise AssessmentContractError("evaluator contexts must be built inside a verified backend")
339
+ if not isinstance(case, CaseView):
340
+ raise AssessmentContractError("case must be CaseView")
341
+ if isinstance(evidence, (str, bytes)) or not isinstance(evidence, Sequence):
342
+ raise AssessmentContractError("evidence must be a sequence of EvidenceView values")
343
+ evidence = tuple(evidence)
344
+ if any(not isinstance(item, EvidenceView) for item in evidence):
345
+ raise AssessmentContractError("evidence must contain EvidenceView values")
346
+ if len({item.evidence_id for item in evidence}) != len(evidence):
347
+ raise AssessmentContractError("evidence identities must be unique within a context")
348
+ if any(item.case_id is not None and item.case_id != case.case_id for item in evidence):
349
+ raise AssessmentContractError("evidence case binding differs from the context")
350
+ for name, value in (("run_id", run_id), ("trust_tier", trust_tier), ("verified_by", verified_by)):
351
+ _text(value, name)
352
+ _optional_text(grading_id, "grading_id")
353
+ values = {"case": case, "outputs": _mapping(outputs, "outputs"),
354
+ "references": _mapping(references, "references"), "evidence": evidence,
355
+ "run_id": run_id, "grading_id": grading_id, "trust_tier": trust_tier,
356
+ "verified_by": verified_by,
357
+ "metadata": _mapping(metadata if metadata is not None else {}, "metadata")}
358
+ for name, value in values.items():
359
+ object.__setattr__(self, name, value)
360
+
361
+ def candidate_case(self) -> CaseView:
362
+ return self.case
363
+
364
+
365
+ def _verified_evaluator_context(*, case: CaseView, outputs: Mapping[str, Any],
366
+ references: Mapping[str, Any], evidence: Sequence[EvidenceView] = (),
367
+ run_id: str, grading_id: str | None = None,
368
+ trust_tier: str, verified_by: str,
369
+ metadata: Mapping[str, Any] | None = None) -> EvaluatorContext:
370
+ """Build after native verification and reference authorization, inside grading.
371
+
372
+ The caller owns those checks. This helper does not read storage, verify
373
+ hashes, authenticate a principal, or upgrade the native evidence's trust.
374
+ """
375
+ return EvaluatorContext(case=case, outputs=outputs, references=references,
376
+ evidence=evidence, run_id=run_id, grading_id=grading_id,
377
+ trust_tier=trust_tier, verified_by=verified_by,
378
+ metadata=metadata, _token=_CONTEXT_TOKEN)
@@ -0,0 +1,57 @@
1
+ """Lazy exact-match selection of assessments already permitted by a backend."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Iterable, Iterator
5
+ from dataclasses import dataclass
6
+
7
+ from .assessment_contracts import Assessment, _ASSESSMENT_AVAILABILITY
8
+ from .workflow_contracts import WorkflowContractError, _text
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class AssessmentSelector:
13
+ """Optional envelope filters combined with AND.
14
+
15
+ Selection does not interpret values, execute projections, group trials, or
16
+ grant access. Custom schema-bound kinds retain their original payloads.
17
+ """
18
+
19
+ case_id: str | None = None
20
+ criterion_id: str | None = None
21
+ evaluator_id: str | None = None
22
+ kind: str | None = None
23
+ availability: str | None = None
24
+ trial: int | None = None
25
+ attempt: int | None = None
26
+
27
+ def __post_init__(self) -> None:
28
+ for name in ("case_id", "criterion_id", "evaluator_id", "kind", "availability"):
29
+ value = getattr(self, name)
30
+ if value is not None:
31
+ _text(value, name)
32
+ if self.availability is not None and self.availability not in _ASSESSMENT_AVAILABILITY:
33
+ raise WorkflowContractError("unsupported assessment availability")
34
+ for name in ("trial", "attempt"):
35
+ value = getattr(self, name)
36
+ if value is not None and (type(value) is not int or value < 1):
37
+ raise WorkflowContractError(name + " must be a positive integer")
38
+
39
+ def matches(self, assessment: Assessment) -> bool:
40
+ if not isinstance(assessment, Assessment):
41
+ raise WorkflowContractError("assessment selection requires Assessment values")
42
+ return all(
43
+ getattr(self, name) is None or getattr(assessment, name) == getattr(self, name)
44
+ for name in ("case_id", "criterion_id", "evaluator_id", "kind", "availability", "trial", "attempt")
45
+ )
46
+
47
+ def select(self, assessments: Iterable[Assessment]) -> Iterator[Assessment]:
48
+ """Yield matching original views without materializing the input."""
49
+ for assessment in assessments:
50
+ if self.matches(assessment):
51
+ yield assessment
52
+
53
+ def to_dict(self) -> dict:
54
+ return {"schema_version": "sphynx.assessment-selector.v1", **{
55
+ name: getattr(self, name)
56
+ for name in ("case_id", "criterion_id", "evaluator_id", "kind", "availability", "trial", "attempt")
57
+ }}