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