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,560 @@
1
+ """Permitted PR coordination values; private rules and source bodies stay on the service."""
2
+ from __future__ import annotations
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass, field, fields
5
+ from datetime import datetime
6
+ import re
7
+ from types import MappingProxyType
8
+ from typing import Any, ClassVar, cast
9
+
10
+ from ._analytical_values import (_Values, require, keys, identifier, identity, digest, count,
11
+ ReportProjectionSummary, MetricProjectionSummary, sha, frame, score_bits, text)
12
+ from .analytical_views import ProjectionProof, _cell, _details
13
+ from .evaluation_protocols import VersionedIdentity
14
+ from .workflow_contracts import WorkflowContractError
15
+
16
+ _PR = re.compile(r'https://github\.com/([A-Za-z0-9][A-Za-z0-9-]*)/([A-Za-z0-9][A-Za-z0-9_.-]*)/pull/([1-9][0-9]*)\Z')
17
+ _SHA = re.compile(r'[0-9a-f]{40}\Z')
18
+ _METHOD = re.compile(r'[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\Z')
19
+ _CODE = re.compile(r'[a-z_]{1,64}\Z')
20
+ _STATES = {'pending','preparing','queued','running','grading','publishing','completed','incomplete','failed','unsupported'}
21
+
22
+
23
+ def _saved(value: Any, prefix: str) -> str:
24
+ require(type(value) is str and re.fullmatch(prefix+r'_[0-9a-f]{32}', value) is not None,
25
+ 'invalid PR operation identity')
26
+ return value
27
+
28
+
29
+ def _commit(value: Any) -> str:
30
+ require(type(value) is str and _SHA.fullmatch(value) is not None, 'source revision needs a full lowercase 40-character commit SHA')
31
+ return value
32
+
33
+
34
+ def _pr(value: Any) -> str:
35
+ require(type(value) is str and len(value)<=512, 'use a canonical HTTPS GitHub PR URL')
36
+ matched = _PR.fullmatch(value)
37
+ require(matched is not None, 'use a canonical HTTPS GitHub PR URL without a query or trailing slash')
38
+ assert matched is not None
39
+ require(int(matched[3])<=2**63-1, 'PR number exceeds its supported range')
40
+ return value
41
+
42
+
43
+ def _boolean(value: Any) -> bool:
44
+ require(type(value) is bool, 'PR state needs an explicit boolean')
45
+ return value
46
+
47
+
48
+ def _code(value: Any) -> str:
49
+ require(type(value) is str and _CODE.fullmatch(value) is not None, 'invalid PR state code')
50
+ return value
51
+
52
+
53
+ def _exact_id(value: Any) -> VersionedIdentity:
54
+ require(isinstance(value, VersionedIdentity)); digest(value.digest)
55
+ return value
56
+
57
+
58
+ def _time(value: Any) -> datetime:
59
+ require(type(value) is str and len(value)<=64, 'invalid PR timestamp')
60
+ try:
61
+ result=datetime.fromisoformat(value.replace('Z','+00:00'))
62
+ require(result.tzinfo is not None and result.utcoffset() is not None, 'PR timestamp needs a timezone')
63
+ return result
64
+ except ValueError:
65
+ raise WorkflowContractError('invalid PR timestamp') from None
66
+
67
+
68
+ class _PrValues(_Values):
69
+ SCHEMA: ClassVar[str | None] = None
70
+ def to_dict(self) -> dict[str, Any]:
71
+ value=super().to_dict()
72
+ return value if self.SCHEMA is None else {'schema_version':self.SCHEMA, **value}
73
+ @classmethod
74
+ def _fields(cls, value: Mapping[str, Any]) -> dict[str, Any]:
75
+ names={item.name for item in fields(cast(Any,cls))}
76
+ result=keys(value, names if cls.SCHEMA is None else names|{'schema_version'})
77
+ if cls.SCHEMA is not None: require(result.pop('schema_version')==cls.SCHEMA, 'unsupported PR response version')
78
+ return result
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class PrEvaluationRequest(_PrValues):
83
+ """Constrain the server-selected private template and both exact PR revisions."""
84
+ template_id: str
85
+ candidate_pr: str
86
+ expected_base_sha: str
87
+ expected_head_sha: str
88
+ candidate_method: str | None = None
89
+ def __post_init__(self) -> None:
90
+ identifier(self.template_id); _pr(self.candidate_pr)
91
+ for name in ('expected_base_sha','expected_head_sha'):
92
+ value=getattr(self,name)
93
+ require(type(value) is str, 'source revision needs a complete hexadecimal commit SHA')
94
+ object.__setattr__(self,name,_commit(value.lower()))
95
+ require(self.candidate_method is None or type(self.candidate_method) is str and _METHOD.fullmatch(self.candidate_method) is not None,
96
+ 'candidate_method needs a stable method identifier')
97
+ @classmethod
98
+ def from_dict(cls, value: Mapping[str, Any]) -> PrEvaluationRequest:
99
+ result=cls(**cls._fields(value))
100
+ require(result.to_dict()==dict(value), 'PR response changed its canonical request')
101
+ return result
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class PrSourceRevisions(_PrValues):
106
+ SCHEMA: ClassVar[str] = 'sphynx.private.pr-revisions.v1'
107
+ candidate_pr: str
108
+ base_repository: str
109
+ base_repository_id: int
110
+ head_repository: str
111
+ head_repository_id: int
112
+ base_sha: str
113
+ head_sha: str
114
+ base_tree_sha: str
115
+ head_tree_sha: str
116
+ def __post_init__(self) -> None:
117
+ _pr(self.candidate_pr)
118
+ for repository in (self.base_repository,self.head_repository):
119
+ require(type(repository) is str and re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9-]*/[A-Za-z0-9][A-Za-z0-9_.-]*',repository) is not None)
120
+ for number in (self.base_repository_id,self.head_repository_id): require(count(number,nullable=False,maximum=2**53-1)!=0)
121
+ for name in ('base_sha','head_sha','base_tree_sha','head_tree_sha'): _commit(getattr(self,name))
122
+ @classmethod
123
+ def from_dict(cls, value: Mapping[str,Any]) -> PrSourceRevisions: return cls(**cls._fields(value))
124
+
125
+
126
+ @dataclass(frozen=True)
127
+ class PrOperationError(_PrValues):
128
+ stage: str
129
+ code: str
130
+ def __post_init__(self) -> None:
131
+ require(self.stage=='coordination'); _code(self.code)
132
+ @classmethod
133
+ def from_dict(cls, value: Mapping[str,Any]) -> PrOperationError: return cls(**cls._fields(value))
134
+
135
+
136
+ @dataclass(frozen=True)
137
+ class PrRequirementStatus(_PrValues):
138
+ requirement_id: str
139
+ suite_id: str
140
+ scope: str
141
+ mode: str
142
+ bank_id: str
143
+ release_id: str
144
+ cohort: VersionedIdentity
145
+ state: str
146
+ error_code: str | None
147
+ def __post_init__(self) -> None:
148
+ for name in ('requirement_id','suite_id','bank_id','release_id'): identifier(getattr(self,name))
149
+ require(self.scope in ('component','subsystem','system') and self.mode in ('local','hosted'))
150
+ _exact_id(self.cohort); require(self.state in _STATES)
151
+ if self.error_code is not None: _code(self.error_code)
152
+ @classmethod
153
+ def from_dict(cls, value: Mapping[str,Any]) -> PrRequirementStatus:
154
+ values=cls._fields(value);values['cohort']=identity(values['cohort']);return cls(**values)
155
+
156
+
157
+ @dataclass(frozen=True)
158
+ class PrEvaluationStatus(_PrValues):
159
+ SCHEMA: ClassVar[str] = 'sphynx.pr-evaluation.v1'
160
+ id: str
161
+ status: str
162
+ authority: str
163
+ request: PrEvaluationRequest = field(repr=False)
164
+ template: VersionedIdentity
165
+ revisions: PrSourceRevisions | None
166
+ plan_digest: str | None
167
+ expected_children: int
168
+ decision_id: str | None
169
+ error: PrOperationError | None
170
+ deadline_at: str
171
+ created_at: str
172
+ updated_at: str
173
+ requirements: tuple[PrRequirementStatus,...] = field(repr=False)
174
+ def __post_init__(self) -> None:
175
+ _saved(self.id,'preval'); require(self.authority=='current_operation_status_not_acceptance')
176
+ require(self.status in ('preparing','running','waiting_for_reports','waiting_for_review','evaluating','completed','failed'))
177
+ require(isinstance(self.request,PrEvaluationRequest)); _exact_id(self.template)
178
+ require(self.template.identity_id==self.request.template_id)
179
+ require(count(self.expected_children,nullable=False,maximum=64)!=0)
180
+ require(isinstance(self.requirements,(tuple,list)) and len(self.requirements)==self.expected_children)
181
+ require(all(isinstance(item,PrRequirementStatus) for item in self.requirements))
182
+ require(len({item.requirement_id for item in self.requirements})==len(self.requirements))
183
+ object.__setattr__(self,'requirements',tuple(self.requirements))
184
+ require(self.error is None or isinstance(self.error,PrOperationError))
185
+ if self.revisions is None: require(self.plan_digest is None)
186
+ else:
187
+ require(isinstance(self.revisions,PrSourceRevisions));digest(self.plan_digest)
188
+ require((self.revisions.candidate_pr,self.revisions.base_sha,self.revisions.head_sha)==
189
+ (self.request.candidate_pr,self.request.expected_base_sha,self.request.expected_head_sha))
190
+ require(self.revisions.base_repository.lower()==self.request.candidate_pr.removeprefix('https://github.com/').split('/pull/')[0].lower())
191
+ if self.decision_id is not None: _saved(self.decision_id,'prdec');require(self.status=='completed')
192
+ if self.status=='completed': require(self.decision_id is not None and self.revisions is not None)
193
+ require(_time(self.deadline_at)>_time(self.created_at) and _time(self.updated_at)>=_time(self.created_at))
194
+ @classmethod
195
+ def from_dict(cls, value: Mapping[str,Any]) -> PrEvaluationStatus:
196
+ values=cls._fields(value);values['request']=PrEvaluationRequest.from_dict(values['request']);values['template']=identity(values['template'])
197
+ if values['revisions'] is not None:values['revisions']=PrSourceRevisions.from_dict(values['revisions'])
198
+ if values['error'] is not None:values['error']=PrOperationError.from_dict(values['error'])
199
+ require(type(values['requirements']) is list and len(values['requirements'])<=64)
200
+ values['requirements']=tuple(PrRequirementStatus.from_dict(item) for item in values['requirements']);return cls(**values)
201
+
202
+
203
+ @dataclass(frozen=True)
204
+ class PrDecisionReceipt(_PrValues):
205
+ """Historical decision evidence; current eligibility is a separate field."""
206
+ SCHEMA: ClassVar[str] = 'sphynx.pr-decision-receipt.v1'
207
+ decision_id: str
208
+ plan_id: str
209
+ policy: VersionedIdentity
210
+ inputs_digest: str
211
+ outcome: str
212
+ passed: bool | None
213
+ reason_code: str
214
+ authority: str
215
+ def __post_init__(self) -> None:
216
+ _saved(self.decision_id,'prdec');_saved(self.plan_id,'preval');_exact_id(self.policy);digest(self.inputs_digest)
217
+ require(self.passed is None or type(self.passed) is bool)
218
+ allowed={'passed':(True,{'policy_passed'}),'failed':(False,{'policy_failed'}),
219
+ 'undetermined':(None,{'policy_undetermined','policy_not_supported','policy_evaluation_failed','required_report_incomplete'})}
220
+ require(self.outcome in allowed and self.passed is allowed[self.outcome][0] and self.reason_code in allowed[self.outcome][1])
221
+ require(self.authority=='historical_decision_requires_current_service_check')
222
+ @classmethod
223
+ def from_dict(cls,value:Mapping[str,Any])->PrDecisionReceipt:
224
+ values=cls._fields(value);values['policy']=identity(values['policy']);return cls(**values)
225
+
226
+
227
+ @dataclass(frozen=True)
228
+ class PrReportView(_PrValues):
229
+ report: ReportProjectionSummary
230
+ metrics: tuple[MetricProjectionSummary,...]
231
+ projection: ProjectionProof
232
+ team_id: str = field(repr=False)
233
+ def __post_init__(self) -> None:
234
+ require(isinstance(self.report,ReportProjectionSummary) and isinstance(self.projection,ProjectionProof));identifier(self.team_id)
235
+ require(isinstance(self.metrics,(tuple,list)) and 1<=len(self.metrics)<=128 and all(isinstance(m,MetricProjectionSummary) for m in self.metrics))
236
+ require(len({m.metric_id for m in self.metrics})==len(self.metrics))
237
+ require(all((m.run_id,m.grading_id)==(self.report.run_id,self.report.grading_id) for m in self.metrics))
238
+ require(self.projection.row_count in (1+len(self.metrics),2+len(self.metrics)))
239
+ require(self.projection.plan_digest==sha(frame('sphynx.evaluation.plan.v2',self.team_id,self.projection.event_id,self.projection.source_digest,self.projection.manifest_digest,self.projection.row_count)))
240
+ object.__setattr__(self,'metrics',tuple(self.metrics))
241
+ def to_dict(self)->dict[str,Any]:
242
+ return {'report':self.report.to_dict(),'metrics':[m.to_dict() for m in self.metrics],
243
+ 'projection':{'team_id':self.team_id,**self.projection.to_dict()}}
244
+ @classmethod
245
+ def from_dict(cls,value:Mapping[str,Any])->PrReportView:
246
+ values=keys(value,{'report','metrics','projection'});proof=keys(values['projection'],{'team_id','event_id','source_digest','plan_digest','manifest_digest','row_count','commit_hash'})
247
+ team=proof.pop('team_id');require(type(values['metrics']) is list and len(values['metrics'])<=128)
248
+ return cls(ReportProjectionSummary.from_dict(values['report']),tuple(MetricProjectionSummary.from_dict(m) for m in values['metrics']),ProjectionProof.from_dict(proof),team)
249
+
250
+
251
+ @dataclass(frozen=True)
252
+ class PrCaseChangeCounts(_PrValues):
253
+ improved: int
254
+ regressed: int
255
+ unchanged: int
256
+ unavailable: int
257
+ total: int
258
+ def __post_init__(self)->None:
259
+ for value in (self.improved,self.regressed,self.unchanged,self.unavailable,self.total):count(value,nullable=False)
260
+ require(self.improved+self.regressed+self.unchanged+self.unavailable==self.total)
261
+ @classmethod
262
+ def from_dict(cls,value:Mapping[str,Any])->PrCaseChangeCounts:return cls(**cls._fields(value))
263
+
264
+
265
+ @dataclass(frozen=True)
266
+ class PrMetricChange(_PrValues):
267
+ delta_bits: str | None
268
+ paired: int | None
269
+ unavailable: int | None
270
+ total: int | None
271
+ case_changes: PrCaseChangeCounts | None
272
+ def __post_init__(self)->None:
273
+ if self.delta_bits is not None:score_bits(self.delta_bits)
274
+ for value in (self.paired,self.unavailable,self.total):count(value)
275
+ require((self.paired is None)==(self.unavailable is None)==(self.total is None))
276
+ if self.total is not None:
277
+ require(self.paired is not None and self.unavailable is not None)
278
+ assert self.paired is not None and self.unavailable is not None
279
+ require(self.paired+self.unavailable==self.total)
280
+ require(self.case_changes is None or isinstance(self.case_changes,PrCaseChangeCounts))
281
+ @property
282
+ def delta(self)->float|None:return score_bits(self.delta_bits) if self.delta_bits is not None else None
283
+ @classmethod
284
+ def from_dict(cls,value:Mapping[str,Any])->PrMetricChange:
285
+ values=cls._fields(value)
286
+ if values['case_changes'] is not None:values['case_changes']=PrCaseChangeCounts.from_dict(values['case_changes'])
287
+ return cls(**values)
288
+
289
+
290
+ @dataclass(frozen=True)
291
+ class PrRegressionSummary(_PrValues):
292
+ SCHEMA: ClassVar[str] = 'sphynx.pr-regressions.v1'
293
+ baseline_grading_id: str
294
+ candidate_grading_id: str
295
+ method_id: str
296
+ metrics: Mapping[str,PrMetricChange] = field(repr=False)
297
+ case_change_count: int
298
+ comparison_digest: str
299
+ evidence_link: str
300
+ def __post_init__(self)->None:
301
+ text(self.baseline_grading_id);text(self.candidate_grading_id);identifier(self.method_id);digest(self.comparison_digest)
302
+ count(self.case_change_count,nullable=False)
303
+ require(isinstance(self.metrics,Mapping) and 0<len(self.metrics)<=128)
304
+ for name,value in self.metrics.items():text(name);require(isinstance(value,PrMetricChange))
305
+ require(sum(value.case_changes.total for value in self.metrics.values() if value.case_changes is not None)==self.case_change_count)
306
+ require(type(self.evidence_link) is str and re.fullmatch(r'/v1/pr-evaluations/preval_[0-9a-f]{32}/requirements/[A-Za-z0-9._~%:-]+/case-changes',self.evidence_link) is not None)
307
+ object.__setattr__(self,'metrics',MappingProxyType(dict(self.metrics)))
308
+ @classmethod
309
+ def from_dict(cls,value:Mapping[str,Any])->PrRegressionSummary:
310
+ values=cls._fields(value);require(isinstance(values['metrics'],Mapping))
311
+ values['metrics']={name:PrMetricChange.from_dict(item) for name,item in values['metrics'].items()};return cls(**values)
312
+
313
+
314
+ @dataclass(frozen=True)
315
+ class PrComparisonStatus(_PrValues):
316
+ status: str
317
+ reason_code: str
318
+ regressions: PrRegressionSummary | None
319
+ uncertainty: None
320
+ def __post_init__(self)->None:
321
+ require(self.uncertainty is None)
322
+ if self.status=='not_requested':
323
+ require(self.reason_code=='comparison_not_part_of_frozen_profile' and self.regressions is None)
324
+ elif self.status=='unavailable':
325
+ require(self.reason_code=='comparison_report_unavailable' and self.regressions is None)
326
+ else:
327
+ require((self.status,self.reason_code) in (('supported','native_comparison'),('incomplete','native_comparison_incomplete'))
328
+ and isinstance(self.regressions,PrRegressionSummary))
329
+ @classmethod
330
+ def from_dict(cls,value:Mapping[str,Any])->PrComparisonStatus:
331
+ values=cls._fields(value)
332
+ if values['regressions'] is not None:values['regressions']=PrRegressionSummary.from_dict(values['regressions'])
333
+ return cls(**values)
334
+
335
+
336
+ @dataclass(frozen=True)
337
+ class PrRequirementResult(_PrValues):
338
+ status: PrRequirementStatus
339
+ baseline: VersionedIdentity
340
+ conditions: Mapping[str,VersionedIdentity] = field(repr=False)
341
+ review_status: str
342
+ provisional: bool
343
+ acceptance_eligible: bool
344
+ report_availability: str
345
+ report: PrReportView | None
346
+ evidence_link: str | None
347
+ board_link: str | None
348
+ comparison: PrComparisonStatus
349
+ def __post_init__(self)->None:
350
+ require(isinstance(self.status,PrRequirementStatus));_exact_id(self.baseline)
351
+ require(isinstance(self.conditions,Mapping) and {'protocol','bank','evaluator','runtime'}<=set(self.conditions)
352
+ and set(self.conditions)<={'protocol','bank','evaluator','runtime','labels','references','hardware'})
353
+ for condition in self.conditions.values():_exact_id(condition)
354
+ object.__setattr__(self,'conditions',MappingProxyType(dict(self.conditions)))
355
+ require(self.review_status in ('pending','provisional','reviewed','disputed','needs_changes'))
356
+ _boolean(self.provisional);_boolean(self.acceptance_eligible);require(self.provisional==(self.review_status!='reviewed'))
357
+ require(self.report_availability in ('available','unavailable') and isinstance(self.comparison,PrComparisonStatus))
358
+ require((self.report is not None)==(self.report_availability=='available'))
359
+ if self.report is not None:
360
+ require(isinstance(self.report,PrReportView))
361
+ require(self.report.projection.row_count==1+len(self.report.metrics)+int(self.comparison.regressions is not None))
362
+ require((self.report.report.scope,self.report.report.mode)==(self.status.scope,self.status.mode))
363
+ require({k:v for k,v in self.report.report.bindings.items() if k!='candidate'}==dict(self.conditions))
364
+ if self.status.state=='completed':
365
+ coverage=self.report.report.coverage
366
+ require(self.report.report.status=='completed' and coverage.required is not None and coverage.required>0
367
+ and coverage.required==coverage.observed and coverage.missing==0 and coverage.undefined==0)
368
+ def to_dict(self)->dict[str,Any]:
369
+ value=super().to_dict();value.pop('status');return {**self.status.to_dict(),**value}
370
+ @classmethod
371
+ def from_dict(cls,value:Mapping[str,Any])->PrRequirementResult:
372
+ core={item.name for item in fields(PrRequirementStatus)}
373
+ extra={item.name for item in fields(cls)}-{'status'};values=keys(value,core|extra)
374
+ status=PrRequirementStatus.from_dict({key:values.pop(key) for key in core})
375
+ values['baseline']=identity(values['baseline']);require(isinstance(values['conditions'],Mapping))
376
+ values['conditions']={key:identity(item) for key,item in values['conditions'].items()}
377
+ if values['report'] is not None:values['report']=PrReportView.from_dict(values['report'])
378
+ values['comparison']=PrComparisonStatus.from_dict(values['comparison']);return cls(status=status,**values)
379
+
380
+
381
+ @dataclass(frozen=True)
382
+ class PrResultApplicability(_PrValues):
383
+ basis: str
384
+ latest_pr_revision_checked: bool
385
+ policy_current: bool
386
+ bank_acceptance_eligible: bool
387
+ reports_available: bool
388
+ def __post_init__(self)->None:
389
+ require(self.basis in ('requested_base_head','frozen_base_head') and self.latest_pr_revision_checked is False)
390
+ for value in (self.policy_current,self.bank_acceptance_eligible,self.reports_available):_boolean(value)
391
+ @classmethod
392
+ def from_dict(cls,value:Mapping[str,Any])->PrResultApplicability:return cls(**cls._fields(value))
393
+
394
+
395
+ @dataclass(frozen=True)
396
+ class PrRequiredCoverage(_PrValues):
397
+ unit: str
398
+ required: int
399
+ completed: int
400
+ incomplete: int
401
+ failed: int
402
+ pending: int
403
+ verified_reports: int
404
+ def __post_init__(self)->None:
405
+ require(self.unit=='required_evaluation' and count(self.required,nullable=False,maximum=64)!=0)
406
+ for name in ('completed','incomplete','failed','pending','verified_reports'):count(getattr(self,name),nullable=False,maximum=self.required)
407
+ require(self.completed+self.incomplete+self.failed+self.pending==self.required)
408
+ @classmethod
409
+ def from_dict(cls,value:Mapping[str,Any])->PrRequiredCoverage:return cls(**cls._fields(value))
410
+
411
+
412
+ @dataclass(frozen=True)
413
+ class PrEvaluationResult(_PrValues):
414
+ """Current permitted read of a frozen operation and its historical decision."""
415
+ SCHEMA: ClassVar[str] = 'sphynx.pr-evaluation-result.v1'
416
+ authority: str
417
+ operation: PrEvaluationStatus
418
+ decision: PrDecisionReceipt | None
419
+ current_acceptance: bool | None
420
+ applicability: PrResultApplicability
421
+ coverage: PrRequiredCoverage
422
+ requirements: tuple[PrRequirementResult,...] = field(repr=False)
423
+ checked_at: str | None = None
424
+ def __post_init__(self)->None:
425
+ require(self.authority=='current_authorized_frozen_revision_summary' and isinstance(self.operation,PrEvaluationStatus))
426
+ require(self.operation.status in ('running','waiting_for_reports','waiting_for_review','evaluating','completed','failed'))
427
+ if self.checked_at is not None:_time(self.checked_at)
428
+ require(isinstance(self.applicability,PrResultApplicability) and isinstance(self.coverage,PrRequiredCoverage))
429
+ require(self.applicability.basis==('requested_base_head' if self.operation.revisions is None else 'frozen_base_head'))
430
+ require(isinstance(self.requirements,(tuple,list)) and len(self.requirements)==self.operation.expected_children
431
+ and all(isinstance(r,PrRequirementResult) for r in self.requirements))
432
+ object.__setattr__(self,'requirements',tuple(self.requirements)); statuses={r.requirement_id:r for r in self.operation.requirements}
433
+ require(len({r.status.requirement_id for r in self.requirements})==len(self.requirements)
434
+ and all(statuses.get(r.status.requirement_id)==r.status for r in self.requirements))
435
+ require((self.decision is not None)==(self.operation.decision_id is not None))
436
+ if self.decision is not None:
437
+ require(isinstance(self.decision,PrDecisionReceipt) and (self.decision.plan_id,self.decision.decision_id)==(self.operation.id,self.operation.decision_id))
438
+ require(self.current_acceptance is None or type(self.current_acceptance) is bool)
439
+ completed=sum(r.status.state=='completed' for r in self.requirements);incomplete=sum(r.status.state=='incomplete' for r in self.requirements)
440
+ failed=sum(r.status.state in ('failed','unsupported') for r in self.requirements);available=sum(r.report is not None for r in self.requirements)
441
+ require((self.coverage.required,self.coverage.completed,self.coverage.incomplete,self.coverage.failed,self.coverage.pending,self.coverage.verified_reports)==
442
+ (len(self.requirements),completed,incomplete,failed,len(self.requirements)-completed-incomplete-failed,available))
443
+ if self.operation.status not in ('completed','failed'):
444
+ require(self.decision is None and self.current_acceptance is None and completed+incomplete==len(self.requirements))
445
+ require(self.applicability.bank_acceptance_eligible==all(r.acceptance_eligible for r in self.requirements)
446
+ and self.applicability.reports_available==(available==len(self.requirements)))
447
+ expected=self.decision.passed if self.decision is not None and self.applicability.policy_current and self.applicability.bank_acceptance_eligible and completed==available==len(self.requirements) else None
448
+ require(self.current_acceptance is expected, 'PR acceptance differs from explicit current eligibility')
449
+ from urllib.parse import quote
450
+ for r in self.requirements:
451
+ if r.evidence_link is not None:require(r.evidence_link==f'/v1/pr-evaluations/{self.operation.id}/requirements/{quote(r.status.requirement_id,safe="")}/projection')
452
+ if r.comparison.regressions is not None:
453
+ require(r.comparison.regressions.evidence_link==f'/v1/pr-evaluations/{self.operation.id}/requirements/{quote(r.status.requirement_id,safe="")}/case-changes')
454
+ if r.report is not None:require(r.comparison.regressions.candidate_grading_id==r.report.report.grading_id)
455
+ if r.report is not None:require(r.evidence_link is not None)
456
+ if r.board_link is not None:require(type(r.board_link) is str and re.fullmatch('/v1/banks/'+re.escape(quote(r.status.bank_id,safe=''))+r'/boards/[A-Za-z0-9._~%:-]+',r.board_link) is not None)
457
+ @classmethod
458
+ def from_dict(cls,value:Mapping[str,Any])->PrEvaluationResult:
459
+ require(isinstance(value,Mapping));values=cls._fields({'checked_at':None,**value});values['operation']=PrEvaluationStatus.from_dict(values['operation'])
460
+ if values['decision'] is not None:values['decision']=PrDecisionReceipt.from_dict(values['decision'])
461
+ values['applicability']=PrResultApplicability.from_dict(values['applicability']);values['coverage']=PrRequiredCoverage.from_dict(values['coverage'])
462
+ require(type(values['requirements']) is list and len(values['requirements'])<=64)
463
+ values['requirements']=tuple(PrRequirementResult.from_dict(item) for item in values['requirements']);return cls(**values)
464
+ def to_dict(self)->dict[str,Any]:
465
+ value=super().to_dict()
466
+ if self.checked_at is None:del value['checked_at']
467
+ return value
468
+ def summary(self)->str:
469
+ label={True:'pass',False:'fail',None:'undetermined'}
470
+ historical='unavailable' if self.decision is None else label[self.decision.passed]
471
+ return (f'PR evaluation {self.operation.id}: {self.operation.status}. Acceptance at this read: {label[self.current_acceptance]}. '
472
+ f'Saved decision: {historical} (historical). Verified reports: {self.coverage.verified_reports}/{self.coverage.required}.')
473
+ def to_text(self)->str:return self.summary()
474
+ def to_markdown(self,*,include_details:bool=False)->str:
475
+ require(type(include_details) is bool)
476
+ lines=[self.summary(),'','| Requirement | Scope | Location | Work | Report | Review |','| --- | --- | --- | --- | --- | --- |']
477
+ for r in self.requirements:
478
+ lines.append('| '+' | '.join(_cell(value) for value in (r.status.requirement_id,r.status.scope,r.status.mode,r.status.state,r.report_availability,r.review_status))+' |')
479
+ for r in self.requirements:
480
+ comparison=r.comparison.regressions
481
+ if comparison is not None:
482
+ lines.extend(['',f'{_cell(r.status.requirement_id)}: comparison {r.comparison.status}; {comparison.case_change_count} case/metric rows.'])
483
+ lines.extend(['','| Metric | Candidate minus baseline | Paired | Unavailable |','| --- | --- | --- | --- |'])
484
+ # Full fields and all metrics remain in the details view.
485
+ for name,value in list(comparison.metrics.items())[:6]:
486
+ values=(name,'unknown' if value.delta is None else f'{value.delta:+.6g}',
487
+ 'unknown' if value.paired is None else value.paired,'unknown' if value.unavailable is None else value.unavailable)
488
+ lines.append('| '+' | '.join(_cell(v) for v in values)+' |')
489
+ if len(comparison.metrics)>6:lines.append(f'Showing 6 of {len(comparison.metrics)} metrics. All metrics are available in comparison.regressions.metrics and the details view.')
490
+ lines.append('Descriptive native comparison. Uncertainty is not validated. Full values and case pages remain available.')
491
+ result='\n'.join(lines)
492
+ return result+_details(self.to_dict()) if include_details else result
493
+
494
+
495
+ @dataclass(frozen=True)
496
+ class PrCaseChange(_PrValues):
497
+ """One native case/metric delta. No question or reference text is present."""
498
+ case_id: str
499
+ metric_id: str
500
+ direction: str
501
+ baseline_bits: str | None
502
+ candidate_bits: str | None
503
+ delta_bits: str | None
504
+ status: str
505
+ def __post_init__(self) -> None:
506
+ text(self.case_id); text(self.metric_id)
507
+ require(self.direction in ('higher','lower'))
508
+ values=[None if v is None else score_bits(v) for v in (self.baseline_bits,self.candidate_bits,self.delta_bits)]
509
+ a,b,delta=values
510
+ if a is None or b is None:
511
+ require(delta is None and self.status=='unavailable')
512
+ else:
513
+ require(delta is not None and delta==b-a)
514
+ assert delta is not None
515
+ expected='unchanged' if delta==0 else 'improved' if (delta>0)==(self.direction=='higher') else 'regressed'
516
+ require(self.status==expected)
517
+ @property
518
+ def baseline(self) -> float | None:return None if self.baseline_bits is None else score_bits(self.baseline_bits)
519
+ @property
520
+ def candidate(self) -> float | None:return None if self.candidate_bits is None else score_bits(self.candidate_bits)
521
+ @property
522
+ def delta(self) -> float | None:return None if self.delta_bits is None else score_bits(self.delta_bits)
523
+ @classmethod
524
+ def from_dict(cls,value: Mapping[str,Any]) -> PrCaseChange:return cls(**cls._fields(value))
525
+
526
+
527
+ @dataclass(frozen=True)
528
+ class PrCaseChangePage(_PrValues):
529
+ """A bounded page bound to the operation's committed comparison."""
530
+ SCHEMA: ClassVar[str]='sphynx.pr-case-changes.v1'
531
+ authority: str
532
+ plan_id: str
533
+ requirement_id: str
534
+ source_digest: str
535
+ comparison_digest: str
536
+ offset: int
537
+ total: int
538
+ items: tuple[PrCaseChange,...]
539
+ next_cursor: str | None
540
+ def __post_init__(self) -> None:
541
+ require(self.authority=='current_authorized_committed_comparison')
542
+ _saved(self.plan_id,'preval');identifier(self.requirement_id)
543
+ digest(self.source_digest);digest(self.comparison_digest)
544
+ count(self.offset,nullable=False);count(self.total,nullable=False)
545
+ object.__setattr__(self,'items',tuple(self.items))
546
+ require(len(self.items)<=500 and all(isinstance(v,PrCaseChange) for v in self.items))
547
+ require(self.offset+len(self.items)<=self.total and (bool(self.items) or self.offset==self.total))
548
+ require(len({(v.case_id,v.metric_id) for v in self.items})==len(self.items))
549
+ require((self.next_cursor is None)==(self.offset+len(self.items)==self.total))
550
+ if self.next_cursor is not None:_case_cursor(self.next_cursor)
551
+ @classmethod
552
+ def from_dict(cls,value: Mapping[str,Any]) -> PrCaseChangePage:
553
+ values=cls._fields(value);require(type(values['items']) is list)
554
+ values['items']=tuple(PrCaseChange.from_dict(v) for v in values['items'])
555
+ return cls(**values)
556
+
557
+
558
+ def _case_cursor(value: Any) -> str:
559
+ require(type(value) is str and 1<=len(value)<=2048 and re.fullmatch(r'[A-Za-z0-9_-]+',value) is not None,'invalid comparison page cursor')
560
+ return value
sphynx/progress.py ADDED
@@ -0,0 +1,89 @@
1
+ """Read-only presentation of cached work status, without polling or retrying."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass, field
6
+ from types import MappingProxyType
7
+ from typing import Literal, cast
8
+
9
+ from .diagnostics import Diagnostic, _safe_text
10
+
11
+ ProgressState = Literal['queued', 'running', 'waiting_for_review', 'partial',
12
+ 'failed', 'completed', 'cancelling', 'cancelled', 'unknown']
13
+ _STATES = {name: name for name in ('queued', 'running', 'waiting_for_review', 'partial',
14
+ 'failed', 'completed', 'cancelling', 'cancelled')}
15
+ _STATES.update({'preparing': 'running', 'building': 'running', 'grading': 'running',
16
+ 'waiting_for_reports': 'running', 'evaluating': 'running',
17
+ 'incomplete': 'partial', 'canceled': 'cancelled'})
18
+ _TERMINAL = {'failed', 'completed', 'cancelled'}
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class ProgressView:
23
+ """An observation, not an execution control or an acceptance verdict.
24
+
25
+ ``state`` groups explicit native names for display. ``native_status`` keeps
26
+ the original name. Unknown names stay unknown. Counts, percentages and time
27
+ estimates are absent because these handles do not supply them.
28
+ """
29
+
30
+ operation: Literal['submission', 'execution', 'grading']
31
+ native_status: str
32
+ state: ProgressState
33
+ terminal: bool | None
34
+ identities: Mapping[str, str] = field(default_factory=dict)
35
+
36
+ def __post_init__(self) -> None:
37
+ if self.operation not in ('submission', 'execution', 'grading'):
38
+ raise ValueError('unknown progress operation')
39
+ if not isinstance(self.native_status, str) or not self.native_status:
40
+ raise ValueError('progress needs the native status')
41
+ if self.state not in {*_STATES.values(), 'unknown'}:
42
+ raise ValueError('unknown presentation state')
43
+ if self.terminal is not None and type(self.terminal) is not bool:
44
+ raise ValueError('terminal must be boolean or unknown')
45
+ safe = Diagnostic('progress', 'Cached work status', (), recovery=self.identities)
46
+ object.__setattr__(self, 'identities', MappingProxyType(dict(safe.recovery)))
47
+
48
+ def to_dict(self) -> dict:
49
+ return {'schema_version': 'sphynx.progress.v1', 'operation': self.operation,
50
+ 'native_status': self.native_status, 'state': self.state,
51
+ 'terminal': self.terminal, 'identities': dict(self.identities)}
52
+
53
+ def to_text(self) -> str:
54
+ status = _safe_text(self.native_status)
55
+ lines = [f'{self.operation}: {self.state} (native: {status})']
56
+ lines.extend(f'{key}: {_safe_text(value)}' for key, value in self.identities.items())
57
+ if self.state == 'unknown':
58
+ lines.append('This native state has no registered display meaning.')
59
+ if self.state == 'completed':
60
+ lines.append('Work completed. Quality and acceptance require their own report checks.')
61
+ lines.append('This is cached status. Refresh the same handle for a newer observation.')
62
+ return '\n'.join(lines)
63
+
64
+ def __str__(self) -> str:
65
+ return self.to_text()
66
+
67
+
68
+ def _progress(operation, status, identities, *, terminal=None, complete=None, failed=None):
69
+ # Missing or unrecognized native values never become queued or completed.
70
+ native = status if isinstance(status, str) and status else 'unavailable'
71
+ state = _STATES.get(native, 'unknown')
72
+ if operation == 'execution' and native == 'captured':
73
+ state = 'completed'
74
+ if terminal is False and state in _TERMINAL:
75
+ state = 'unknown'
76
+ if (state == 'completed' and complete is False) or (state == 'failed' and failed is False):
77
+ state = 'unknown'
78
+ if terminal is None and state != 'unknown':
79
+ terminal = state in _TERMINAL if state != 'partial' else None
80
+ return ProgressView(operation, native, cast(ProgressState, state), terminal,
81
+ {key: value for key, value in identities.items() if value is not None})
82
+
83
+
84
+ def _hosted_progress(record, operation):
85
+ key = {'submission': 'experiment_id', 'execution': 'run_id', 'grading': 'evaluation_id'}[operation]
86
+ identities = {key: record['id']}
87
+ identities.update({key: record[key] for key in ('experiment_id', 'run_id', 'evaluation_id')
88
+ if record.get(key) is not None})
89
+ return _progress(operation, record.get('status'), identities)