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,370 @@
1
+ """Versioned evaluator requirements and explicit report-scoped assessments.
2
+
3
+ Declarations grant no access. Trusted backends verify native records, authorize
4
+ references and select exact stage evidence before creating grading contexts.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Mapping
9
+ from dataclasses import dataclass, field
10
+ import math
11
+ import re
12
+ from typing import Any
13
+
14
+ from .assessment_contracts import (
15
+ Assessment, EvaluatorContext, _ASSESSMENT_AVAILABILITY, _freeze_json,
16
+ )
17
+ from .evaluation_protocols import VersionedIdentity, _envelope
18
+ from .stage_contracts import StageEvidence, StageSelector
19
+ from .workflow_contracts import (
20
+ PayloadSchema, WorkflowContractError, _object, _optional_text, _text, _texts,
21
+ _thaw, contract_digest,
22
+ )
23
+
24
+ BINDING_SCHEMA = "sphynx.evaluator-binding.v1"
25
+ REQUIREMENT_SCHEMA = "sphynx.evaluator-input.v1"
26
+ REPORT_ASSESSMENT_SCHEMA = "sphynx.report-assessment.v1"
27
+ REPORT_ASSESSMENTS_CAPABILITY = "sphynx.report-assessments.v1"
28
+ _REPORT_CONTEXT_TOKEN = object()
29
+
30
+
31
+ def _identity(value: Any, name: str) -> None:
32
+ if not isinstance(value, VersionedIdentity) or value.digest is None:
33
+ raise WorkflowContractError(name + " requires an exact version and digest")
34
+
35
+
36
+ def _schema_identity(schema: PayloadSchema) -> VersionedIdentity:
37
+ return VersionedIdentity(schema.schema_id, schema.version, contract_digest(schema.to_dict()))
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class InputRequirement:
42
+ source: str
43
+ schema: VersionedIdentity
44
+ fields: tuple[str, ...] = ()
45
+ optional: bool = False
46
+
47
+ def __post_init__(self) -> None:
48
+ if self.source not in ("inputs", "outputs", "references"):
49
+ raise WorkflowContractError("requirement source must be inputs, outputs or references")
50
+ _identity(self.schema, "input schema")
51
+ object.__setattr__(self, "fields", _texts(self.fields, "required fields"))
52
+ if type(self.optional) is not bool:
53
+ raise WorkflowContractError("optional must be boolean")
54
+
55
+ @classmethod
56
+ def for_schema(cls, source: str, schema: PayloadSchema, *, fields=(), optional=False):
57
+ if not isinstance(schema, PayloadSchema):
58
+ raise WorkflowContractError("input requirement needs PayloadSchema")
59
+ return cls(source, _schema_identity(schema), fields, optional)
60
+
61
+ def to_dict(self) -> dict[str, Any]:
62
+ return {"schema_version": REQUIREMENT_SCHEMA, "source": self.source,
63
+ "schema": self.schema.to_dict(), "fields": list(self.fields), "optional": self.optional}
64
+
65
+ @classmethod
66
+ def from_dict(cls, value):
67
+ values = _envelope(value, REQUIREMENT_SCHEMA, cls)
68
+ if "schema" in values:
69
+ values["schema"] = VersionedIdentity.from_dict(values["schema"])
70
+ try:
71
+ return cls(**values)
72
+ except TypeError as exc:
73
+ raise WorkflowContractError("invalid input requirement fields") from exc
74
+
75
+
76
+ @dataclass(frozen=True)
77
+ class EvaluatorBinding:
78
+ binding_id: str
79
+ version: str
80
+ evaluator: VersionedIdentity
81
+ target: str
82
+ requirements: tuple[InputRequirement, ...] = ()
83
+ stage_id: str | None = None
84
+ occurrence: int = 1
85
+ stage_schema: VersionedIdentity | None = None
86
+
87
+ def __post_init__(self) -> None:
88
+ _text(self.binding_id, "binding_id"); _text(self.version, "binding version")
89
+ _identity(self.evaluator, "evaluator")
90
+ if self.target not in ("case", "stage", "report"):
91
+ raise WorkflowContractError("binding target must be case, stage or report")
92
+ if not isinstance(self.requirements, (list, tuple)) or any(
93
+ not isinstance(item, InputRequirement) for item in self.requirements
94
+ ):
95
+ raise WorkflowContractError("requirements must contain InputRequirement values")
96
+ requirements = tuple(self.requirements)
97
+ if len({item.source for item in requirements}) != len(requirements):
98
+ raise WorkflowContractError("declare one requirement per input source")
99
+ if self.target == "report" and any(item.source == "inputs" for item in requirements):
100
+ raise WorkflowContractError("report targets have no case inputs")
101
+ object.__setattr__(self, "requirements", requirements)
102
+ if type(self.occurrence) is not int or self.occurrence < 1:
103
+ raise WorkflowContractError("stage occurrence must be a positive integer")
104
+ if self.target == "stage":
105
+ _text(self.stage_id, "stage_id"); _identity(self.stage_schema, "stage schema")
106
+ elif self.stage_id is not None or self.stage_schema is not None or self.occurrence != 1:
107
+ raise WorkflowContractError("only stage bindings accept stage selectors")
108
+
109
+ @classmethod
110
+ def for_evaluator(cls, binding_id: str, version: str, definition, *, target="case", **kwargs):
111
+ from .workflow_contracts import EvaluatorDefinition
112
+ if not isinstance(definition, EvaluatorDefinition):
113
+ raise WorkflowContractError("binding requires EvaluatorDefinition")
114
+ identity = VersionedIdentity(definition.evaluator_id, definition.version,
115
+ contract_digest(definition.to_dict()))
116
+ return cls(binding_id, version, identity, target, **kwargs)
117
+
118
+ @property
119
+ def binding_digest(self) -> str:
120
+ return contract_digest(self.to_dict())
121
+
122
+ def selector(self, context: EvaluatorContext) -> StageSelector:
123
+ if self.target != "stage" or not isinstance(context, EvaluatorContext):
124
+ raise WorkflowContractError("stage selector requires a case evaluator context")
125
+ return StageSelector(context.case.case_id, context.metadata["trial"],
126
+ context.metadata["attempt"], self.stage_id, self.occurrence)
127
+
128
+ def to_dict(self) -> dict[str, Any]:
129
+ return {"schema_version": BINDING_SCHEMA, "binding_id": self.binding_id, "version": self.version,
130
+ "evaluator": self.evaluator.to_dict(), "target": self.target,
131
+ "requirements": [item.to_dict() for item in self.requirements],
132
+ "stage_id": self.stage_id, "occurrence": self.occurrence,
133
+ "stage_schema": None if self.stage_schema is None else self.stage_schema.to_dict()}
134
+
135
+ @classmethod
136
+ def from_dict(cls, value):
137
+ values = _envelope(value, BINDING_SCHEMA, cls)
138
+ if "evaluator" in values:
139
+ values["evaluator"] = VersionedIdentity.from_dict(values["evaluator"])
140
+ if "requirements" in values:
141
+ raw = values["requirements"]
142
+ if not isinstance(raw, (list, tuple)):
143
+ raise WorkflowContractError("requirements must be a sequence")
144
+ values["requirements"] = tuple(InputRequirement.from_dict(item) for item in raw)
145
+ if values.get("stage_schema") is not None:
146
+ values["stage_schema"] = VersionedIdentity.from_dict(values["stage_schema"])
147
+ try:
148
+ return cls(**values)
149
+ except TypeError as exc:
150
+ raise WorkflowContractError("invalid evaluator binding fields") from exc
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class ReportAssessment:
155
+ """A report-scoped result. It has no case, trial or candidate-attempt ID."""
156
+
157
+ criterion_id: str
158
+ evaluator_id: str
159
+ run_id: str
160
+ grading_id: str
161
+ kind: str
162
+ availability: str
163
+ value: Any = None
164
+ reason: str | None = None
165
+ evidence_ids: tuple[str, ...] = ()
166
+ schema: PayloadSchema | None = field(default=None, repr=False, compare=False)
167
+
168
+ def __post_init__(self) -> None:
169
+ for name in ("criterion_id", "evaluator_id", "run_id", "grading_id", "kind"):
170
+ _text(getattr(self, name), name)
171
+ _optional_text(self.reason, "reason")
172
+ object.__setattr__(self, "evidence_ids", _texts(self.evidence_ids, "evidence_ids"))
173
+ if type(self.availability) is not str or self.availability not in _ASSESSMENT_AVAILABILITY:
174
+ raise WorkflowContractError("unsupported report assessment availability")
175
+ builtin = self.kind in ("score", "assertion", "label")
176
+ if builtin and self.schema is not None:
177
+ raise WorkflowContractError("builtin report assessments do not accept a custom schema")
178
+ if not builtin:
179
+ if not isinstance(self.schema, PayloadSchema) or (
180
+ self.kind != "structured" and not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.:/@+\-]*[.:][A-Za-z0-9_.:/@+\-]+", self.kind)
181
+ ):
182
+ raise WorkflowContractError("custom report assessment requires a schema and namespaced kind")
183
+ if self.availability != "available":
184
+ if self.value is not None or self.reason is None:
185
+ raise WorkflowContractError("unavailable report assessment requires reason and no value")
186
+ elif self.kind == "assertion":
187
+ if type(self.value) is not bool:
188
+ raise WorkflowContractError("available report assertion requires boolean")
189
+ elif self.kind == "score":
190
+ try:
191
+ finite = type(self.value) in (int, float) and math.isfinite(self.value)
192
+ except OverflowError:
193
+ finite = False
194
+ if not finite:
195
+ raise WorkflowContractError("available report score requires finite number")
196
+ elif self.kind == "label":
197
+ _text(self.value, "report label")
198
+ else:
199
+ object.__setattr__(self, "value", self.schema.validate(self.value))
200
+
201
+ def to_dict(self) -> dict[str, Any]:
202
+ return {"schema_version": REPORT_ASSESSMENT_SCHEMA, "scope": "report",
203
+ "criterion_id": self.criterion_id, "evaluator_id": self.evaluator_id,
204
+ "run_id": self.run_id, "grading_id": self.grading_id,
205
+ "kind": self.kind, "availability": self.availability,
206
+ "value": _thaw(self.value), "reason": self.reason, "evidence_ids": list(self.evidence_ids),
207
+ "payload_schema": None if self.schema is None else {
208
+ "schema_id": self.schema.schema_id, "version": self.schema.version,
209
+ "implementation_id": self.schema.implementation_id}}
210
+
211
+
212
+ @dataclass(frozen=True, init=False)
213
+ class ReportEvaluatorContext:
214
+ """Grader-only report context, deliberately without a general serializer."""
215
+
216
+ outputs: Mapping[str, Any] = field(repr=False)
217
+ references: Mapping[str, Any] = field(repr=False)
218
+ run_id: str
219
+ grading_id: str
220
+ trust_tier: str
221
+ verified_by: str
222
+ metadata: Mapping[str, Any] = field(repr=False)
223
+ evidence_ids: tuple[str, ...]
224
+
225
+ def __init__(self, *, outputs, references, run_id, grading_id, trust_tier,
226
+ verified_by, metadata=None, evidence_ids=(), _token=None):
227
+ if _token is not _REPORT_CONTEXT_TOKEN:
228
+ raise WorkflowContractError("report context must be built inside a verified backend")
229
+ for name, value in (("run_id", run_id), ("grading_id", grading_id),
230
+ ("trust_tier", trust_tier), ("verified_by", verified_by)):
231
+ _text(value, name); object.__setattr__(self, name, value)
232
+ object.__setattr__(self, "outputs", _object(outputs, "report outputs"))
233
+ object.__setattr__(self, "references", _object(references, "report references"))
234
+ object.__setattr__(self, "metadata", _object({} if metadata is None else metadata, "report metadata"))
235
+ object.__setattr__(self, "evidence_ids", _texts(evidence_ids, "report evidence_ids"))
236
+
237
+
238
+ def _verified_report_context(**kwargs) -> ReportEvaluatorContext:
239
+ # The trusted caller owns native verification and reference authorization.
240
+ return ReportEvaluatorContext(**kwargs, _token=_REPORT_CONTEXT_TOKEN)
241
+
242
+
243
+ class EvaluatorInputUnavailable(WorkflowContractError):
244
+ """Required saved input is unavailable; this is not a quality score."""
245
+
246
+
247
+ def _registered_schema(registry, identity: VersionedIdentity) -> PayloadSchema:
248
+ schema = registry.get_schema(identity.identity_id)
249
+ if _schema_identity(schema) != identity:
250
+ raise WorkflowContractError("evaluator input schema identity changed")
251
+ return schema
252
+
253
+
254
+ def _binding_dependencies(registry, binding: EvaluatorBinding) -> dict[str, Any]:
255
+ if not isinstance(binding, EvaluatorBinding):
256
+ raise WorkflowContractError("grading binding must be EvaluatorBinding")
257
+ definition = registry.get_evaluator(binding.evaluator.identity_id)
258
+ current = VersionedIdentity(definition.evaluator_id, definition.version,
259
+ contract_digest(definition.to_dict()))
260
+ if current != binding.evaluator:
261
+ raise WorkflowContractError("bound evaluator identity changed")
262
+ schemas = [_registered_schema(registry, requirement.schema).to_dict()
263
+ for requirement in binding.requirements]
264
+ if binding.stage_schema is not None:
265
+ schemas.append(_registered_schema(registry, binding.stage_schema).to_dict())
266
+ schemas.extend(registry.get_schema(identifier).to_dict() for identifier in definition.assessment_schema_ids)
267
+ return {"binding": binding.to_dict(), "evaluator": definition.to_dict(), "schemas": schemas}
268
+
269
+
270
+ def _validate_custom_result(registry, definition, row) -> None:
271
+ identity = row.payload_schema if isinstance(row, Assessment) else (
272
+ None if row.schema is None else {"schema_id": row.schema.schema_id,
273
+ "version": row.schema.version,
274
+ "implementation_id": row.schema.implementation_id})
275
+ if identity is None:
276
+ return
277
+ if identity["schema_id"] not in definition.assessment_schema_ids:
278
+ raise WorkflowContractError("evaluator returned an undeclared custom assessment schema")
279
+ schema = registry.get_schema(identity["schema_id"])
280
+ if any(getattr(schema, key) != identity[key] for key in ("schema_id", "version", "implementation_id")):
281
+ raise WorkflowContractError("custom assessment schema identity changed")
282
+ if row.availability == "available":
283
+ schema.validate(_thaw(row.value))
284
+
285
+
286
+ def _dispatch_binding(registry, binding: EvaluatorBinding, context, *, stage=None):
287
+ """Use only inside grading after native verification and authorization.
288
+
289
+ A stage reader must stamp the verified run/trial/attempt into EvidenceView
290
+ metadata. This checks joins; it does not authenticate an arbitrary caller.
291
+ """
292
+ _binding_dependencies(registry, binding)
293
+ report_target = binding.target == "report"
294
+ if not isinstance(context, ReportEvaluatorContext if report_target else EvaluatorContext):
295
+ raise WorkflowContractError("binding target and trusted context differ")
296
+ if binding.target == "stage":
297
+ from .assessment_contracts import _verified_evaluator_context
298
+ selector = binding.selector(context)
299
+ if not isinstance(stage, StageEvidence) or stage.selector != selector:
300
+ raise WorkflowContractError("stage evidence selector differs from grading selection")
301
+ identity = stage.evidence.metadata
302
+ if (identity.get("run_id"), identity.get("trial"), identity.get("attempt")) != (
303
+ context.run_id, selector.trial, selector.attempt
304
+ ):
305
+ raise WorkflowContractError("stage evidence run/trial/attempt binding differs")
306
+ # Native artifact schema and permitted projection schema are distinct.
307
+ if stage.availability == "available":
308
+ schema = _registered_schema(registry, binding.stage_schema)
309
+ if any(stage.projection_schema.get(key) != getattr(schema, key)
310
+ for key in ("schema_id", "version", "implementation_id")):
311
+ raise WorkflowContractError("stage projection schema identity differs")
312
+ outputs = schema.validate(_thaw(stage.projection))
313
+ if not isinstance(outputs, Mapping):
314
+ raise WorkflowContractError("case evaluator context requires a mapping stage projection")
315
+ else:
316
+ outputs = {}
317
+ context = _verified_evaluator_context(
318
+ case=context.case, outputs=outputs, references=context.references,
319
+ evidence=(stage.evidence,), run_id=context.run_id, grading_id=context.grading_id,
320
+ trust_tier=context.trust_tier, verified_by=context.verified_by,
321
+ metadata={**_thaw(context.metadata), "stage_selector": selector.to_dict(),
322
+ "binding_availability": {"outputs": stage.availability},
323
+ "binding_missing_reason": stage.reason})
324
+ elif stage is not None:
325
+ raise WorkflowContractError("non-stage binding does not accept stage evidence")
326
+
327
+ missing = {}
328
+ for requirement in binding.requirements:
329
+ schema = _registered_schema(registry, requirement.schema)
330
+ value = context.case.inputs if requirement.source == "inputs" else getattr(context, requirement.source)
331
+ availability = context.metadata.get("binding_availability", {}).get(requirement.source, "available")
332
+ absent = [name for name in requirement.fields if name not in value]
333
+ if availability != "available" or absent:
334
+ if not requirement.optional:
335
+ raise EvaluatorInputUnavailable("required evaluator " + requirement.source + " unavailable")
336
+ missing[requirement.source] = "unavailable" if availability != "available" else "required_fields_absent"
337
+ continue
338
+ if requirement.source == "inputs" and context.case.schema_version != schema.schema_id:
339
+ raise WorkflowContractError("case input schema differs from evaluator requirement")
340
+ schema.validate(_thaw(value))
341
+ if missing:
342
+ if report_target:
343
+ context = _verified_report_context(outputs=context.outputs, references=context.references,
344
+ run_id=context.run_id, grading_id=context.grading_id, trust_tier=context.trust_tier,
345
+ verified_by=context.verified_by, evidence_ids=context.evidence_ids,
346
+ metadata={**_thaw(context.metadata), "binding_missing": missing})
347
+ else:
348
+ from .assessment_contracts import _verified_evaluator_context
349
+ context = _verified_evaluator_context(case=context.case, outputs=context.outputs,
350
+ references=context.references, evidence=context.evidence, run_id=context.run_id,
351
+ grading_id=context.grading_id, trust_tier=context.trust_tier, verified_by=context.verified_by,
352
+ metadata={**_thaw(context.metadata), "binding_missing": missing})
353
+ definition = registry.get_evaluator(binding.evaluator.identity_id)
354
+ rows = tuple(registry.get_evaluator_callable(definition.evaluator_id)(context))
355
+ evidence_ids = set(context.evidence_ids if report_target else (item.evidence_id for item in context.evidence))
356
+ for row in rows:
357
+ if not isinstance(row, ReportAssessment if report_target else Assessment):
358
+ raise WorkflowContractError("evaluator returned the wrong assessment scope")
359
+ if (row.evaluator_id, row.run_id, row.grading_id) != (
360
+ definition.evaluator_id, context.run_id, context.grading_id
361
+ ) or row.criterion_id not in definition.criterion_ids or not set(row.evidence_ids) <= evidence_ids:
362
+ raise WorkflowContractError("bound assessment identity or evidence differs")
363
+ if not report_target and (row.case_id, row.trial, row.attempt) != (
364
+ context.case.case_id, context.metadata["trial"], context.metadata["attempt"]
365
+ ):
366
+ raise WorkflowContractError("bound assessment case/trial/attempt differs")
367
+ if binding.target == "stage" and stage.evidence.evidence_id not in row.evidence_ids:
368
+ raise WorkflowContractError("stage assessment must retain its selected stage evidence ID")
369
+ _validate_custom_result(registry, definition, row)
370
+ return rows