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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Explicit decision callbacks over permitted saved-report snapshots.
|
|
2
|
+
|
|
3
|
+
A verdict records a selected policy's outcome. It does not execute candidates,
|
|
4
|
+
create a grading pass, grant access, or upgrade native evidence authority.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import html
|
|
9
|
+
import inspect
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, Mapping, TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from .workflow_contracts import (
|
|
16
|
+
BackendDescriptor, ReferenceBinding, WorkflowContractError,
|
|
17
|
+
_object, _text, _thaw, contract_digest,
|
|
18
|
+
)
|
|
19
|
+
from .workflow_registry import CallbackDefinition
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from .workflows import Report
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DecisionPolicyError(WorkflowContractError):
|
|
26
|
+
"""A selected decision callback failed or returned an invalid outcome."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _reference_dict(reference: ReferenceBinding) -> dict:
|
|
30
|
+
return {"record_id": reference.record_id, "status": reference.status,
|
|
31
|
+
"trust_tier": reference.trust_tier, "run_id": reference.run_id,
|
|
32
|
+
"metadata": _thaw(reference.metadata)}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ReportSnapshot:
|
|
37
|
+
"""Immutable permitted report data and its native binding at read time."""
|
|
38
|
+
|
|
39
|
+
backend: BackendDescriptor
|
|
40
|
+
reference: ReferenceBinding
|
|
41
|
+
data: Mapping[str, Any]
|
|
42
|
+
|
|
43
|
+
def __post_init__(self) -> None:
|
|
44
|
+
if not isinstance(self.backend, BackendDescriptor) or not isinstance(self.reference, ReferenceBinding):
|
|
45
|
+
raise WorkflowContractError("report snapshot requires native metadata bindings")
|
|
46
|
+
object.__setattr__(self, "data", _object(self.data, "report snapshot"))
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def projection_digest(self) -> str:
|
|
50
|
+
return contract_digest(self.data)
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict:
|
|
53
|
+
return {"backend": self.backend.to_dict(), "reference": _reference_dict(self.reference),
|
|
54
|
+
"data": _thaw(self.data)}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class DecisionContext:
|
|
59
|
+
"""Policy inputs. Construction alone does not authenticate their contents."""
|
|
60
|
+
|
|
61
|
+
candidate: ReportSnapshot
|
|
62
|
+
baseline: ReportSnapshot | None = None
|
|
63
|
+
comparison: Any = None
|
|
64
|
+
parameters: Mapping[str, Any] = field(default_factory=dict)
|
|
65
|
+
|
|
66
|
+
def __post_init__(self) -> None:
|
|
67
|
+
if not isinstance(self.candidate, ReportSnapshot):
|
|
68
|
+
raise WorkflowContractError("decision candidate must be ReportSnapshot")
|
|
69
|
+
if self.baseline is not None and not isinstance(self.baseline, ReportSnapshot):
|
|
70
|
+
raise WorkflowContractError("decision baseline must be ReportSnapshot")
|
|
71
|
+
if self.baseline is None and self.comparison is not None:
|
|
72
|
+
raise WorkflowContractError("a comparison requires a baseline report")
|
|
73
|
+
object.__setattr__(self, "comparison", _object({"value": self.comparison}, "comparison")["value"])
|
|
74
|
+
object.__setattr__(self, "parameters", _object(self.parameters, "decision parameters"))
|
|
75
|
+
|
|
76
|
+
def to_dict(self) -> dict:
|
|
77
|
+
return {"candidate": self.candidate.to_dict(),
|
|
78
|
+
"baseline": self.baseline.to_dict() if self.baseline else None,
|
|
79
|
+
"comparison": _thaw(self.comparison), "parameters": _thaw(self.parameters)}
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def context_digest(self) -> str:
|
|
83
|
+
return contract_digest(self.to_dict())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class DecisionOutcome:
|
|
88
|
+
"""An explicit domain outcome and optional binary CI decision.
|
|
89
|
+
|
|
90
|
+
None means the policy did not establish a binary pass/fail result. There is
|
|
91
|
+
no default threshold, aggregation, comparison interpretation, or pass state.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
outcome: str
|
|
95
|
+
passed: bool | None
|
|
96
|
+
reason: str
|
|
97
|
+
details: Mapping[str, Any] = field(default_factory=dict)
|
|
98
|
+
|
|
99
|
+
def __post_init__(self) -> None:
|
|
100
|
+
_text(self.outcome, "outcome")
|
|
101
|
+
_text(self.reason, "reason")
|
|
102
|
+
if self.passed is not None and type(self.passed) is not bool:
|
|
103
|
+
raise WorkflowContractError("passed must be boolean or None")
|
|
104
|
+
object.__setattr__(self, "details", _object(self.details, "decision details"))
|
|
105
|
+
|
|
106
|
+
def to_dict(self) -> dict:
|
|
107
|
+
return {"outcome": self.outcome, "passed": self.passed,
|
|
108
|
+
"reason": self.reason, "details": _thaw(self.details)}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _markdown(value: str) -> str:
|
|
112
|
+
inline = " ".join(value.splitlines())
|
|
113
|
+
return re.sub(r"([\\`*_{}\[\]()#+.!|~=\-])", r"\\\1", html.escape(inline, quote=False))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass(frozen=True)
|
|
117
|
+
class Verdict:
|
|
118
|
+
"""A policy-bound result with read-only JSON, Markdown and CI projections."""
|
|
119
|
+
|
|
120
|
+
policy: CallbackDefinition
|
|
121
|
+
context: DecisionContext
|
|
122
|
+
result: DecisionOutcome
|
|
123
|
+
|
|
124
|
+
def __post_init__(self) -> None:
|
|
125
|
+
if not isinstance(self.policy, CallbackDefinition) or self.policy.kind != "decision":
|
|
126
|
+
raise WorkflowContractError("verdict requires a decision policy definition")
|
|
127
|
+
if not isinstance(self.context, DecisionContext) or not isinstance(self.result, DecisionOutcome):
|
|
128
|
+
raise WorkflowContractError("verdict requires decision context and outcome")
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def ci_exit_code(self) -> int:
|
|
132
|
+
"""0 for explicit pass, 1 for explicit failure, 2 for undetermined."""
|
|
133
|
+
return 2 if self.result.passed is None else (0 if self.result.passed else 1)
|
|
134
|
+
|
|
135
|
+
def to_dict(self) -> dict:
|
|
136
|
+
def identity(snapshot):
|
|
137
|
+
if snapshot is None:
|
|
138
|
+
return None
|
|
139
|
+
return {"backend_id": snapshot.backend.backend_id,
|
|
140
|
+
"backend_digest": snapshot.backend.descriptor_digest,
|
|
141
|
+
"reference": _reference_dict(snapshot.reference),
|
|
142
|
+
"projection_digest": snapshot.projection_digest}
|
|
143
|
+
|
|
144
|
+
return {"schema_version": "sphynx.verdict.v1", "policy": self.policy.to_dict(),
|
|
145
|
+
"context_digest": self.context.context_digest,
|
|
146
|
+
"candidate": identity(self.context.candidate), "baseline": identity(self.context.baseline),
|
|
147
|
+
"parameters": _thaw(self.context.parameters), "result": self.result.to_dict(),
|
|
148
|
+
"ci_exit_code": self.ci_exit_code}
|
|
149
|
+
|
|
150
|
+
def to_json(self) -> str:
|
|
151
|
+
return json.dumps(self.to_dict(), ensure_ascii=False, sort_keys=True, indent=2, allow_nan=False)
|
|
152
|
+
|
|
153
|
+
def to_markdown(self) -> str:
|
|
154
|
+
state = {True: "Requirements met", False: "Requirements not met", None: "Undetermined"}[self.result.passed]
|
|
155
|
+
lines = ["## Verdict", "", "- Decision: " + state,
|
|
156
|
+
"- Outcome: " + _markdown(self.result.outcome),
|
|
157
|
+
"- Policy: " + _markdown(self.policy.callback_id) + " @ " + _markdown(self.policy.version),
|
|
158
|
+
"- Candidate grading: " + _markdown(self.context.candidate.reference.record_id or "unidentified"),
|
|
159
|
+
"- Trust tier: " + _markdown(self.context.candidate.reference.trust_tier), ""]
|
|
160
|
+
lines.extend("> " + _markdown(line) for line in self.result.reason.splitlines())
|
|
161
|
+
return "\n".join(lines) + "\n"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _snapshot(report: Report) -> ReportSnapshot:
|
|
165
|
+
from .workflows import _reference
|
|
166
|
+
|
|
167
|
+
data = report.to_dict() # Native backend verifies and authorizes this projection.
|
|
168
|
+
backend = report._backend()
|
|
169
|
+
return ReportSnapshot(backend.descriptor, _reference(backend, report.native_ref, "grading"), data)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def decide_report(report: Report, policy_id: str, *, baseline: Report | None = None,
|
|
173
|
+
parameters: Mapping[str, Any] | None = None) -> Verdict:
|
|
174
|
+
"""Explicitly invoke a registered synchronous decision policy once.
|
|
175
|
+
|
|
176
|
+
Optional comparison uses Report.compare and keeps the backend's JSON meaning.
|
|
177
|
+
Opaque comparison objects need a backend-supplied to_dict projection. The
|
|
178
|
+
policy owns compatibility, population, missingness and domain decision rules.
|
|
179
|
+
Exceptions remain errors; no successful verdict is manufactured from them.
|
|
180
|
+
"""
|
|
181
|
+
from .workflows import Report
|
|
182
|
+
|
|
183
|
+
if not isinstance(report, Report) or (baseline is not None and not isinstance(baseline, Report)):
|
|
184
|
+
raise WorkflowContractError("decision inputs must be shared Report values")
|
|
185
|
+
registry = report.registry
|
|
186
|
+
assert registry is not None
|
|
187
|
+
definition = registry.get_policy(policy_id, kind="decision")
|
|
188
|
+
callback = registry.get_policy_callable(policy_id, kind="decision")
|
|
189
|
+
if inspect.iscoroutinefunction(callback):
|
|
190
|
+
raise DecisionPolicyError("decision callbacks must be synchronous")
|
|
191
|
+
parameters = _object({} if parameters is None else parameters, "decision parameters")
|
|
192
|
+
comparison = report.compare(baseline) if baseline is not None else None
|
|
193
|
+
if callable(getattr(comparison, "to_dict", None)):
|
|
194
|
+
comparison = comparison.to_dict()
|
|
195
|
+
context = DecisionContext(_snapshot(report), _snapshot(baseline) if baseline is not None else None,
|
|
196
|
+
comparison, parameters)
|
|
197
|
+
context_digest = context.context_digest
|
|
198
|
+
try:
|
|
199
|
+
result = callback(context)
|
|
200
|
+
except Exception as exc:
|
|
201
|
+
raise DecisionPolicyError("selected decision policy failed") from exc
|
|
202
|
+
if inspect.iscoroutine(result):
|
|
203
|
+
result.close()
|
|
204
|
+
if not isinstance(result, DecisionOutcome):
|
|
205
|
+
raise DecisionPolicyError("decision callback must return DecisionOutcome")
|
|
206
|
+
if context.context_digest != context_digest:
|
|
207
|
+
raise DecisionPolicyError("decision callback modified its input snapshot")
|
|
208
|
+
if (registry.get_policy(policy_id, kind="decision") != definition or
|
|
209
|
+
registry.get_policy_callable(policy_id, kind="decision") is not callback):
|
|
210
|
+
raise DecisionPolicyError("decision policy binding changed during execution")
|
|
211
|
+
return Verdict(definition, context, result)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Explicit built-in workflow bootstrap, separate from shared orchestration.
|
|
2
|
+
|
|
3
|
+
The factory imports the enabled adapter lazily and returns a fresh operator
|
|
4
|
+
registry. Importing this module neither reads case assets nor executes candidate,
|
|
5
|
+
evaluator, reference, policy, or projection callbacks.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from .workflow_registry import WorkflowRegistry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def default_workflow_registry() -> WorkflowRegistry:
|
|
13
|
+
"""Create the default extension bindings without a global mutable store."""
|
|
14
|
+
from .rerank_workflow import build_rerank_registry
|
|
15
|
+
|
|
16
|
+
return build_rerank_registry()
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Discover installed extension metadata, then explicitly enable reviewed code.
|
|
2
|
+
|
|
3
|
+
An installed distribution can include ``sphynx_extensions.json`` in its dist-info
|
|
4
|
+
directory and entry points in the ``sphynx.extensions`` group. Discovery never
|
|
5
|
+
loads entry points. An operator must approve the descriptor digest before
|
|
6
|
+
activation. The digest pins metadata; it does not authenticate package code.
|
|
7
|
+
Only enable packages whose installed code the operator already trusts.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from collections.abc import Iterable, Mapping
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from importlib import metadata
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .workflow_contracts import (
|
|
19
|
+
WorkflowContractError, _object, _text, _texts, _thaw, contract_digest,
|
|
20
|
+
)
|
|
21
|
+
from .workflow_registry import WorkflowRegistry
|
|
22
|
+
|
|
23
|
+
_MANIFEST = "sphynx_extensions.json"
|
|
24
|
+
_GROUP = "sphynx.extensions"
|
|
25
|
+
_CONTRACT = "sphynx.workflow.v2"
|
|
26
|
+
_FACTORY = re.compile(r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*:[A-Za-z_]\w*\Z", re.ASCII)
|
|
27
|
+
_ROLES = {
|
|
28
|
+
"backends": ("list_backends", "backend_id"),
|
|
29
|
+
"schemas": ("list_schemas", "schema_id"),
|
|
30
|
+
"datasets": ("list_datasets", "dataset_id"),
|
|
31
|
+
"providers": ("list_providers", "provider_id"),
|
|
32
|
+
"criteria": ("list_criteria", "criterion_id"),
|
|
33
|
+
"evaluators": ("list_evaluators", "evaluator_id"),
|
|
34
|
+
"metrics": ("list_metrics", "metric_id"),
|
|
35
|
+
"suites": ("list_suites", "suite_id"),
|
|
36
|
+
"policies": ("list_policies", "callback_id"),
|
|
37
|
+
"projections": ("list_projections", "callback_id"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ExtensionError(WorkflowContractError):
|
|
42
|
+
"""An installed extension is malformed, unapproved or incompatible."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _strict_json(raw: str) -> dict[str, Any]:
|
|
46
|
+
def pairs(items):
|
|
47
|
+
result = {}
|
|
48
|
+
for key, value in items:
|
|
49
|
+
if key in result:
|
|
50
|
+
raise ExtensionError("duplicate extension manifest field")
|
|
51
|
+
result[key] = value
|
|
52
|
+
return result
|
|
53
|
+
if type(raw) is not str or len(raw.encode("utf-8")) > 1_048_576:
|
|
54
|
+
raise ExtensionError("extension manifest must be UTF-8 JSON of at most 1 MiB")
|
|
55
|
+
try:
|
|
56
|
+
value = json.loads(raw, object_pairs_hook=pairs,
|
|
57
|
+
parse_constant=lambda value: (_ for _ in ()).throw(ValueError(value)))
|
|
58
|
+
except (ValueError, TypeError) as exc:
|
|
59
|
+
raise ExtensionError(f"invalid extension manifest: {exc}") from None
|
|
60
|
+
if not isinstance(value, dict):
|
|
61
|
+
raise ExtensionError("extension manifest must be an object")
|
|
62
|
+
return value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class PackagedExtension:
|
|
67
|
+
"""Installed metadata snapshot. This descriptor grants no execution trust."""
|
|
68
|
+
|
|
69
|
+
extension_id: str
|
|
70
|
+
version: str
|
|
71
|
+
implementation_id: str
|
|
72
|
+
distribution: str
|
|
73
|
+
distribution_version: str
|
|
74
|
+
entry_point: str
|
|
75
|
+
factory: str
|
|
76
|
+
contract_version: str
|
|
77
|
+
capabilities: tuple[str, ...] = ()
|
|
78
|
+
required_capabilities: tuple[str, ...] = ()
|
|
79
|
+
registrations: Mapping[str, Any] = field(default_factory=dict)
|
|
80
|
+
metadata: Mapping[str, Any] = field(default_factory=dict)
|
|
81
|
+
_installed_distribution: Any = field(default=None, repr=False, compare=False)
|
|
82
|
+
|
|
83
|
+
def __post_init__(self):
|
|
84
|
+
for name in ("extension_id", "version", "implementation_id", "distribution",
|
|
85
|
+
"distribution_version", "entry_point", "factory", "contract_version"):
|
|
86
|
+
_text(getattr(self, name), name)
|
|
87
|
+
if "." not in self.extension_id and ":" not in self.extension_id:
|
|
88
|
+
raise ExtensionError("extension_id must be namespaced")
|
|
89
|
+
if not _FACTORY.fullmatch(self.factory):
|
|
90
|
+
raise ExtensionError("factory must be an exact module:callable entry point without extras")
|
|
91
|
+
for name in ("capabilities", "required_capabilities"):
|
|
92
|
+
object.__setattr__(self, name, _texts(getattr(self, name), name, namespaced=True))
|
|
93
|
+
if not isinstance(self.registrations, Mapping) or set(self.registrations) - set(_ROLES):
|
|
94
|
+
raise ExtensionError("registrations contains an unknown registry role")
|
|
95
|
+
normalized = {role: list(_texts(values, role)) for role, values in self.registrations.items()}
|
|
96
|
+
object.__setattr__(self, "registrations", _object(normalized, "registrations"))
|
|
97
|
+
object.__setattr__(self, "metadata", _object(self.metadata, "metadata"))
|
|
98
|
+
|
|
99
|
+
def to_dict(self) -> dict[str, Any]:
|
|
100
|
+
return {"schema_version": "sphynx.packaged-extension.v1",
|
|
101
|
+
**{name: getattr(self, name) for name in (
|
|
102
|
+
"extension_id", "version", "implementation_id", "distribution",
|
|
103
|
+
"distribution_version", "entry_point", "factory", "contract_version")},
|
|
104
|
+
"capabilities": list(self.capabilities),
|
|
105
|
+
"required_capabilities": list(self.required_capabilities),
|
|
106
|
+
"registrations": _thaw(self.registrations), "metadata": _thaw(self.metadata)}
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def descriptor_digest(self) -> str:
|
|
110
|
+
return contract_digest(self.to_dict())
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _from_distribution(dist: Any) -> tuple[PackagedExtension, ...]:
|
|
114
|
+
raw = dist.read_text(_MANIFEST)
|
|
115
|
+
if raw is None:
|
|
116
|
+
return ()
|
|
117
|
+
manifest = _strict_json(raw)
|
|
118
|
+
if set(manifest) != {"schema_version", "extensions"} or manifest["schema_version"] != "sphynx.extensions.v1":
|
|
119
|
+
raise ExtensionError("unsupported extension manifest envelope")
|
|
120
|
+
rows = manifest["extensions"]
|
|
121
|
+
if not isinstance(rows, list) or not rows:
|
|
122
|
+
raise ExtensionError("extensions must be a nonempty array")
|
|
123
|
+
entries = {}
|
|
124
|
+
for ep in dist.entry_points:
|
|
125
|
+
if ep.group != _GROUP:
|
|
126
|
+
continue
|
|
127
|
+
if ep.name in entries:
|
|
128
|
+
raise ExtensionError("duplicate extension entry point")
|
|
129
|
+
entries[ep.name] = ep
|
|
130
|
+
result = []
|
|
131
|
+
required = {"extension_id", "version", "implementation_id", "entry_point", "contract_version", "registrations"}
|
|
132
|
+
optional = {"capabilities", "required_capabilities", "metadata"}
|
|
133
|
+
for row in rows:
|
|
134
|
+
if not isinstance(row, dict) or not required <= row.keys() or row.keys() - required - optional:
|
|
135
|
+
raise ExtensionError("invalid extension descriptor fields")
|
|
136
|
+
ep = entries.get(row["entry_point"])
|
|
137
|
+
if ep is None:
|
|
138
|
+
raise ExtensionError("extension has no unique installed entry point")
|
|
139
|
+
result.append(PackagedExtension(**row, distribution=dist.metadata["Name"],
|
|
140
|
+
distribution_version=dist.version, factory=ep.value,
|
|
141
|
+
_installed_distribution=dist))
|
|
142
|
+
if len({item.extension_id for item in result}) != len(result):
|
|
143
|
+
raise ExtensionError("duplicate extension ID in distribution")
|
|
144
|
+
return tuple(result)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def discover_extensions(*, distributions: Iterable[Any] | None = None) -> tuple[PackagedExtension, ...]:
|
|
148
|
+
"""Read installed metadata only. Never import or load a factory.
|
|
149
|
+
|
|
150
|
+
``distributions`` permits explicit installed-environment inventories and
|
|
151
|
+
offline tests. Malformed or duplicate declarations fail visibly.
|
|
152
|
+
"""
|
|
153
|
+
selected = metadata.distributions() if distributions is None else distributions
|
|
154
|
+
result = [item for dist in selected for item in _from_distribution(dist)]
|
|
155
|
+
if len({item.extension_id for item in result}) != len(result):
|
|
156
|
+
raise ExtensionError("duplicate installed extension ID; select an unambiguous environment")
|
|
157
|
+
return tuple(sorted(result, key=lambda item: item.extension_id))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _membership(registry: WorkflowRegistry) -> dict[str, set[str]]:
|
|
161
|
+
return {role: {getattr(item, identifier) for item in getattr(registry, method)()}
|
|
162
|
+
for role, (method, identifier) in _ROLES.items()}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def enable_extension(extension: PackagedExtension, registry: WorkflowRegistry, *,
|
|
166
|
+
approved_digest: str) -> WorkflowRegistry:
|
|
167
|
+
"""Explicitly load trusted installed code into a staged registry.
|
|
168
|
+
|
|
169
|
+
Approve a reviewed descriptor digest, then use the returned registry. The
|
|
170
|
+
original registration stores remain unchanged if activation fails. Trusted
|
|
171
|
+
factory side effects and shared implementation state are not sandboxed.
|
|
172
|
+
Factories accept one registry, register exactly their declared additions,
|
|
173
|
+
and return None. Candidate-supplied import strings are not accepted here.
|
|
174
|
+
"""
|
|
175
|
+
if not isinstance(extension, PackagedExtension) or not isinstance(registry, WorkflowRegistry):
|
|
176
|
+
raise ExtensionError("enable_extension requires discovered metadata and WorkflowRegistry")
|
|
177
|
+
if extension.descriptor_digest != approved_digest:
|
|
178
|
+
raise ExtensionError("extension descriptor has not been approved")
|
|
179
|
+
if extension.contract_version != _CONTRACT:
|
|
180
|
+
raise ExtensionError("extension requires an unsupported workflow contract")
|
|
181
|
+
dist = extension._installed_distribution
|
|
182
|
+
if dist is None:
|
|
183
|
+
raise ExtensionError("extension must come from an installed distribution inventory")
|
|
184
|
+
current = [item for item in _from_distribution(dist) if item.extension_id == extension.extension_id]
|
|
185
|
+
if len(current) != 1 or current[0].descriptor_digest != approved_digest:
|
|
186
|
+
raise ExtensionError("installed extension metadata changed after review")
|
|
187
|
+
available = {cap for backend in registry.list_backends() for cap in backend.capabilities}
|
|
188
|
+
if not set(extension.required_capabilities) <= available:
|
|
189
|
+
raise ExtensionError("extension requires unavailable backend capabilities")
|
|
190
|
+
entries = [ep for ep in dist.entry_points if ep.group == _GROUP and ep.name == extension.entry_point]
|
|
191
|
+
if len(entries) != 1 or entries[0].value != extension.factory:
|
|
192
|
+
raise ExtensionError("installed extension factory changed after review")
|
|
193
|
+
staged = registry.fork()
|
|
194
|
+
before = _membership(staged)
|
|
195
|
+
factory = entries[0].load()
|
|
196
|
+
if not callable(factory):
|
|
197
|
+
raise ExtensionError("installed extension factory is not callable")
|
|
198
|
+
if factory(staged) is not None:
|
|
199
|
+
raise ExtensionError("extension factory must register into its supplied registry and return None")
|
|
200
|
+
after = _membership(staged)
|
|
201
|
+
for role in _ROLES:
|
|
202
|
+
expected = set(extension.registrations.get(role, ()))
|
|
203
|
+
if after[role] - before[role] != expected or not before[role] <= after[role]:
|
|
204
|
+
raise ExtensionError(f"extension registered unexpected {role}")
|
|
205
|
+
provided = {cap for backend in staged.list_backends()
|
|
206
|
+
if backend.backend_id not in before["backends"] for cap in backend.capabilities}
|
|
207
|
+
if not set(extension.capabilities) <= provided:
|
|
208
|
+
raise ExtensionError("extension did not register its declared capabilities")
|
|
209
|
+
for suite in staged.list_suites():
|
|
210
|
+
staged.resolve_suite(suite.suite_id)
|
|
211
|
+
staged.fork() # Recheck validator/provider/backend pins after trusted factory execution.
|
|
212
|
+
return staged
|