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/audit_access.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Typed selected-bank views. The authenticated service remains authoritative."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .audit_contracts import BankRelease, ReviewEvent, ReviewTarget
|
|
10
|
+
from .audit_requests import _DIGEST, _exact_identity, _identifier, _key, _positive_version
|
|
11
|
+
from .evaluation_protocols import VersionedIdentity
|
|
12
|
+
from .workflow_contracts import WorkflowContractError, _object, _thaw
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _fields(value, schema, required):
|
|
16
|
+
if (not isinstance(value, Mapping) or value.get('schema_version') != schema
|
|
17
|
+
or set(value) != {'schema_version', *required}):
|
|
18
|
+
raise WorkflowContractError('invalid or unsupported audit response fields')
|
|
19
|
+
return {key: item for key, item in value.items() if key != 'schema_version'}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _common(bank, release, through):
|
|
23
|
+
_identifier(bank); _identifier(release); _positive_version(through)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _items(values, cls, maximum):
|
|
27
|
+
if not isinstance(values, (tuple, list)) or len(values) > maximum or any(not isinstance(x, cls) for x in values):
|
|
28
|
+
raise WorkflowContractError('invalid or oversized audit page items')
|
|
29
|
+
return tuple(values)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _protocol(value, identity):
|
|
33
|
+
value = _object(value, 'protected protocol')
|
|
34
|
+
if (set(value) != {'schema_version', 'protocol_id', 'version', 'implementation_id',
|
|
35
|
+
'instructions', 'required_fields', 'allow_extra_fields'}
|
|
36
|
+
or value.get('schema_version') != 'sphynx.bank-review-protocol.v1'
|
|
37
|
+
or value.get('protocol_id') != identity.identity_id or value.get('version') != identity.version):
|
|
38
|
+
raise WorkflowContractError('review protocol identity differs')
|
|
39
|
+
instructions, required = value['instructions'], value['required_fields']
|
|
40
|
+
if (type(value['implementation_id']) is not str or not value['implementation_id'].strip()
|
|
41
|
+
or type(instructions) is not str or not instructions.strip() or len(instructions.encode('utf-8')) > 8192
|
|
42
|
+
or type(value['allow_extra_fields']) is not bool or not isinstance(required, Mapping)
|
|
43
|
+
or not 1 <= len(required) <= 64):
|
|
44
|
+
raise WorkflowContractError('review protocol manifest shape differs')
|
|
45
|
+
for name, kind in required.items():
|
|
46
|
+
_identifier(name)
|
|
47
|
+
if kind not in ('string', 'boolean', 'number', 'integer', 'object', 'array'):
|
|
48
|
+
raise WorkflowContractError('unsupported review protocol field kind')
|
|
49
|
+
# This exact manifest schema has only strings, booleans and objects. Match
|
|
50
|
+
# the service's sorted UTF-8 digest profile; do not generalize to float JSON
|
|
51
|
+
# or replace the separate legacy/public canonical digest contracts.
|
|
52
|
+
encoded = json.dumps({'profile': 'sphynx.private-audit-json.v1', 'value': _thaw(value)},
|
|
53
|
+
ensure_ascii=False, sort_keys=True, separators=(',', ':'), allow_nan=False).encode('utf-8')
|
|
54
|
+
if 'sha256:' + hashlib.sha256(encoded).hexdigest() != identity.digest:
|
|
55
|
+
raise WorkflowContractError('review protocol manifest digest differs')
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _events(items, bank, release, through, next_after):
|
|
60
|
+
versions = [event.version for event in items]
|
|
61
|
+
if versions != sorted(set(versions)) or any(event.version > through or event.bank_id != bank
|
|
62
|
+
or event.release_id != release for event in items) or len({event.team_id for event in items}) > 1:
|
|
63
|
+
raise WorkflowContractError('audit history identity or ordering differs')
|
|
64
|
+
if next_after is not None and (type(next_after) is not int or not items or next_after != versions[-1]):
|
|
65
|
+
raise WorkflowContractError('audit history cursor does not bind the last event')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _page_dict(schema, page):
|
|
69
|
+
return {'schema_version': schema, 'bank_id': page.bank_id, 'release_id': page.release_id,
|
|
70
|
+
'through_version': page.through_version, 'items': [item.to_dict() for item in page.items],
|
|
71
|
+
'next_after': page.next_after}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _metadata(event):
|
|
75
|
+
allowed = {'release_digest', 'contribution_id', 'target_id', 'decision',
|
|
76
|
+
'supersedes_contribution_id', 'adjudication_id', 'contribution_set_digest'}
|
|
77
|
+
if set(event.details) - allowed or any(item is not None and type(item) is not str for item in event.details.values()):
|
|
78
|
+
raise WorkflowContractError('ordinary audit metadata contains unsupported detail')
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _Readable:
|
|
82
|
+
def to_text(self) -> str:
|
|
83
|
+
raise NotImplementedError
|
|
84
|
+
|
|
85
|
+
def __str__(self) -> str:
|
|
86
|
+
return self.to_text()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class ReviewTargetPage(_Readable):
|
|
91
|
+
bank_id: str
|
|
92
|
+
release_id: str
|
|
93
|
+
through_version: int
|
|
94
|
+
items: tuple[ReviewTarget, ...] = field(repr=False)
|
|
95
|
+
protocols_by_digest: Mapping[str, Mapping[str, Any]] = field(repr=False)
|
|
96
|
+
next_after: str | None = None
|
|
97
|
+
|
|
98
|
+
def __post_init__(self):
|
|
99
|
+
_common(self.bank_id, self.release_id, self.through_version)
|
|
100
|
+
items = _items(self.items, ReviewTarget, 20)
|
|
101
|
+
keys = [_identifier(item.target_id) for item in items]
|
|
102
|
+
if keys != sorted(set(keys)) or (self.next_after is not None and (not keys or self.next_after != keys[-1])):
|
|
103
|
+
raise WorkflowContractError('target page order or cursor differs')
|
|
104
|
+
if not isinstance(self.protocols_by_digest, Mapping) or set(self.protocols_by_digest) != {item.protocol.digest for item in items}:
|
|
105
|
+
raise WorkflowContractError('target page protocol set differs')
|
|
106
|
+
protocols = {}
|
|
107
|
+
for item in items:
|
|
108
|
+
_exact_identity(item.subject); _exact_identity(item.protocol)
|
|
109
|
+
protocols[item.protocol.digest] = _protocol(self.protocols_by_digest[item.protocol.digest], item.protocol)
|
|
110
|
+
object.__setattr__(self, 'items', items)
|
|
111
|
+
object.__setattr__(self, 'protocols_by_digest', _object(protocols, 'protected protocol manifests'))
|
|
112
|
+
|
|
113
|
+
def to_dict(self) -> dict[str, Any]:
|
|
114
|
+
return {**_page_dict('sphynx.bank-review-targets.v1', self),
|
|
115
|
+
'protocols_by_digest': _thaw(self.protocols_by_digest)}
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def from_dict(cls, value: Mapping[str, Any]) -> ReviewTargetPage:
|
|
119
|
+
data = _fields(value, 'sphynx.bank-review-targets.v1',
|
|
120
|
+
{'bank_id', 'release_id', 'through_version', 'items', 'protocols_by_digest', 'next_after'})
|
|
121
|
+
data['items'] = _decode_items(data['items'], ReviewTarget, 20)
|
|
122
|
+
return cls(**data)
|
|
123
|
+
|
|
124
|
+
def to_text(self) -> str:
|
|
125
|
+
return f'Review targets: {self.bank_id}/{self.release_id} at version {self.through_version}; {len(self.items)} protected items. Inspect authorized items explicitly.'
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _decode_items(value, cls, maximum):
|
|
129
|
+
if not isinstance(value, (list, tuple)) or len(value) > maximum:
|
|
130
|
+
raise WorkflowContractError('invalid or oversized audit page items')
|
|
131
|
+
return tuple(cls.from_dict(item) for item in value)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass(frozen=True)
|
|
135
|
+
class ReviewHistoryPage(_Readable):
|
|
136
|
+
bank_id: str
|
|
137
|
+
release_id: str
|
|
138
|
+
through_version: int
|
|
139
|
+
items: tuple[ReviewEvent, ...] = field(repr=False)
|
|
140
|
+
next_after: int | None = None
|
|
141
|
+
protected: bool = False
|
|
142
|
+
|
|
143
|
+
def __post_init__(self):
|
|
144
|
+
_common(self.bank_id, self.release_id, self.through_version)
|
|
145
|
+
if type(self.protected) is not bool:
|
|
146
|
+
raise WorkflowContractError('protected history selection must be boolean')
|
|
147
|
+
items = _items(self.items, ReviewEvent, 10 if self.protected else 100)
|
|
148
|
+
_events(items, self.bank_id, self.release_id, self.through_version, self.next_after)
|
|
149
|
+
if not self.protected:
|
|
150
|
+
for item in items:
|
|
151
|
+
_metadata(item)
|
|
152
|
+
object.__setattr__(self, 'items', items)
|
|
153
|
+
|
|
154
|
+
def to_dict(self) -> dict[str, Any]:
|
|
155
|
+
schema = 'sphynx.bank-review-protected-history.v1' if self.protected else 'sphynx.bank-review-history.v1'
|
|
156
|
+
return _page_dict(schema, self)
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_dict(cls, value: Mapping[str, Any]) -> ReviewHistoryPage:
|
|
160
|
+
protected = isinstance(value, Mapping) and value.get('schema_version') == 'sphynx.bank-review-protected-history.v1'
|
|
161
|
+
schema = 'sphynx.bank-review-protected-history.v1' if protected else 'sphynx.bank-review-history.v1'
|
|
162
|
+
data = _fields(value, schema, {'bank_id', 'release_id', 'through_version', 'items', 'next_after'})
|
|
163
|
+
data['items'] = _decode_items(data['items'], ReviewEvent, 10 if protected else 100)
|
|
164
|
+
return cls(**data, protected=protected)
|
|
165
|
+
|
|
166
|
+
def to_text(self) -> str:
|
|
167
|
+
return f'Review history: {self.bank_id}/{self.release_id} through version {self.through_version}; {len(self.items)} events; protected={self.protected}.'
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True)
|
|
171
|
+
class TargetReviewPage(_Readable):
|
|
172
|
+
bank_id: str
|
|
173
|
+
release_id: str
|
|
174
|
+
target_id: str
|
|
175
|
+
through_version: int
|
|
176
|
+
subject: VersionedIdentity
|
|
177
|
+
protocol_identity: VersionedIdentity
|
|
178
|
+
protocol: Mapping[str, Any] = field(repr=False)
|
|
179
|
+
current_contribution_set_digest: str
|
|
180
|
+
items: tuple[ReviewEvent, ...] = field(repr=False)
|
|
181
|
+
next_after: int | None = None
|
|
182
|
+
|
|
183
|
+
def __post_init__(self):
|
|
184
|
+
_common(self.bank_id, self.release_id, self.through_version); _identifier(self.target_id)
|
|
185
|
+
_exact_identity(self.subject); _exact_identity(self.protocol_identity)
|
|
186
|
+
if type(self.current_contribution_set_digest) is not str or not _DIGEST.fullmatch(self.current_contribution_set_digest):
|
|
187
|
+
raise WorkflowContractError('review page requires an exact current-set digest')
|
|
188
|
+
items = _items(self.items, ReviewEvent, 10)
|
|
189
|
+
_events(items, self.bank_id, self.release_id, self.through_version, self.next_after)
|
|
190
|
+
if any(item.kind not in ('contribution', 'adjudication') or item.details.get('target_id') != self.target_id for item in items):
|
|
191
|
+
raise WorkflowContractError('review event belongs to another target or kind')
|
|
192
|
+
object.__setattr__(self, 'items', items)
|
|
193
|
+
object.__setattr__(self, 'protocol', _protocol(self.protocol, self.protocol_identity))
|
|
194
|
+
|
|
195
|
+
def to_dict(self) -> dict[str, Any]:
|
|
196
|
+
return {**_page_dict('sphynx.bank-target-review-details.v1', self), 'target_id': self.target_id,
|
|
197
|
+
'subject': self.subject.to_dict(), 'protocol_identity': self.protocol_identity.to_dict(),
|
|
198
|
+
'protocol': _thaw(self.protocol), 'current_contribution_set_digest': self.current_contribution_set_digest}
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def from_dict(cls, value: Mapping[str, Any]) -> TargetReviewPage:
|
|
202
|
+
data = _fields(value, 'sphynx.bank-target-review-details.v1', {'bank_id', 'release_id', 'target_id',
|
|
203
|
+
'through_version', 'subject', 'protocol_identity', 'protocol', 'current_contribution_set_digest', 'items', 'next_after'})
|
|
204
|
+
data['items'] = _decode_items(data['items'], ReviewEvent, 10)
|
|
205
|
+
for name in ('subject', 'protocol_identity'):
|
|
206
|
+
data[name] = VersionedIdentity.from_dict(data[name])
|
|
207
|
+
return cls(**data)
|
|
208
|
+
|
|
209
|
+
def to_text(self) -> str:
|
|
210
|
+
return f'Target reviews: {self.bank_id}/{self.release_id}/{self.target_id} through version {self.through_version}; {len(self.items)} protected events. Current authority is rechecked on each read.'
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass(frozen=True)
|
|
214
|
+
class ReviewOperationReceipt(_Readable):
|
|
215
|
+
operation_key: str
|
|
216
|
+
event: ReviewEvent
|
|
217
|
+
release_snapshot: BankRelease = field(repr=False)
|
|
218
|
+
eligibility_authority: str = 'receipt_snapshot_requires_current_read'
|
|
219
|
+
|
|
220
|
+
def __post_init__(self):
|
|
221
|
+
_key(self.operation_key)
|
|
222
|
+
if not isinstance(self.event, ReviewEvent) or not isinstance(self.release_snapshot, BankRelease):
|
|
223
|
+
raise WorkflowContractError('invalid review receipt records')
|
|
224
|
+
event, release = self.event, self.release_snapshot
|
|
225
|
+
_metadata(event)
|
|
226
|
+
if (self.eligibility_authority != 'receipt_snapshot_requires_current_read'
|
|
227
|
+
or (event.bank_id, event.release_id, event.team_id, event.version) !=
|
|
228
|
+
(release.bank_id, release.release_id, release.team_id, release.version)):
|
|
229
|
+
raise WorkflowContractError('review receipt identity or historical authority differs')
|
|
230
|
+
|
|
231
|
+
def to_dict(self) -> dict[str, Any]:
|
|
232
|
+
return {'schema_version': 'sphynx.bank-review-operation.v1', 'operation_key': self.operation_key,
|
|
233
|
+
'event': self.event.to_dict(), 'release_snapshot': self.release_snapshot.to_dict(),
|
|
234
|
+
'eligibility_authority': self.eligibility_authority}
|
|
235
|
+
|
|
236
|
+
@classmethod
|
|
237
|
+
def from_dict(cls, value: Mapping[str, Any]) -> ReviewOperationReceipt:
|
|
238
|
+
data = _fields(value, 'sphynx.bank-review-operation.v1',
|
|
239
|
+
{'operation_key', 'event', 'release_snapshot', 'eligibility_authority'})
|
|
240
|
+
data['event'] = ReviewEvent.from_dict(data['event'])
|
|
241
|
+
data['release_snapshot'] = BankRelease.from_dict(data['release_snapshot'])
|
|
242
|
+
return cls(**data)
|
|
243
|
+
|
|
244
|
+
def to_text(self) -> str:
|
|
245
|
+
return f'Historical review receipt: {self.event.bank_id}/{self.event.release_id} at version {self.event.version}. Read the current release before using eligibility.'
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Neutral private audit records; construction never grants review authority.
|
|
2
|
+
|
|
3
|
+
Only an authenticated service can stamp an actor or decide eligibility. These
|
|
4
|
+
classes validate permitted wire records. Protected target/judgment payloads are
|
|
5
|
+
hidden from repr, and become explicit only through authorized reads/to_dict().
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from dataclasses import dataclass, field, fields
|
|
11
|
+
from datetime import datetime, timedelta
|
|
12
|
+
import re
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
from typing import Any, ClassVar
|
|
15
|
+
|
|
16
|
+
from .evaluation_protocols import VersionedIdentity, _envelope
|
|
17
|
+
from .workflow_contracts import WorkflowContractError, _object, _optional_text, _text, _texts, _thaw
|
|
18
|
+
|
|
19
|
+
_DIGEST = re.compile(r"sha256:[0-9a-f]{64}\Z")
|
|
20
|
+
_OPERATION = re.compile(r"[A-Za-z0-9._:-]{16,100}\Z")
|
|
21
|
+
_TARGETS = ("bank_curation", "output_judgment", "evaluator_audit")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _version(value):
|
|
25
|
+
if type(value) is not int or value < 1:
|
|
26
|
+
raise WorkflowContractError("audit version must be a positive integer")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _identity(value):
|
|
30
|
+
if not isinstance(value, VersionedIdentity) or value.digest is None:
|
|
31
|
+
raise WorkflowContractError("audit identity requires an exact version and digest")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _wire(value):
|
|
36
|
+
if isinstance(value, VersionedIdentity):
|
|
37
|
+
return value.to_dict()
|
|
38
|
+
if isinstance(value, Mapping):
|
|
39
|
+
return {key: _wire(item) for key, item in value.items()}
|
|
40
|
+
return _thaw(value)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _construct(cls, values):
|
|
44
|
+
try:
|
|
45
|
+
return cls(**values)
|
|
46
|
+
except TypeError as exc:
|
|
47
|
+
raise WorkflowContractError("invalid " + cls.__name__ + " fields") from exc
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class _AuditWire:
|
|
51
|
+
_SCHEMA: ClassVar[str]
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
return {"schema_version": self._SCHEMA,
|
|
55
|
+
**{item.name: _wire(getattr(self, item.name)) for item in fields(self)}}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class BankRelease(_AuditWire):
|
|
60
|
+
"""A selected-bank summary, with separate use/ranking/acceptance eligibility."""
|
|
61
|
+
|
|
62
|
+
_SCHEMA: ClassVar[str] = "sphynx.bank-release.v1"
|
|
63
|
+
bank_id: str
|
|
64
|
+
release_id: str
|
|
65
|
+
team_id: str
|
|
66
|
+
release_digest: str
|
|
67
|
+
version: int
|
|
68
|
+
review_status: str
|
|
69
|
+
state: str
|
|
70
|
+
bindings: Mapping[str, VersionedIdentity]
|
|
71
|
+
eligibility: Mapping[str, Any]
|
|
72
|
+
parent_release_id: str | None = None
|
|
73
|
+
counts: Mapping[str, int] = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
def __post_init__(self):
|
|
76
|
+
for name in ("bank_id", "release_id", "team_id"):
|
|
77
|
+
_text(getattr(self, name), name)
|
|
78
|
+
if type(self.release_digest) is not str or not _DIGEST.fullmatch(self.release_digest):
|
|
79
|
+
raise WorkflowContractError("release_digest must be canonical SHA-256")
|
|
80
|
+
_version(self.version)
|
|
81
|
+
if self.review_status not in ("pending", "provisional", "reviewed", "disputed", "needs_changes"):
|
|
82
|
+
raise WorkflowContractError("invalid bank review status")
|
|
83
|
+
if self.state not in ("active", "held", "withdrawn"):
|
|
84
|
+
raise WorkflowContractError("invalid bank release state")
|
|
85
|
+
_optional_text(self.parent_release_id, "parent_release_id")
|
|
86
|
+
if self.parent_release_id == self.release_id:
|
|
87
|
+
raise WorkflowContractError("release cannot be its own parent")
|
|
88
|
+
if not isinstance(self.bindings, Mapping) or not {"bank", "review_protocol", "review_policy"} <= set(self.bindings):
|
|
89
|
+
raise WorkflowContractError("release requires bank, review_protocol and review_policy identities")
|
|
90
|
+
bindings = {_text(key, "binding role"): _identity(value) for key, value in self.bindings.items()}
|
|
91
|
+
object.__setattr__(self, "bindings", MappingProxyType(bindings))
|
|
92
|
+
eligibility = _object(self.eligibility, "eligibility")
|
|
93
|
+
if set(eligibility) != {"development", "ranking", "acceptance"}:
|
|
94
|
+
raise WorkflowContractError("eligibility requires separate development, ranking and acceptance")
|
|
95
|
+
for value in eligibility.values():
|
|
96
|
+
if not isinstance(value, Mapping) or set(value) != {"eligible", "reasons"} or type(value["eligible"]) is not bool:
|
|
97
|
+
raise WorkflowContractError("eligibility requires an explicit boolean and reason list")
|
|
98
|
+
_texts(value["reasons"], "eligibility reasons")
|
|
99
|
+
if (not value["eligible"] and not value["reasons"]) or (self.state != "active" and value["eligible"]):
|
|
100
|
+
raise WorkflowContractError("ineligible/held release needs explicit truthful eligibility")
|
|
101
|
+
object.__setattr__(self, "eligibility", eligibility)
|
|
102
|
+
counts = _object(self.counts, "review counts")
|
|
103
|
+
if any(type(value) is not int or value < 0 for value in counts.values()):
|
|
104
|
+
raise WorkflowContractError("review counts must be nonnegative integers")
|
|
105
|
+
object.__setattr__(self, "counts", counts)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def from_dict(cls, value: Mapping[str, Any]) -> BankRelease:
|
|
109
|
+
values = _envelope(value, cls._SCHEMA, cls)
|
|
110
|
+
bindings = values.get("bindings")
|
|
111
|
+
if not isinstance(bindings, Mapping):
|
|
112
|
+
raise WorkflowContractError("release bindings must be an object")
|
|
113
|
+
values["bindings"] = {key: VersionedIdentity.from_dict(item) for key, item in bindings.items()}
|
|
114
|
+
return _construct(cls, values)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True)
|
|
118
|
+
class ReviewTarget(_AuditWire):
|
|
119
|
+
"""An authorized private review target, without domain-specific fields."""
|
|
120
|
+
|
|
121
|
+
_SCHEMA: ClassVar[str] = "sphynx.review-target.v1"
|
|
122
|
+
target_id: str
|
|
123
|
+
kind: str
|
|
124
|
+
subject: VersionedIdentity
|
|
125
|
+
protocol: VersionedIdentity
|
|
126
|
+
display: Mapping[str, Any] = field(repr=False)
|
|
127
|
+
required_for_curation: bool = False
|
|
128
|
+
|
|
129
|
+
def __post_init__(self):
|
|
130
|
+
_text(self.target_id, "target_id")
|
|
131
|
+
if self.kind not in _TARGETS:
|
|
132
|
+
raise WorkflowContractError("invalid review target kind")
|
|
133
|
+
_identity(self.subject); _identity(self.protocol)
|
|
134
|
+
if type(self.required_for_curation) is not bool or (self.required_for_curation and self.kind != "bank_curation"):
|
|
135
|
+
raise WorkflowContractError("only bank curation targets can be required for bank review")
|
|
136
|
+
object.__setattr__(self, "display", _object(self.display, "private review display"))
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_dict(cls, value: Mapping[str, Any]) -> ReviewTarget:
|
|
140
|
+
values = _envelope(value, cls._SCHEMA, cls)
|
|
141
|
+
for key in ("subject", "protocol"):
|
|
142
|
+
values[key] = VersionedIdentity.from_dict(values.get(key))
|
|
143
|
+
return _construct(cls, values)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass(frozen=True)
|
|
147
|
+
class ReviewContribution(_AuditWire):
|
|
148
|
+
"""A write request. Actor, team, trust and timestamp are server-owned."""
|
|
149
|
+
|
|
150
|
+
_SCHEMA: ClassVar[str] = "sphynx.review-contribution-request.v1"
|
|
151
|
+
target_id: str
|
|
152
|
+
decision: str
|
|
153
|
+
expected_version: int
|
|
154
|
+
idempotency_key: str
|
|
155
|
+
judgment: Mapping[str, Any] = field(repr=False)
|
|
156
|
+
supersedes_contribution_id: str | None = None
|
|
157
|
+
|
|
158
|
+
def __post_init__(self):
|
|
159
|
+
_text(self.target_id, "target_id")
|
|
160
|
+
if self.decision not in ("approve", "reject", "abstain"):
|
|
161
|
+
raise WorkflowContractError("review decision must be approve, reject or abstain")
|
|
162
|
+
_version(self.expected_version)
|
|
163
|
+
if type(self.idempotency_key) is not str or not _OPERATION.fullmatch(self.idempotency_key):
|
|
164
|
+
raise WorkflowContractError("idempotency_key requires 16-100 ASCII identifier characters")
|
|
165
|
+
_optional_text(self.supersedes_contribution_id, "supersedes_contribution_id")
|
|
166
|
+
object.__setattr__(self, "judgment", _object(self.judgment, "private review judgment"))
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def from_dict(cls, value: Mapping[str, Any]) -> ReviewContribution:
|
|
170
|
+
return _construct(cls, _envelope(value, cls._SCHEMA, cls))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class ReviewEvent(_AuditWire):
|
|
175
|
+
"""An append-only service receipt. It cannot authenticate itself."""
|
|
176
|
+
|
|
177
|
+
_SCHEMA: ClassVar[str] = "sphynx.bank-review-event.v1"
|
|
178
|
+
event_id: str
|
|
179
|
+
bank_id: str
|
|
180
|
+
release_id: str
|
|
181
|
+
team_id: str
|
|
182
|
+
version: int
|
|
183
|
+
actor_id: str
|
|
184
|
+
created_at: str
|
|
185
|
+
kind: str
|
|
186
|
+
details: Mapping[str, Any] = field(default_factory=dict, repr=False)
|
|
187
|
+
|
|
188
|
+
def __post_init__(self):
|
|
189
|
+
for name in ("event_id", "bank_id", "release_id", "team_id", "actor_id", "created_at"):
|
|
190
|
+
_text(getattr(self, name), name)
|
|
191
|
+
_version(self.version)
|
|
192
|
+
if self.kind not in ("release_created", "contribution", "adjudication", "hold", "resume", "withdraw"):
|
|
193
|
+
raise WorkflowContractError("invalid bank review event kind")
|
|
194
|
+
try:
|
|
195
|
+
timestamp = datetime.fromisoformat(self.created_at.replace("Z", "+00:00"))
|
|
196
|
+
except ValueError as exc:
|
|
197
|
+
raise WorkflowContractError("created_at must be an ISO UTC timestamp") from exc
|
|
198
|
+
if timestamp.utcoffset() != timedelta(0):
|
|
199
|
+
raise WorkflowContractError("created_at must include UTC timezone")
|
|
200
|
+
object.__setattr__(self, "details", _object(self.details, "permitted event details"))
|
|
201
|
+
|
|
202
|
+
@classmethod
|
|
203
|
+
def from_dict(cls, value: Mapping[str, Any]) -> ReviewEvent:
|
|
204
|
+
return _construct(cls, _envelope(value, cls._SCHEMA, cls))
|
sphynx/audit_requests.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Immutable neutral private-audit requests; the service owns authorization."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
import unicodedata
|
|
9
|
+
from types import MappingProxyType
|
|
10
|
+
from typing import Any, ClassVar
|
|
11
|
+
|
|
12
|
+
from .audit_contracts import ReviewTarget, _AuditWire, _construct, _identity
|
|
13
|
+
from .evaluation_protocols import VersionedIdentity, _envelope
|
|
14
|
+
from .workflow_contracts import WorkflowContractError
|
|
15
|
+
|
|
16
|
+
_ID = re.compile(r'[A-Za-z0-9._:-]{1,128}\Z')
|
|
17
|
+
_KEY = re.compile(r'[A-Za-z0-9._:-]{16,100}\Z')
|
|
18
|
+
_DIGEST = re.compile(r'sha256:[0-9a-f]{64}\Z')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _identifier(value: Any) -> str:
|
|
22
|
+
if type(value) is not str or not _ID.fullmatch(value) or value in ('.', '..'):
|
|
23
|
+
raise WorkflowContractError('audit path identity requires 1-128 ASCII identifier characters')
|
|
24
|
+
return value
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _positive_version(value: Any) -> int:
|
|
28
|
+
if type(value) is not int or not 1 <= value <= 2**63 - 1:
|
|
29
|
+
raise WorkflowContractError('audit version must be a positive signed 64-bit integer')
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _key(value: Any) -> str:
|
|
34
|
+
if type(value) is not str or not _KEY.fullmatch(value):
|
|
35
|
+
raise WorkflowContractError('audit key requires 16-100 ASCII identifier characters')
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _bounded(value: Any, maximum: int) -> None:
|
|
40
|
+
nodes = 0
|
|
41
|
+
def walk(item, depth):
|
|
42
|
+
nonlocal nodes
|
|
43
|
+
nodes += 1
|
|
44
|
+
if depth > 20 or nodes > 4096:
|
|
45
|
+
raise WorkflowContractError('audit JSON exceeds structure limits')
|
|
46
|
+
if isinstance(item, Mapping):
|
|
47
|
+
if len(item) > 128 or any(type(key) is not str for key in item):
|
|
48
|
+
raise WorkflowContractError('audit JSON has invalid object fields')
|
|
49
|
+
for child in item.values():
|
|
50
|
+
walk(child, depth + 1)
|
|
51
|
+
elif isinstance(item, (tuple, list)):
|
|
52
|
+
for child in item:
|
|
53
|
+
walk(child, depth + 1)
|
|
54
|
+
elif type(item) is int:
|
|
55
|
+
if not -(2**63) <= item <= 2**64 - 1:
|
|
56
|
+
raise WorkflowContractError('audit integer exceeds exact service JSON range')
|
|
57
|
+
elif type(item) is float:
|
|
58
|
+
if not math.isfinite(item):
|
|
59
|
+
raise WorkflowContractError('audit number must be finite')
|
|
60
|
+
elif item is not None and type(item) not in (str, bool):
|
|
61
|
+
raise WorkflowContractError('audit payload must contain JSON values')
|
|
62
|
+
walk(value, 0)
|
|
63
|
+
try:
|
|
64
|
+
size = len(json.dumps(value, ensure_ascii=False, allow_nan=False, separators=(',', ':')).encode('utf-8'))
|
|
65
|
+
except (ValueError, TypeError, UnicodeError, RecursionError) as exc:
|
|
66
|
+
raise WorkflowContractError('invalid audit JSON') from exc
|
|
67
|
+
if size > maximum:
|
|
68
|
+
raise WorkflowContractError('audit JSON exceeds byte limit')
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _exact_identity(value: VersionedIdentity) -> VersionedIdentity:
|
|
72
|
+
_identity(value)
|
|
73
|
+
for text, maximum in ((value.identity_id, 512), (value.version, 128)):
|
|
74
|
+
if len(text.encode('utf-8')) > maximum or any(unicodedata.category(char) == 'Cc' for char in text):
|
|
75
|
+
raise WorkflowContractError('audit identity exceeds service text limits')
|
|
76
|
+
return value
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _reason(value: Any) -> str:
|
|
80
|
+
if type(value) is not str or not value.strip():
|
|
81
|
+
raise WorkflowContractError('audit reason requires 1-8192 UTF-8 bytes')
|
|
82
|
+
try:
|
|
83
|
+
size = len(value.encode('utf-8'))
|
|
84
|
+
except UnicodeEncodeError as exc:
|
|
85
|
+
raise WorkflowContractError('audit reason requires valid UTF-8') from exc
|
|
86
|
+
if size > 8192:
|
|
87
|
+
raise WorkflowContractError('audit reason requires 1-8192 UTF-8 bytes')
|
|
88
|
+
return value
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(frozen=True)
|
|
92
|
+
class BankReleaseDeclaration:
|
|
93
|
+
"""Operator request. Contains identities/targets, no executable policy."""
|
|
94
|
+
release_id: str
|
|
95
|
+
bindings: Mapping[str, VersionedIdentity]
|
|
96
|
+
targets: tuple[ReviewTarget, ...] = field(repr=False)
|
|
97
|
+
idempotency_key: str
|
|
98
|
+
parent_release_id: str | None = None
|
|
99
|
+
_SCHEMA: ClassVar[str] = 'sphynx.bank-release-declaration.v1'
|
|
100
|
+
|
|
101
|
+
def __post_init__(self):
|
|
102
|
+
_identifier(self.release_id); _key(self.idempotency_key)
|
|
103
|
+
if self.parent_release_id is not None:
|
|
104
|
+
_identifier(self.parent_release_id)
|
|
105
|
+
if self.parent_release_id == self.release_id:
|
|
106
|
+
raise WorkflowContractError('release cannot be its own parent')
|
|
107
|
+
if (not isinstance(self.bindings, Mapping) or len(self.bindings) > 32 or
|
|
108
|
+
not {'bank', 'review_protocol', 'review_policy'} <= self.bindings.keys()):
|
|
109
|
+
raise WorkflowContractError('release needs exact bank/protocol/policy bindings')
|
|
110
|
+
bindings = {_identifier(key): _exact_identity(item) for key, item in self.bindings.items()}
|
|
111
|
+
if not isinstance(self.targets, (tuple, list)) or not 1 <= len(self.targets) <= 256:
|
|
112
|
+
raise WorkflowContractError('release needs 1-256 explicit targets')
|
|
113
|
+
if any(not isinstance(item, ReviewTarget) for item in self.targets):
|
|
114
|
+
raise WorkflowContractError('release targets must be ReviewTarget values')
|
|
115
|
+
if len({item.target_id for item in self.targets}) != len(self.targets):
|
|
116
|
+
raise WorkflowContractError('release has duplicate target identities')
|
|
117
|
+
for item in self.targets:
|
|
118
|
+
_identifier(item.target_id); _exact_identity(item.subject); _exact_identity(item.protocol)
|
|
119
|
+
_bounded(item.to_dict()['display'], 65536)
|
|
120
|
+
object.__setattr__(self, 'bindings', MappingProxyType(bindings))
|
|
121
|
+
object.__setattr__(self, 'targets', tuple(self.targets))
|
|
122
|
+
_bounded(self.to_dict(), 262144)
|
|
123
|
+
|
|
124
|
+
def to_dict(self) -> dict[str, Any]:
|
|
125
|
+
return {'schema_version': self._SCHEMA, 'release_id': self.release_id,
|
|
126
|
+
'parent_release_id': self.parent_release_id,
|
|
127
|
+
'bindings': {key: item.to_dict() for key, item in self.bindings.items()},
|
|
128
|
+
'targets': [item.to_dict() for item in self.targets], 'idempotency_key': self.idempotency_key}
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def from_dict(cls, value: Mapping[str, Any]) -> BankReleaseDeclaration:
|
|
132
|
+
values = _envelope(value, cls._SCHEMA, cls)
|
|
133
|
+
bindings, targets = values.get('bindings'), values.get('targets')
|
|
134
|
+
if not isinstance(bindings, Mapping) or not isinstance(targets, (tuple, list)):
|
|
135
|
+
raise WorkflowContractError('invalid release bindings/targets')
|
|
136
|
+
values['bindings'] = {key: VersionedIdentity.from_dict(item) for key, item in bindings.items()}
|
|
137
|
+
values['targets'] = tuple(ReviewTarget.from_dict(item) for item in targets)
|
|
138
|
+
return _construct(cls, values)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class ReviewAdjudication(_AuditWire):
|
|
143
|
+
target_id: str
|
|
144
|
+
decision: str
|
|
145
|
+
expected_version: int
|
|
146
|
+
idempotency_key: str
|
|
147
|
+
contribution_set_digest: str
|
|
148
|
+
rationale: str = field(repr=False)
|
|
149
|
+
_SCHEMA: ClassVar[str] = 'sphynx.review-adjudication-request.v1'
|
|
150
|
+
|
|
151
|
+
def __post_init__(self):
|
|
152
|
+
_identifier(self.target_id); _positive_version(self.expected_version); _key(self.idempotency_key)
|
|
153
|
+
if self.decision not in ('approve', 'reject'):
|
|
154
|
+
raise WorkflowContractError('adjudication requires approve or reject')
|
|
155
|
+
if type(self.contribution_set_digest) is not str or not _DIGEST.fullmatch(self.contribution_set_digest):
|
|
156
|
+
raise WorkflowContractError('adjudication needs the exact current contribution-set digest')
|
|
157
|
+
_reason(self.rationale); _bounded(self.to_dict(), 16384)
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def from_dict(cls, value: Mapping[str, Any]) -> ReviewAdjudication:
|
|
161
|
+
return _construct(cls, _envelope(value, cls._SCHEMA, cls))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True)
|
|
165
|
+
class BankReleaseTransition(_AuditWire):
|
|
166
|
+
action: str
|
|
167
|
+
expected_version: int
|
|
168
|
+
idempotency_key: str
|
|
169
|
+
reason: str = field(repr=False)
|
|
170
|
+
_SCHEMA: ClassVar[str] = 'sphynx.bank-release-transition.v1'
|
|
171
|
+
|
|
172
|
+
def __post_init__(self):
|
|
173
|
+
if self.action not in ('hold', 'resume', 'withdraw'):
|
|
174
|
+
raise WorkflowContractError('unknown release transition')
|
|
175
|
+
_positive_version(self.expected_version); _key(self.idempotency_key); _reason(self.reason)
|
|
176
|
+
_bounded(self.to_dict(), 16384)
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def from_dict(cls, value: Mapping[str, Any]) -> BankReleaseTransition:
|
|
180
|
+
return _construct(cls, _envelope(value, cls._SCHEMA, cls))
|