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/evidence.py
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"""Versioned, content-addressed local run bundles.
|
|
2
|
+
|
|
3
|
+
Only label-free case inputs and candidate process observations belong here.
|
|
4
|
+
References and grading results live outside captures. Files are written once
|
|
5
|
+
under a new run ID; readers never repair or rewrite historical evidence.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Mapping
|
|
16
|
+
|
|
17
|
+
from .contracts import canonical_json
|
|
18
|
+
from .records import EVIDENCE_SCHEMA_VERSION, EvidenceCapability
|
|
19
|
+
|
|
20
|
+
RUN_SCHEMA_VERSION = "sphynx.run.v1"
|
|
21
|
+
ARTIFACT_SCHEMA_VERSION = "sphynx.artifact.v1"
|
|
22
|
+
EVENT_SCHEMA_VERSION = "sphynx.event.v1"
|
|
23
|
+
COVERAGE_SCHEMA_VERSION = "sphynx.coverage.v1"
|
|
24
|
+
RUNNER_CLOCK_DOMAIN = "python.monotonic_ns.process_local"
|
|
25
|
+
RUN_ID_PATTERN = re.compile(r"run-[0-9a-f]{32}\Z")
|
|
26
|
+
CAPTURE_ID_PATTERN = re.compile(r"cap-[0-9a-f]{32}\Z")
|
|
27
|
+
_CAPABILITIES = frozenset({
|
|
28
|
+
"candidate_input", "case_snapshot", "candidate_stdout_raw",
|
|
29
|
+
"candidate_stderr_raw", "candidate_response_validated", "runner_timing",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EvidenceError(ValueError):
|
|
34
|
+
"""A saved run is missing, altered, or uses an unsupported schema."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class RunRef:
|
|
39
|
+
"""Durable, absolute reference to one completed local run bundle."""
|
|
40
|
+
|
|
41
|
+
run_id: str
|
|
42
|
+
path: Path
|
|
43
|
+
pack_id: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def sha256_bytes(data: bytes) -> str:
|
|
47
|
+
return "sha256:" + hashlib.sha256(data).hexdigest()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _new_bytes(path: Path, data: bytes) -> None:
|
|
51
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
with path.open("xb") as stream:
|
|
53
|
+
stream.write(data)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def write_json(path: Path, value: Any) -> str:
|
|
57
|
+
"""Write canonical JSON once and return its exact byte digest."""
|
|
58
|
+
|
|
59
|
+
data = (canonical_json(value) + "\n").encode("utf-8")
|
|
60
|
+
_new_bytes(path, data)
|
|
61
|
+
return sha256_bytes(data)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def write_jsonl(path: Path, rows: list[Mapping[str, Any]]) -> str:
|
|
65
|
+
data = "".join(canonical_json(row) + "\n" for row in rows).encode("utf-8")
|
|
66
|
+
_new_bytes(path, data)
|
|
67
|
+
return sha256_bytes(data)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def write_blob(
|
|
71
|
+
capture_dir: Path,
|
|
72
|
+
*,
|
|
73
|
+
role: str,
|
|
74
|
+
data: bytes,
|
|
75
|
+
media_type: str,
|
|
76
|
+
observed_bytes: int,
|
|
77
|
+
complete: bool,
|
|
78
|
+
observed: bool = True,
|
|
79
|
+
) -> dict[str, Any]:
|
|
80
|
+
"""Save retained bytes and describe whether they are the complete original.
|
|
81
|
+
|
|
82
|
+
Identical blobs may share a content address within one capture. A truncated
|
|
83
|
+
stream's digest authenticates its retained prefix, never the unseen suffix.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
digest = sha256_bytes(data)
|
|
87
|
+
relative_path = "blobs/" + digest.removeprefix("sha256:")
|
|
88
|
+
path = capture_dir / relative_path
|
|
89
|
+
if path.exists():
|
|
90
|
+
if path.read_bytes() != data:
|
|
91
|
+
raise EvidenceError(f"content-address collision at {path}")
|
|
92
|
+
else:
|
|
93
|
+
_new_bytes(path, data)
|
|
94
|
+
return {
|
|
95
|
+
"schema_version": ARTIFACT_SCHEMA_VERSION,
|
|
96
|
+
"role": role,
|
|
97
|
+
"path": relative_path,
|
|
98
|
+
"sha256": digest,
|
|
99
|
+
"retained_bytes": len(data),
|
|
100
|
+
"observed_bytes": observed_bytes,
|
|
101
|
+
"complete": complete,
|
|
102
|
+
"observed": observed,
|
|
103
|
+
"media_type": media_type,
|
|
104
|
+
"source": "runner_observed",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _read_json(path: Path) -> dict[str, Any]:
|
|
109
|
+
try:
|
|
110
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
111
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
112
|
+
raise EvidenceError(f"cannot read JSON evidence {path}: {exc}") from exc
|
|
113
|
+
if not isinstance(value, dict):
|
|
114
|
+
raise EvidenceError(f"evidence {path} must be a JSON object")
|
|
115
|
+
return value
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _safe_capture_dir(run_path: Path, capture_id: str) -> Path:
|
|
119
|
+
if not isinstance(capture_id, str) or not CAPTURE_ID_PATTERN.fullmatch(capture_id):
|
|
120
|
+
raise EvidenceError(f"invalid capture ID {capture_id!r}")
|
|
121
|
+
return run_path / "captures" / capture_id
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def open_run(path: str | Path | RunRef) -> RunRef:
|
|
125
|
+
"""Reopen a completed run by absolute or caller-relative bundle path.
|
|
126
|
+
|
|
127
|
+
Integrity checking is separate in :func:`verify_run`. Readers can inspect
|
|
128
|
+
the manifest even when a particular capture has become corrupt.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
run_path = path.path if isinstance(path, RunRef) else Path(path)
|
|
132
|
+
run_path = run_path.expanduser().resolve()
|
|
133
|
+
manifest = _read_json(run_path / "manifest.json")
|
|
134
|
+
if manifest.get("schema_version") != RUN_SCHEMA_VERSION:
|
|
135
|
+
raise EvidenceError("unsupported run schema_version")
|
|
136
|
+
run_id = manifest.get("run_id")
|
|
137
|
+
if not isinstance(run_id, str) or not RUN_ID_PATTERN.fullmatch(run_id) or run_path.name != run_id:
|
|
138
|
+
raise EvidenceError("run ID does not match bundle directory")
|
|
139
|
+
pack_id = manifest.get("pack_id")
|
|
140
|
+
if not isinstance(pack_id, str) or not pack_id:
|
|
141
|
+
raise EvidenceError("run pack_id is missing")
|
|
142
|
+
if manifest.get("status") not in ("completed", "completed_with_failures"):
|
|
143
|
+
raise EvidenceError("run is not terminal")
|
|
144
|
+
return RunRef(run_id, run_path, pack_id)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def read_run_manifest(run_ref: RunRef | str | Path) -> dict[str, Any]:
|
|
148
|
+
ref = open_run(run_ref)
|
|
149
|
+
return _read_json(ref.path / "manifest.json")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def read_capture_manifest(run_ref: RunRef | str | Path, capture_id: str) -> dict[str, Any]:
|
|
153
|
+
ref = open_run(run_ref)
|
|
154
|
+
return _checked_capture_manifest(ref, capture_id)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _checked_capture_manifest(ref: RunRef, capture_id: str) -> dict[str, Any]:
|
|
158
|
+
root = _read_json(ref.path / "manifest.json")
|
|
159
|
+
expected = root.get("capture_manifest_hashes")
|
|
160
|
+
if not isinstance(expected, dict) or capture_id not in expected:
|
|
161
|
+
raise EvidenceError(f"capture {capture_id} is not indexed by the run")
|
|
162
|
+
path = _safe_capture_dir(ref.path, capture_id) / "manifest.json"
|
|
163
|
+
try:
|
|
164
|
+
raw = path.read_bytes()
|
|
165
|
+
except OSError as exc:
|
|
166
|
+
raise EvidenceError(f"cannot read capture manifest {capture_id}: {exc}") from exc
|
|
167
|
+
if sha256_bytes(raw) != expected[capture_id]:
|
|
168
|
+
raise EvidenceError(f"capture manifest hash mismatch: {capture_id}")
|
|
169
|
+
return _read_json(path)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def read_capture_artifact(run_ref: RunRef | str | Path, capture_id: str, role: str) -> bytes:
|
|
173
|
+
"""Return exact retained bytes only after checking its index and digest."""
|
|
174
|
+
|
|
175
|
+
ref = open_run(run_ref)
|
|
176
|
+
capture_dir = _safe_capture_dir(ref.path, capture_id)
|
|
177
|
+
capture = _checked_capture_manifest(ref, capture_id)
|
|
178
|
+
index_path = capture_dir / "artifacts.jsonl"
|
|
179
|
+
try:
|
|
180
|
+
index_bytes = index_path.read_bytes()
|
|
181
|
+
except OSError as exc:
|
|
182
|
+
raise EvidenceError(f"cannot read artifact index {capture_id}: {exc}") from exc
|
|
183
|
+
if sha256_bytes(index_bytes) != capture.get("artifacts_sha256"):
|
|
184
|
+
raise EvidenceError(f"artifact index hash mismatch: {capture_id}")
|
|
185
|
+
artifacts = _read_jsonl(index_path)
|
|
186
|
+
matches = [row for row in artifacts if row.get("role") == role]
|
|
187
|
+
if len(matches) != 1:
|
|
188
|
+
raise EvidenceError(f"expected one {role!r} artifact for {capture_id}")
|
|
189
|
+
return _verified_artifact_bytes(capture_dir, matches[0])
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _read_jsonl(path: Path) -> list[dict[str, Any]]:
|
|
193
|
+
try:
|
|
194
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
195
|
+
rows = [json.loads(line) for line in lines]
|
|
196
|
+
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
|
|
197
|
+
raise EvidenceError(f"cannot read JSONL evidence {path}: {exc}") from exc
|
|
198
|
+
if any(not isinstance(row, dict) for row in rows):
|
|
199
|
+
raise EvidenceError(f"evidence {path} must contain JSON objects")
|
|
200
|
+
return rows
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _verified_artifact_bytes(capture_dir: Path, artifact: Mapping[str, Any]) -> bytes:
|
|
204
|
+
digest = artifact.get("sha256")
|
|
205
|
+
if not isinstance(digest, str) or not digest.startswith("sha256:") or len(digest) != 71:
|
|
206
|
+
raise EvidenceError("artifact has invalid SHA-256 digest")
|
|
207
|
+
expected_path = "blobs/" + digest.removeprefix("sha256:")
|
|
208
|
+
if artifact.get("path") != expected_path:
|
|
209
|
+
raise EvidenceError("artifact path is not its content address")
|
|
210
|
+
try:
|
|
211
|
+
data = (capture_dir / expected_path).read_bytes()
|
|
212
|
+
except OSError as exc:
|
|
213
|
+
raise EvidenceError(f"cannot read artifact {expected_path}: {exc}") from exc
|
|
214
|
+
if sha256_bytes(data) != digest or len(data) != artifact.get("retained_bytes"):
|
|
215
|
+
raise EvidenceError(f"artifact hash/size mismatch: {artifact.get('role')}")
|
|
216
|
+
return data
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def verify_run(run_ref: RunRef | str | Path) -> RunRef:
|
|
220
|
+
"""Check manifests, all indexed artifacts, and all metadata file hashes."""
|
|
221
|
+
|
|
222
|
+
ref = open_run(run_ref)
|
|
223
|
+
manifest = _read_json(ref.path / "manifest.json")
|
|
224
|
+
if manifest.get("trust_tier") != "local_unverified" or manifest.get("provenance") != "runner_observed":
|
|
225
|
+
raise EvidenceError("unsupported run trust tier or provenance")
|
|
226
|
+
cases = manifest.get("cases")
|
|
227
|
+
if not isinstance(cases, list) or manifest.get("case_count") != len(cases):
|
|
228
|
+
raise EvidenceError("run case_count mismatch")
|
|
229
|
+
case_by_id: dict[str, dict[str, Any]] = {}
|
|
230
|
+
for item in cases:
|
|
231
|
+
if not isinstance(item, dict) or not isinstance(item.get("case_id"), str):
|
|
232
|
+
raise EvidenceError("run case identity is invalid")
|
|
233
|
+
if item["case_id"] in case_by_id:
|
|
234
|
+
raise EvidenceError("run has duplicate case IDs")
|
|
235
|
+
case_by_id[item["case_id"]] = item
|
|
236
|
+
policy = manifest.get("execution_policy")
|
|
237
|
+
if not isinstance(policy, dict) or type(policy.get("trials")) is not int or policy["trials"] < 1:
|
|
238
|
+
raise EvidenceError("run execution policy is invalid")
|
|
239
|
+
capture_ids = manifest.get("capture_ids")
|
|
240
|
+
if not isinstance(capture_ids, list) or any(not isinstance(value, str) for value in capture_ids) or len(set(capture_ids)) != len(capture_ids):
|
|
241
|
+
raise EvidenceError("run capture_ids are missing or duplicated")
|
|
242
|
+
if manifest.get("capture_count") != len(capture_ids):
|
|
243
|
+
raise EvidenceError("run capture_count mismatch")
|
|
244
|
+
if len(capture_ids) != len(cases) * policy["trials"]:
|
|
245
|
+
raise EvidenceError("run capture count does not match fixed case/trial workload")
|
|
246
|
+
hashes = manifest.get("capture_manifest_hashes")
|
|
247
|
+
if not isinstance(hashes, dict) or set(hashes) != set(capture_ids):
|
|
248
|
+
raise EvidenceError("run capture-manifest hashes do not match IDs")
|
|
249
|
+
statuses: list[str] = []
|
|
250
|
+
for capture_id in capture_ids:
|
|
251
|
+
capture_dir = _safe_capture_dir(ref.path, capture_id)
|
|
252
|
+
manifest_path = capture_dir / "manifest.json"
|
|
253
|
+
try:
|
|
254
|
+
manifest_bytes = manifest_path.read_bytes()
|
|
255
|
+
except OSError as exc:
|
|
256
|
+
raise EvidenceError(f"cannot read capture manifest {capture_id}: {exc}") from exc
|
|
257
|
+
if sha256_bytes(manifest_bytes) != hashes[capture_id]:
|
|
258
|
+
raise EvidenceError(f"capture manifest hash mismatch: {capture_id}")
|
|
259
|
+
capture = _read_json(manifest_path)
|
|
260
|
+
if capture.get("schema_version") != EVIDENCE_SCHEMA_VERSION:
|
|
261
|
+
raise EvidenceError(f"unsupported capture schema_version: {capture_id}")
|
|
262
|
+
identity = capture.get("identity")
|
|
263
|
+
if not isinstance(identity, dict) or identity.get("run_id") != ref.run_id or identity.get("capture_id") != capture_id:
|
|
264
|
+
raise EvidenceError(f"capture identity mismatch: {capture_id}")
|
|
265
|
+
if identity.get("schema_version") != EVIDENCE_SCHEMA_VERSION or identity.get("trust_tier") != "local_unverified":
|
|
266
|
+
raise EvidenceError(f"unsupported capture schema: {capture_id}")
|
|
267
|
+
case = case_by_id.get(identity.get("case_id"))
|
|
268
|
+
if case is None or identity.get("pool_digest") != case.get("pool_digest") or capture.get("query_id") != case.get("query_id"):
|
|
269
|
+
raise EvidenceError(f"capture does not match run case/pool: {capture_id}")
|
|
270
|
+
status = capture.get("status")
|
|
271
|
+
if status not in {"success", "timeout", "crash", "spawn_error", "process_error", "input_write_error", "output_truncated", "invalid_response"}:
|
|
272
|
+
raise EvidenceError(f"invalid terminal status: {capture_id}")
|
|
273
|
+
if (status == "success") != (capture.get("failure_code") is None):
|
|
274
|
+
raise EvidenceError(f"terminal failure code mismatch: {capture_id}")
|
|
275
|
+
statuses.append(status)
|
|
276
|
+
for filename, field in (
|
|
277
|
+
("events.jsonl", "events_sha256"),
|
|
278
|
+
("artifacts.jsonl", "artifacts_sha256"),
|
|
279
|
+
("coverage.json", "coverage_sha256"),
|
|
280
|
+
):
|
|
281
|
+
try:
|
|
282
|
+
data = (capture_dir / filename).read_bytes()
|
|
283
|
+
except OSError as exc:
|
|
284
|
+
raise EvidenceError(f"cannot read {filename} for {capture_id}: {exc}") from exc
|
|
285
|
+
if sha256_bytes(data) != capture.get(field):
|
|
286
|
+
raise EvidenceError(f"{filename} hash mismatch: {capture_id}")
|
|
287
|
+
artifacts = _read_jsonl(capture_dir / "artifacts.jsonl")
|
|
288
|
+
roles = [row.get("role") for row in artifacts]
|
|
289
|
+
if len(roles) != len(set(roles)):
|
|
290
|
+
raise EvidenceError(f"duplicate artifact role: {capture_id}")
|
|
291
|
+
if not {"candidate_request", "case_snapshot", "candidate_stdout", "candidate_stderr"}.issubset(roles):
|
|
292
|
+
raise EvidenceError(f"required artifacts missing: {capture_id}")
|
|
293
|
+
for artifact in artifacts:
|
|
294
|
+
if artifact.get("schema_version") != ARTIFACT_SCHEMA_VERSION:
|
|
295
|
+
raise EvidenceError(f"unsupported artifact schema: {capture_id}")
|
|
296
|
+
_verified_artifact_bytes(capture_dir, artifact)
|
|
297
|
+
events = _read_jsonl(capture_dir / "events.jsonl")
|
|
298
|
+
if any(row.get("schema_version") != EVENT_SCHEMA_VERSION
|
|
299
|
+
or row.get("clock_domain") != RUNNER_CLOCK_DOMAIN
|
|
300
|
+
or row.get("actor") != "runner"
|
|
301
|
+
or row.get("source") != "runner_observed" for row in events):
|
|
302
|
+
raise EvidenceError(f"unsupported event schema, clock, or provenance: {capture_id}")
|
|
303
|
+
if [row.get("sequence") for row in events] != list(range(1, len(events) + 1)):
|
|
304
|
+
raise EvidenceError(f"event order mismatch: {capture_id}")
|
|
305
|
+
event_ids = [row.get("event_id") for row in events]
|
|
306
|
+
if len(set(event_ids)) != len(event_ids):
|
|
307
|
+
raise EvidenceError(f"duplicate event ID: {capture_id}")
|
|
308
|
+
monotonic = [row.get("monotonic_ns") for row in events]
|
|
309
|
+
if any(type(value) is not int for value in monotonic) or monotonic != sorted(monotonic):
|
|
310
|
+
raise EvidenceError(f"non-monotonic event timeline: {capture_id}")
|
|
311
|
+
if not events or events[-1].get("event_type") != "case_terminal" or events[-1].get("status") != status:
|
|
312
|
+
raise EvidenceError(f"terminal event mismatch: {capture_id}")
|
|
313
|
+
if any(row.get("capture_id") != capture_id for row in events):
|
|
314
|
+
raise EvidenceError(f"event capture ID mismatch: {capture_id}")
|
|
315
|
+
timing = capture.get("timing")
|
|
316
|
+
if not isinstance(timing, dict) or type(timing.get("elapsed_ns")) is not int or timing["elapsed_ns"] < 0:
|
|
317
|
+
raise EvidenceError(f"invalid capture timing: {capture_id}")
|
|
318
|
+
if timing.get("clock_domain") != RUNNER_CLOCK_DOMAIN:
|
|
319
|
+
raise EvidenceError(f"unsupported capture timing clock: {capture_id}")
|
|
320
|
+
if type(timing.get("started_monotonic_ns")) is not int or type(timing.get("ended_monotonic_ns")) is not int:
|
|
321
|
+
raise EvidenceError(f"invalid capture timing endpoints: {capture_id}")
|
|
322
|
+
if timing.get("ended_monotonic_ns", 0) - timing.get("started_monotonic_ns", 0) != timing["elapsed_ns"]:
|
|
323
|
+
raise EvidenceError(f"capture timing span mismatch: {capture_id}")
|
|
324
|
+
latency_ns = timing.get("candidate_latency_ns")
|
|
325
|
+
if latency_ns is not None and (type(latency_ns) is not int or latency_ns < 0 or latency_ns > timing["elapsed_ns"]):
|
|
326
|
+
raise EvidenceError(f"invalid candidate latency: {capture_id}")
|
|
327
|
+
coverage = _read_json(capture_dir / "coverage.json")
|
|
328
|
+
if coverage.get("schema_version") != COVERAGE_SCHEMA_VERSION or coverage.get("capture_id") != capture_id:
|
|
329
|
+
raise EvidenceError(f"unsupported coverage schema: {capture_id}")
|
|
330
|
+
capabilities = coverage.get("capabilities")
|
|
331
|
+
if not isinstance(capabilities, list) or len(capabilities) != len(_CAPABILITIES):
|
|
332
|
+
raise EvidenceError(f"invalid coverage capability inventory: {capture_id}")
|
|
333
|
+
ids: set[str] = set()
|
|
334
|
+
for item in capabilities:
|
|
335
|
+
if not isinstance(item, dict) or set(item) != {"capability_id", "capability_schema_version", "state", "observed_count", "reason"}:
|
|
336
|
+
raise EvidenceError(f"invalid coverage capability record: {capture_id}")
|
|
337
|
+
if item.get("capability_schema_version") != "v1":
|
|
338
|
+
raise EvidenceError(f"unsupported capability schema_version: {capture_id}")
|
|
339
|
+
try:
|
|
340
|
+
capability = EvidenceCapability(**item)
|
|
341
|
+
except (TypeError, ValueError) as exc:
|
|
342
|
+
raise EvidenceError(f"invalid coverage capability: {capture_id}: {exc}") from exc
|
|
343
|
+
ids.add(capability.capability_id)
|
|
344
|
+
if ids != _CAPABILITIES:
|
|
345
|
+
raise EvidenceError(f"coverage capability IDs differ from runner profile: {capture_id}")
|
|
346
|
+
successes = sum(status == "success" for status in statuses)
|
|
347
|
+
if manifest.get("success_count") != successes or manifest.get("failure_count") != len(statuses) - successes:
|
|
348
|
+
raise EvidenceError("run terminal counts mismatch")
|
|
349
|
+
expected_status = "completed" if successes == len(statuses) else "completed_with_failures"
|
|
350
|
+
if manifest.get("status") != expected_status:
|
|
351
|
+
raise EvidenceError("run terminal status mismatch")
|
|
352
|
+
return ref
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Exact opt-in execution paths; legacy drivers keep their own control checks."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .evaluation_protocols import _envelope
|
|
9
|
+
from .workflow_contracts import BackendDescriptor, WorkflowContractError, _text, _texts
|
|
10
|
+
|
|
11
|
+
PATH_SCHEMA = "sphynx.execution-path.v1"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ExecutionPath:
|
|
16
|
+
"""One scope/location pair. Declarations do not implement enforcement."""
|
|
17
|
+
|
|
18
|
+
scope: str
|
|
19
|
+
mode: str
|
|
20
|
+
operations: tuple[str, ...] = ("execute",)
|
|
21
|
+
capabilities: tuple[str, ...] = ()
|
|
22
|
+
enforcement: tuple[str, ...] = ()
|
|
23
|
+
|
|
24
|
+
def __post_init__(self) -> None:
|
|
25
|
+
_text(self.scope, "scope")
|
|
26
|
+
_text(self.mode, "mode")
|
|
27
|
+
for name in ("operations", "capabilities", "enforcement"):
|
|
28
|
+
object.__setattr__(self, name, _texts(getattr(self, name), name,
|
|
29
|
+
namespaced=name == "capabilities"))
|
|
30
|
+
if not self.operations:
|
|
31
|
+
raise WorkflowContractError("execution path must declare operations")
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> dict[str, Any]:
|
|
34
|
+
return {"schema_version": PATH_SCHEMA, "scope": self.scope, "mode": self.mode,
|
|
35
|
+
"operations": list(self.operations), "capabilities": list(self.capabilities),
|
|
36
|
+
"enforcement": list(self.enforcement)}
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_dict(cls, value: Mapping[str, Any]) -> ExecutionPath:
|
|
40
|
+
try:
|
|
41
|
+
return cls(**_envelope(value, PATH_SCHEMA, cls))
|
|
42
|
+
except TypeError as exc:
|
|
43
|
+
raise WorkflowContractError("invalid execution path fields") from exc
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def execution_paths(descriptor: BackendDescriptor) -> tuple[ExecutionPath, ...] | None:
|
|
47
|
+
"""Validate exact opt-in paths. None means historical/native validation."""
|
|
48
|
+
if not isinstance(descriptor, BackendDescriptor):
|
|
49
|
+
raise WorkflowContractError("execution support requires BackendDescriptor")
|
|
50
|
+
if "execution_paths" not in descriptor.metadata:
|
|
51
|
+
return None
|
|
52
|
+
raw = descriptor.metadata["execution_paths"]
|
|
53
|
+
if not isinstance(raw, (list, tuple)) or not raw:
|
|
54
|
+
raise WorkflowContractError("execution_paths must be a nonempty sequence")
|
|
55
|
+
paths = tuple(ExecutionPath.from_dict(item) for item in raw)
|
|
56
|
+
pairs = {(item.scope, item.mode) for item in paths}
|
|
57
|
+
if len(pairs) != len(paths):
|
|
58
|
+
raise WorkflowContractError("execution path pairs must be unique")
|
|
59
|
+
for item in paths:
|
|
60
|
+
if item.scope not in descriptor.supported_scopes or item.mode not in descriptor.supported_modes:
|
|
61
|
+
raise WorkflowContractError("execution path exceeds declared scopes/modes")
|
|
62
|
+
if not set(item.capabilities).issubset(descriptor.capabilities):
|
|
63
|
+
raise WorkflowContractError("execution path exceeds declared capabilities")
|
|
64
|
+
return paths
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def validate_execution_requirement(
|
|
68
|
+
descriptor: BackendDescriptor, *, scope: str | None = None,
|
|
69
|
+
mode: str | None = None, operation: str | None = None,
|
|
70
|
+
capabilities: tuple[str, ...] = (), enforcement: str | None = None,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Reject unsupported requests without reading cases or starting work.
|
|
73
|
+
|
|
74
|
+
A legacy descriptor has no general operation/enforcement claim. Its native
|
|
75
|
+
methods must still validate controls; this helper never enables them.
|
|
76
|
+
Explicit enforcement requests require an exact path declaration.
|
|
77
|
+
"""
|
|
78
|
+
paths = execution_paths(descriptor)
|
|
79
|
+
if scope is not None:
|
|
80
|
+
_text(scope, "scope")
|
|
81
|
+
if scope not in descriptor.supported_scopes:
|
|
82
|
+
raise WorkflowContractError("backend does not support scope " + repr(scope))
|
|
83
|
+
if mode is not None:
|
|
84
|
+
_text(mode, "mode")
|
|
85
|
+
if mode not in descriptor.supported_modes:
|
|
86
|
+
raise WorkflowContractError("backend does not support mode " + repr(mode))
|
|
87
|
+
if operation is not None:
|
|
88
|
+
_text(operation, "operation")
|
|
89
|
+
requested = set(_texts(capabilities, "required capabilities", namespaced=True))
|
|
90
|
+
if enforcement is not None:
|
|
91
|
+
_text(enforcement, "enforcement")
|
|
92
|
+
if not requested.issubset(descriptor.capabilities):
|
|
93
|
+
raise WorkflowContractError("backend lacks required execution capabilities")
|
|
94
|
+
if paths is None:
|
|
95
|
+
if enforcement is not None:
|
|
96
|
+
raise WorkflowContractError("legacy backend does not declare requested enforcement")
|
|
97
|
+
return
|
|
98
|
+
matches = [item for item in paths
|
|
99
|
+
if (scope is None or item.scope == scope) and (mode is None or item.mode == mode)]
|
|
100
|
+
if not matches:
|
|
101
|
+
raise WorkflowContractError("backend does not support this scope/mode pair")
|
|
102
|
+
if operation is not None:
|
|
103
|
+
matches = [item for item in matches if operation in item.operations]
|
|
104
|
+
if not matches:
|
|
105
|
+
raise WorkflowContractError("execution path does not support operation " + repr(operation))
|
|
106
|
+
matches = [item for item in matches if requested.issubset(item.capabilities)
|
|
107
|
+
and (enforcement is None or enforcement in item.enforcement)]
|
|
108
|
+
if not matches:
|
|
109
|
+
raise WorkflowContractError("execution path lacks requested capabilities/enforcement")
|