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,218 @@
|
|
|
1
|
+
"""Trusted, offline SciFact development intake; never executes candidate code.
|
|
2
|
+
|
|
3
|
+
Only the pinned existing public-only bundle and native BEIR SciFact files are
|
|
4
|
+
accepted. Published sparse qrels are not converted into human negative labels.
|
|
5
|
+
Exporting data does not register a hosted benchmark or prove redistribution rights.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from types import MappingProxyType
|
|
14
|
+
|
|
15
|
+
from ..benchmarks import AssetRef, BenchmarkCase, BenchmarkRegistry, BenchmarkRelease, LabelCase, LabelSetRelease
|
|
16
|
+
from ..contracts import canonical_json
|
|
17
|
+
from ..public_bundle import load_public_rerank_bundle
|
|
18
|
+
from ..public_rerank import build_public_rerank_request, load_public_rerank_pack
|
|
19
|
+
from .contracts import HostedError
|
|
20
|
+
|
|
21
|
+
BENCHMARK_REF = "beir.scifact.bm25-top15.dev15@1.0.0"
|
|
22
|
+
LABEL_REF = "beir.scifact.native-qrels.dev15@1.0.0"
|
|
23
|
+
INPUT_REF = "beir.scifact.inputs.dev15@1.0.0"
|
|
24
|
+
REFERENCE_REF = "beir.scifact.references.dev15@1.0.0"
|
|
25
|
+
CORPUS_REF = "beir.scifact.pool-corpus.dev15@1.0.0"
|
|
26
|
+
EVALUATION_ID = "rag.rerank.public.v1"
|
|
27
|
+
METRIC_RELEASE = "rag.rerank.beir.metric.v1"
|
|
28
|
+
GRADER_RELEASE = "rag.rerank.scifact.dev15.grader.v1"
|
|
29
|
+
POLICY_REVISION = "native-sparse-qrels.unjudged-as-zero.pool-conditional.v1"
|
|
30
|
+
VERSION = "1.0.0"
|
|
31
|
+
INPUT_SCHEMA = "sphynx.hosted.public-rerank.inputs.v1"
|
|
32
|
+
REFERENCE_SCHEMA = "sphynx.hosted.public-rerank.references.v1"
|
|
33
|
+
METADATA_SCHEMA = "sphynx.hosted.public-benchmark.v1"
|
|
34
|
+
BUNDLE_SCHEMA = "sphynx.hosted.public-benchmark-bundle.v1"
|
|
35
|
+
FILES = ("inputs.json", "references.json", "benchmark.json")
|
|
36
|
+
REGISTERED_LOCATORS = MappingProxyType({INPUT_REF: "inputs.json", REFERENCE_REF: "references.json", BENCHMARK_REF: "benchmark.json"})
|
|
37
|
+
# Operator-owned byte pins. They are not supplied by a candidate or inferred from
|
|
38
|
+
# its freshly edited manifest. Tests substitute only fictional public fixtures.
|
|
39
|
+
SOURCE_PINS = MappingProxyType({
|
|
40
|
+
"manifest.json": "739df50af296cd5760c5499e8711005f7bba35ad30e2aaa08d667fa19c45465d",
|
|
41
|
+
"frozen-pools.json": "b3e6a31c62eccd90962bbce6372efe9bbf9fd397beedea7ea01d3a777e7d28f6",
|
|
42
|
+
"corpus.jsonl": "dec31c8182f3d744c7d2c09423756fd1d17cbef75808db13ba01cc0aab4d1ac6",
|
|
43
|
+
"queries.jsonl": "8ff84a7c903f722981cd8d595c022660140c51867b27608a6d4910db86080313",
|
|
44
|
+
"qrels/test.tsv": "0864bb985e0ca2367ba217977e72004d549054b2b06666ed9d4825ac7c21284c",
|
|
45
|
+
})
|
|
46
|
+
MAX_FILE_BYTES = 16 * 1024 * 1024
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _require(condition, code="invalid_public_benchmark"):
|
|
50
|
+
if not condition:
|
|
51
|
+
raise HostedError(code, "public benchmark inputs or provenance are invalid")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def encoded(value):
|
|
55
|
+
return (canonical_json(value) + "\n").encode("utf-8")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def byte_digest(raw):
|
|
59
|
+
return "sha256:" + hashlib.sha256(raw).hexdigest()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def read_json(raw):
|
|
63
|
+
_require(type(raw) is bytes and 0 < len(raw) <= MAX_FILE_BYTES)
|
|
64
|
+
def unique(pairs):
|
|
65
|
+
result = {}
|
|
66
|
+
for key, item in pairs:
|
|
67
|
+
_require(key not in result, "duplicate_public_field")
|
|
68
|
+
result[key] = item
|
|
69
|
+
return result
|
|
70
|
+
try:
|
|
71
|
+
return json.loads(raw, object_pairs_hook=unique,
|
|
72
|
+
parse_constant=lambda _: (_ for _ in ()).throw(ValueError()))
|
|
73
|
+
except (ValueError, UnicodeError, RecursionError):
|
|
74
|
+
raise HostedError("invalid_public_benchmark", "public benchmark JSON is invalid") from None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _pinned(root, relative, digest):
|
|
78
|
+
root = Path(root).absolute()
|
|
79
|
+
_require(root.is_dir() and not root.is_symlink())
|
|
80
|
+
path = root.joinpath(*relative.split("/"))
|
|
81
|
+
_require(path.is_file() and not path.is_symlink() and path.resolve().is_relative_to(root.resolve()))
|
|
82
|
+
_require(path.stat().st_size <= MAX_FILE_BYTES)
|
|
83
|
+
raw = path.read_bytes()
|
|
84
|
+
_require(byte_digest(raw) == "sha256:" + digest, "public_source_digest_mismatch")
|
|
85
|
+
return raw
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def corpus_projection(requests, references):
|
|
89
|
+
documents = {}
|
|
90
|
+
for request, reference in zip(requests, references):
|
|
91
|
+
for candidate, label in zip(request["candidates"], reference["pool"]):
|
|
92
|
+
doc = label["document_id"]
|
|
93
|
+
_require(doc not in documents or documents[doc] == candidate["input"])
|
|
94
|
+
documents[doc] = candidate["input"]
|
|
95
|
+
return {"schema_version": "sphynx.public-rerank.pool-corpus.v1", "documents": [
|
|
96
|
+
{"document_id": key, "input": value} for key, value in sorted(documents.items())]}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def build_release_records(inputs, references, corpus):
|
|
100
|
+
input_asset = AssetRef(INPUT_REF, VERSION, byte_digest(encoded(inputs)))
|
|
101
|
+
ref_asset = AssetRef(REFERENCE_REF, VERSION, byte_digest(encoded(references)))
|
|
102
|
+
corpus_asset = AssetRef(CORPUS_REF, VERSION, byte_digest(encoded(corpus)))
|
|
103
|
+
cases = tuple(BenchmarkCase(request["case_id"], byte_digest(encoded(request)), "scifact", "development",
|
|
104
|
+
"unclassified", "unclassified", None, len(request["query"].split())) for request in inputs["requests"])
|
|
105
|
+
labels = LabelSetRelease("sphynx.label-set-release.v1", LABEL_REF, VERSION, input_asset, corpus_asset,
|
|
106
|
+
ref_asset, "passage_qrels", POLICY_REVISION, METRIC_RELEASE, "native_public_qrels",
|
|
107
|
+
"native_qrels_unjudged", tuple(LabelCase(case.case_id, case.input_digest, "judged") for case in cases), (), ())
|
|
108
|
+
benchmark = BenchmarkRelease("sphynx.benchmark-release.v1", BENCHMARK_REF, VERSION, EVALUATION_ID,
|
|
109
|
+
GRADER_RELEASE, "sphynx.rag.rerank.v1", "component", input_asset, corpus_asset, labels.asset_ref,
|
|
110
|
+
"passage_qrels", METRIC_RELEASE, "any", "development", cases)
|
|
111
|
+
BenchmarkRegistry((labels,), (benchmark,)).resolve(BENCHMARK_REF)
|
|
112
|
+
return labels, benchmark
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True)
|
|
116
|
+
class PublicBenchmarkExport:
|
|
117
|
+
directory: Path
|
|
118
|
+
manifest_sha256: str
|
|
119
|
+
benchmark_ref: str
|
|
120
|
+
benchmark_digest: str
|
|
121
|
+
label_digest: str
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def export_scifact_development(public_bundle_dir, native_scifact_dir, output_dir):
|
|
125
|
+
"""Validate fixed public sources, then create a new four-file data package.
|
|
126
|
+
|
|
127
|
+
Candidate execution receives only inputs.json. Full sparse native judgments
|
|
128
|
+
for the selected queries and pool judgment-presence flags stay grader-only.
|
|
129
|
+
No network, cloud operations, model imports or execution occur.
|
|
130
|
+
"""
|
|
131
|
+
source_raw = _pinned(public_bundle_dir, "frozen-pools.json", SOURCE_PINS["frozen-pools.json"])
|
|
132
|
+
_pinned(public_bundle_dir, "manifest.json", SOURCE_PINS["manifest.json"])
|
|
133
|
+
bundle = load_public_rerank_bundle(public_bundle_dir, expected_manifest_sha256=SOURCE_PINS["manifest.json"])
|
|
134
|
+
_require(bundle.dataset_sha256 == SOURCE_PINS["frozen-pools.json"])
|
|
135
|
+
native_raw = {name: _pinned(native_scifact_dir, name, SOURCE_PINS[name]) for name in
|
|
136
|
+
("corpus.jsonl", "queries.jsonl", "qrels/test.tsv")}
|
|
137
|
+
def jsonl(raw):
|
|
138
|
+
return [read_json(line) for line in raw.splitlines() if line.strip()]
|
|
139
|
+
corpus, queries = {}, {}
|
|
140
|
+
for row in jsonl(native_raw["corpus.jsonl"]):
|
|
141
|
+
_require(type(row) is dict and type(row.get("_id")) is str and row["_id"] not in corpus)
|
|
142
|
+
_require(type(row.get("title")) is str and type(row.get("text")) is str)
|
|
143
|
+
corpus[row["_id"]] = (row["title"] + " " + row["text"]).strip()
|
|
144
|
+
for row in jsonl(native_raw["queries.jsonl"]):
|
|
145
|
+
_require(type(row) is dict and type(row.get("_id")) is str and row["_id"] not in queries and type(row.get("text")) is str)
|
|
146
|
+
queries[row["_id"]] = row["text"]
|
|
147
|
+
try:
|
|
148
|
+
lines = native_raw["qrels/test.tsv"].decode("utf-8").splitlines()
|
|
149
|
+
_require(lines[0] == "query-id\tcorpus-id\tscore")
|
|
150
|
+
qrels = {}
|
|
151
|
+
for line in lines[1:]:
|
|
152
|
+
query, doc, score = line.split("\t")
|
|
153
|
+
_require(query in queries and doc in corpus and score in ("0", "1", "2", "3"))
|
|
154
|
+
_require(doc not in qrels.setdefault(query, {}))
|
|
155
|
+
qrels[query][doc] = int(score)
|
|
156
|
+
except (ValueError, IndexError, UnicodeError):
|
|
157
|
+
raise HostedError("invalid_public_benchmark", "native qrels are invalid") from None
|
|
158
|
+
_require((len(corpus), len(queries), len(qrels), sum(map(len, qrels.values()))) == (5183, 1109, 300, 339))
|
|
159
|
+
pack = load_public_rerank_pack(bundle.dataset_path, split="development", expected_sha256=bundle.dataset_sha256)
|
|
160
|
+
selected = [row for row in pack.rows if row["dataset"] == "scifact"]
|
|
161
|
+
_require(len(selected) == 15)
|
|
162
|
+
requests, refs = [], []
|
|
163
|
+
for row in selected:
|
|
164
|
+
query_id = row["query_id"]
|
|
165
|
+
_require(query_id in qrels and row["query"] == queries.get(query_id) and any(v > 0 for v in qrels[query_id].values()))
|
|
166
|
+
labels = []
|
|
167
|
+
for candidate in row["candidates"]:
|
|
168
|
+
doc = candidate["document_id"]
|
|
169
|
+
_require(candidate["input"] == corpus.get(doc) and candidate["candidate_id"] == "scifact:" + doc)
|
|
170
|
+
native = qrels[query_id].get(doc)
|
|
171
|
+
_require(candidate["grade"] == (native if native is not None else 0), "native_qrel_alignment_mismatch")
|
|
172
|
+
labels.append({"candidate_id": candidate["candidate_id"], "document_id": doc,
|
|
173
|
+
"native_judged": native is not None, "native_grade": native})
|
|
174
|
+
requests.append(build_public_rerank_request(pack, row["task_id"]))
|
|
175
|
+
refs.append({"case_id": row["task_id"], "query_id": query_id,
|
|
176
|
+
"qrels": dict(sorted(qrels[query_id].items())), "pool": labels})
|
|
177
|
+
_require(sum(any(c["native_grade"] is not None and c["native_grade"] > 0 for c in row["pool"]) for row in refs) == 14)
|
|
178
|
+
_require(sum(c["native_judged"] for row in refs for c in row["pool"]) == 14)
|
|
179
|
+
inputs = {"schema_version": INPUT_SCHEMA, "benchmark_ref": BENCHMARK_REF, "requests": requests}
|
|
180
|
+
references = {"schema_version": REFERENCE_SCHEMA, "benchmark_ref": BENCHMARK_REF,
|
|
181
|
+
"policy_revision": POLICY_REVISION, "queries": refs}
|
|
182
|
+
projected_corpus = corpus_projection(requests, refs)
|
|
183
|
+
label_release, benchmark_release = build_release_records(inputs, references, projected_corpus)
|
|
184
|
+
metadata = {"schema_version": METADATA_SCHEMA, "benchmark_release": benchmark_release.to_dict(),
|
|
185
|
+
"label_release": label_release.to_dict(), "corpus_projection_schema": projected_corpus["schema_version"],
|
|
186
|
+
"registered_asset_locators": dict(REGISTERED_LOCATORS),
|
|
187
|
+
"provenance": {"source_files": {name: "sha256:" + value for name, value in sorted(SOURCE_PINS.items())},
|
|
188
|
+
"original_archive_url": "https://public.ukp.informatik.tu-darmstadt.de/thakur/BEIR/datasets/scifact.zip",
|
|
189
|
+
"original_archive_sha256": "sha256:536e14446a0ba56ed1398ab1055f39fe852686ecad24a6306c80c490fa8e0165",
|
|
190
|
+
"native_qrel_source": "qrels/test.tsv", "native_provenance_verified": True,
|
|
191
|
+
"review_receipts_verified": False, "human_review_claimed": False,
|
|
192
|
+
"selection": "existing_scifact_development_15_bm25_top15", "previously_opened": True,
|
|
193
|
+
"fresh_holdout": False, "use": "development_regression",
|
|
194
|
+
"licenses": [
|
|
195
|
+
{"asset": "claims_and_evidence", "license": "CC-BY-4.0", "source": "https://github.com/allenai/scifact/blob/master/LICENSE.md"},
|
|
196
|
+
{"asset": "abstracts", "license": "ODC-BY-1.0", "source": "https://github.com/allenai/scifact/blob/master/LICENSE.md"},
|
|
197
|
+
{"asset": "beir_repackaging_declared", "license": "CC-BY-SA-4.0", "source": "https://huggingface.co/datasets/BeIR/scifact"}],
|
|
198
|
+
"changes": ["select previously opened 15 development queries", "retain BM25 top15 order and scores",
|
|
199
|
+
"preserve absent native qrels as null with explicit effective-zero metric policy"],
|
|
200
|
+
"distribution_rights_inferred": False},
|
|
201
|
+
"population": {"case_count": 15, "pool_passages": 225, "native_judged_passages": 14,
|
|
202
|
+
"unjudged_passages": 211, "quality_eligible_cases": 14, "pool_miss_cases": 1},
|
|
203
|
+
"metric_policy": {"top_k": 5, "relevance_threshold": 1, "gain": "2**native_grade-1",
|
|
204
|
+
"unjudged_passage_effective_grade": 0, "unjudged_passages_are_human_negatives": False,
|
|
205
|
+
"quality_population": "queries_with_positive_native_qrel_in_frozen_pool",
|
|
206
|
+
"empty_positive_pool": "undefined", "failed_eligible_response": "missing",
|
|
207
|
+
"latency": "trusted_runner_process_wall_including_startup_and_model_loading"}}
|
|
208
|
+
contents = {"inputs.json": encoded(inputs), "references.json": encoded(references), "benchmark.json": encoded(metadata)}
|
|
209
|
+
manifest = {"schema_version": BUNDLE_SCHEMA, "benchmark_ref": BENCHMARK_REF,
|
|
210
|
+
"benchmark_digest": benchmark_release.digest, "label_digest": label_release.digest,
|
|
211
|
+
"files": {name: {"digest": byte_digest(raw), "size_bytes": len(raw)} for name, raw in contents.items()}}
|
|
212
|
+
root = Path(output_dir).absolute()
|
|
213
|
+
_require(not root.exists() and not root.is_symlink(), "public_export_exists")
|
|
214
|
+
root.mkdir(parents=True, exist_ok=False)
|
|
215
|
+
for name, raw in {**contents, "manifest.json": encoded(manifest)}.items():
|
|
216
|
+
with root.joinpath(name).open("xb") as stream:
|
|
217
|
+
stream.write(raw)
|
|
218
|
+
return PublicBenchmarkExport(root, byte_digest(encoded(manifest)), BENCHMARK_REF, benchmark_release.digest, label_release.digest)
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
"""Strict, versioned configuration for the first hosted reranking pilot.
|
|
2
|
+
|
|
3
|
+
Parsing never installs dependencies, downloads weights, or executes candidate code.
|
|
4
|
+
Version 1 retains the standard-library CPU profile. A single version 2 model
|
|
5
|
+
configuration requires separate operator admission before source publication.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
import re
|
|
13
|
+
import tomllib
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any, Mapping
|
|
16
|
+
|
|
17
|
+
from ..contracts import INTERFACE_VERSION, CandidateSpec, ContractError, canonical_json
|
|
18
|
+
|
|
19
|
+
CONFIG_SCHEMA = "sphynx.candidate.repository.v1"
|
|
20
|
+
FAMILY_SCHEMA = "sphynx.candidate.family.v1"
|
|
21
|
+
FAMILY_SELECTION_SCHEMA = "sphynx.candidate.family-selection.v1"
|
|
22
|
+
FAMILY_SUBMISSION_SCHEMA = "sphynx.hosted.prepared-submission.v2"
|
|
23
|
+
MAX_FAMILY_METHODS = 8
|
|
24
|
+
RUNTIME_PROFILE = "python-cpu.v1"
|
|
25
|
+
MAX_CONFIG_BYTES = 65_536
|
|
26
|
+
MAX_FILE_BYTES = 262_144
|
|
27
|
+
MAX_SOURCE_BYTES = 1_048_576
|
|
28
|
+
MAX_SOURCE_FILES = 16
|
|
29
|
+
_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]*$")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class HostedError(ValueError):
|
|
33
|
+
"""A payload-free diagnostic with a stable error code."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, code: str, message: str):
|
|
36
|
+
self.code = code
|
|
37
|
+
super().__init__(f"{code}: {message}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fields(value: object, expected: set[str], label: str) -> Mapping[str, Any]:
|
|
41
|
+
if not isinstance(value, Mapping) or set(value) != expected:
|
|
42
|
+
raise HostedError("invalid_configuration", f"{label} has missing or unsupported fields")
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def stable_id(value: object, label: str) -> str:
|
|
47
|
+
if not isinstance(value, str) or not _ID.fullmatch(value):
|
|
48
|
+
raise HostedError("invalid_configuration", f"{label} must be a stable identifier")
|
|
49
|
+
return value
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def source_path(value: object) -> str:
|
|
53
|
+
if not isinstance(value, str) or not value or len(value) > 240:
|
|
54
|
+
raise HostedError("invalid_source_path", "source paths must be short relative paths")
|
|
55
|
+
parts = value.split("/")
|
|
56
|
+
if any(part in ("", ".", "..") or part.startswith(".") for part in parts):
|
|
57
|
+
raise HostedError("invalid_source_path", "source paths cannot contain hidden or traversal components")
|
|
58
|
+
if not re.fullmatch(r"[A-Za-z0-9_./-]+", value):
|
|
59
|
+
raise HostedError("invalid_source_path", "source paths must use portable relative names")
|
|
60
|
+
return value
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def argv(value: object, label: str) -> tuple[str, ...]:
|
|
64
|
+
if not isinstance(value, list) or not 1 <= len(value) <= 32:
|
|
65
|
+
raise HostedError("invalid_configuration", f"{label} must be an argv array")
|
|
66
|
+
if any(not isinstance(arg, str) or not arg.strip() or "\x00" in arg or len(arg) > 1024 for arg in value):
|
|
67
|
+
raise HostedError("invalid_configuration", f"{label} contains invalid arguments")
|
|
68
|
+
if value[0] != "python":
|
|
69
|
+
raise HostedError("unsupported_runtime", f"{label} must use the platform Python interpreter")
|
|
70
|
+
return tuple(value)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass(frozen=True)
|
|
74
|
+
class CandidateConfig:
|
|
75
|
+
name: str
|
|
76
|
+
version: str
|
|
77
|
+
interface_version: str
|
|
78
|
+
entry_point: tuple[str, ...]
|
|
79
|
+
source_files: tuple[str, ...]
|
|
80
|
+
execution_kind: str
|
|
81
|
+
build_commands: tuple[tuple[str, ...], ...]
|
|
82
|
+
dependencies: tuple[str, ...]
|
|
83
|
+
runtime_profile: str
|
|
84
|
+
python: str
|
|
85
|
+
model_kind: str
|
|
86
|
+
method_json: str | None = None
|
|
87
|
+
|
|
88
|
+
def to_dict(self) -> dict[str, Any]:
|
|
89
|
+
result = {
|
|
90
|
+
"schema_version": CONFIG_SCHEMA,
|
|
91
|
+
"candidate": {
|
|
92
|
+
"name": self.name, "version": self.version,
|
|
93
|
+
"interface_version": self.interface_version,
|
|
94
|
+
"entry_point": list(self.entry_point), "source_files": list(self.source_files),
|
|
95
|
+
"execution_kind": self.execution_kind,
|
|
96
|
+
},
|
|
97
|
+
"build": {"commands": [list(command) for command in self.build_commands],
|
|
98
|
+
"dependencies": list(self.dependencies)},
|
|
99
|
+
"runtime": {"profile": self.runtime_profile, "python": self.python},
|
|
100
|
+
"model": {"kind": self.model_kind},
|
|
101
|
+
}
|
|
102
|
+
if self.method_json is not None:
|
|
103
|
+
result["method"] = json.loads(self.method_json)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def digest(self) -> str:
|
|
108
|
+
return "sha256:" + hashlib.sha256(canonical_json(self.to_dict()).encode("utf-8")).hexdigest()
|
|
109
|
+
|
|
110
|
+
def candidate_spec(self) -> CandidateSpec:
|
|
111
|
+
return CandidateSpec(self.name, self.version, self.entry_point, self.interface_version)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _load_configuration(payload: bytes) -> Mapping[str, Any]:
|
|
115
|
+
if not isinstance(payload, bytes) or not 0 < len(payload) <= MAX_CONFIG_BYTES:
|
|
116
|
+
raise HostedError("invalid_configuration", "configuration must be bounded UTF-8 TOML")
|
|
117
|
+
try:
|
|
118
|
+
raw = tomllib.loads(payload.decode("utf-8"))
|
|
119
|
+
except (UnicodeDecodeError, tomllib.TOMLDecodeError, RecursionError):
|
|
120
|
+
raise HostedError("invalid_configuration", "configuration is not valid UTF-8 TOML") from None
|
|
121
|
+
return raw
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_candidate_config(payload: bytes) -> CandidateConfig:
|
|
125
|
+
"""Parse a single configuration; model parsing does not grant admission."""
|
|
126
|
+
raw = _load_configuration(payload)
|
|
127
|
+
if raw.get("schema_version") == "sphynx.candidate.repository.v2":
|
|
128
|
+
return _parse_registered_model(raw)
|
|
129
|
+
return _parse_candidate_config(raw)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _parse_registered_model(raw):
|
|
133
|
+
try:
|
|
134
|
+
from .model_contracts import parse_model_configuration
|
|
135
|
+
except ModuleNotFoundError as exc:
|
|
136
|
+
if exc.name != __package__ + ".model_contracts":
|
|
137
|
+
raise
|
|
138
|
+
raise HostedError("models_not_admitted", "this platform payload does not admit model candidates") from None
|
|
139
|
+
return parse_model_configuration(raw)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _parse_candidate_config(raw: Mapping[str, Any]) -> CandidateConfig:
|
|
143
|
+
if not isinstance(raw, Mapping):
|
|
144
|
+
raise HostedError("invalid_configuration", "candidate configuration must be a table")
|
|
145
|
+
fields(raw, {"schema_version", "candidate", "build", "runtime", "model"} |
|
|
146
|
+
({"method"} if "method" in raw else set()), "configuration")
|
|
147
|
+
if raw["schema_version"] != CONFIG_SCHEMA:
|
|
148
|
+
raise HostedError("unsupported_configuration_version", "expected " + CONFIG_SCHEMA)
|
|
149
|
+
candidate = fields(raw["candidate"], {
|
|
150
|
+
"name", "version", "interface_version", "entry_point", "source_files", "execution_kind",
|
|
151
|
+
}, "candidate")
|
|
152
|
+
build = fields(raw["build"], {"commands", "dependencies"}, "build")
|
|
153
|
+
runtime = fields(raw["runtime"], {"profile", "python"}, "runtime")
|
|
154
|
+
model = fields(raw["model"], {"kind"}, "model")
|
|
155
|
+
name = stable_id(candidate["name"], "candidate.name")
|
|
156
|
+
version = stable_id(candidate["version"], "candidate.version")
|
|
157
|
+
if candidate["interface_version"] != INTERFACE_VERSION:
|
|
158
|
+
raise HostedError("unsupported_interface", "expected " + INTERFACE_VERSION)
|
|
159
|
+
command = argv(candidate["entry_point"], "candidate.entry_point")
|
|
160
|
+
if len(command) < 2 or not command[1].endswith(".py"):
|
|
161
|
+
raise HostedError("invalid_entry_point", "entry point must be python followed by a declared .py file")
|
|
162
|
+
entry_file = source_path(command[1])
|
|
163
|
+
sources = candidate["source_files"]
|
|
164
|
+
if not isinstance(sources, list) or not 1 <= len(sources) <= MAX_SOURCE_FILES:
|
|
165
|
+
raise HostedError("invalid_configuration", "source_files must declare 1 to 16 Python files")
|
|
166
|
+
sources = tuple(source_path(path) for path in sources)
|
|
167
|
+
if any(not path.endswith(".py") for path in sources) or len({path.casefold() for path in sources}) != len(sources):
|
|
168
|
+
raise HostedError("invalid_configuration", "source_files must contain unique portable .py paths")
|
|
169
|
+
if entry_file not in sources:
|
|
170
|
+
raise HostedError("invalid_entry_point", "entry point must appear in source_files")
|
|
171
|
+
if candidate["execution_kind"] != "reranker":
|
|
172
|
+
raise HostedError("unsupported_execution_kind", "the first pilot supports fresh reranker execution only")
|
|
173
|
+
commands = build["commands"]
|
|
174
|
+
if not isinstance(commands, list) or len(commands) > 8:
|
|
175
|
+
raise HostedError("invalid_configuration", "build.commands must contain at most eight argv arrays")
|
|
176
|
+
commands = tuple(argv(command, "build.commands") for command in commands)
|
|
177
|
+
if not isinstance(build["dependencies"], list):
|
|
178
|
+
raise HostedError("invalid_configuration", "build.dependencies must be an array")
|
|
179
|
+
if build["dependencies"]:
|
|
180
|
+
raise HostedError("dependencies_not_supported", "this pilot requires dependencies = []; an offline dependency broker is pending")
|
|
181
|
+
if runtime["profile"] != RUNTIME_PROFILE or runtime["python"] != "3.12":
|
|
182
|
+
raise HostedError("unsupported_runtime", "this pilot requires python-cpu.v1 with Python 3.12")
|
|
183
|
+
if model["kind"] != "none":
|
|
184
|
+
raise HostedError("models_not_supported", "this pilot requires model.kind = none; pinned model artifacts are pending")
|
|
185
|
+
method_json = None
|
|
186
|
+
if "method" in raw:
|
|
187
|
+
method = fields(raw["method"], {"description", "parameters", "random_seeds", "determinism"}, "method")
|
|
188
|
+
if not isinstance(method["description"], str) or not 1 <= len(method["description"]) <= 2048:
|
|
189
|
+
raise HostedError("invalid_configuration", "method.description must contain 1 to 2048 characters")
|
|
190
|
+
if method["determinism"] not in ("deterministic", "stochastic", "unspecified"):
|
|
191
|
+
raise HostedError("invalid_configuration", "method.determinism must be deterministic, stochastic, or unspecified")
|
|
192
|
+
if (not isinstance(method["random_seeds"], list) or len(method["random_seeds"]) > 16 or
|
|
193
|
+
any(type(seed) is not int or not 0 <= seed < 2**64 for seed in method["random_seeds"])):
|
|
194
|
+
raise HostedError("invalid_configuration", "method.random_seeds must contain at most 16 nonnegative integer seeds")
|
|
195
|
+
def validate_parameter(value, depth=0):
|
|
196
|
+
if depth > 5: raise HostedError("invalid_configuration", "method parameters are too deeply nested")
|
|
197
|
+
if isinstance(value, dict):
|
|
198
|
+
if len(value) > 32 or any(not isinstance(key, str) or not 1 <= len(key) <= 128 for key in value):
|
|
199
|
+
raise HostedError("invalid_configuration", "method parameter keys are invalid")
|
|
200
|
+
for item in value.values(): validate_parameter(item, depth + 1)
|
|
201
|
+
elif isinstance(value, list):
|
|
202
|
+
if len(value) > 64: raise HostedError("invalid_configuration", "method parameter array is too long")
|
|
203
|
+
for item in value: validate_parameter(item, depth + 1)
|
|
204
|
+
elif not (type(value) in (bool, int) or isinstance(value, str) and len(value) <= 2048 or
|
|
205
|
+
type(value) is float and math.isfinite(value)):
|
|
206
|
+
raise HostedError("invalid_configuration", "method parameters must be finite JSON values")
|
|
207
|
+
if not isinstance(method["parameters"], dict):
|
|
208
|
+
raise HostedError("invalid_configuration", "method.parameters must be a table")
|
|
209
|
+
validate_parameter(method["parameters"])
|
|
210
|
+
method_json = canonical_json(dict(method))
|
|
211
|
+
config = CandidateConfig(name, version, INTERFACE_VERSION, command, sources, "reranker",
|
|
212
|
+
commands, (), RUNTIME_PROFILE, "3.12", "none", method_json)
|
|
213
|
+
try:
|
|
214
|
+
config.candidate_spec()
|
|
215
|
+
except ContractError:
|
|
216
|
+
raise HostedError("invalid_configuration", "candidate does not satisfy the existing CandidateSpec") from None
|
|
217
|
+
return config
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def candidate_method_id(value: object) -> str:
|
|
221
|
+
"""Case-sensitive selector, never a path or an executable expression."""
|
|
222
|
+
if type(value) is not str or not 1 <= len(value) <= 128 or not _ID.fullmatch(value):
|
|
223
|
+
raise HostedError("invalid_candidate_method", "candidate_method must be a stable identifier of at most 128 characters")
|
|
224
|
+
return value
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@dataclass(frozen=True)
|
|
228
|
+
class CandidateFamily:
|
|
229
|
+
name: str
|
|
230
|
+
version: str
|
|
231
|
+
default_method: str | None
|
|
232
|
+
methods: tuple[tuple[str, CandidateConfig], ...]
|
|
233
|
+
|
|
234
|
+
def __post_init__(self):
|
|
235
|
+
stable_id(self.name, "family.name"); stable_id(self.version, "family.version")
|
|
236
|
+
if len(self.name) > 128 or len(self.version) > 128:
|
|
237
|
+
raise HostedError("invalid_configuration", "family name and version must contain at most 128 characters")
|
|
238
|
+
if (type(self.methods) is not tuple or not 1 <= len(self.methods) <= MAX_FAMILY_METHODS
|
|
239
|
+
or any(type(item) is not tuple or len(item) != 2 or not isinstance(item[1], CandidateConfig) for item in self.methods)):
|
|
240
|
+
raise HostedError("invalid_configuration", "family methods must be a bounded immutable configuration list")
|
|
241
|
+
names = tuple(candidate_method_id(key) for key, _ in self.methods)
|
|
242
|
+
if len(set(names)) != len(names):
|
|
243
|
+
raise HostedError("invalid_configuration", "family method names must be unique")
|
|
244
|
+
if self.default_method is not None and candidate_method_id(self.default_method) not in names:
|
|
245
|
+
raise HostedError("candidate_method_not_found", "the declared family default is not one of its methods")
|
|
246
|
+
|
|
247
|
+
def to_dict(self) -> dict[str, Any]:
|
|
248
|
+
family = {"name": self.name, "version": self.version}
|
|
249
|
+
if self.default_method is not None:
|
|
250
|
+
family["default_method"] = self.default_method
|
|
251
|
+
return {"schema_version": FAMILY_SCHEMA, "family": family,
|
|
252
|
+
"methods": {key: config.to_dict() for key, config in self.methods}}
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def digest(self) -> str:
|
|
256
|
+
return "sha256:" + hashlib.sha256(canonical_json(self.to_dict()).encode()).hexdigest()
|
|
257
|
+
|
|
258
|
+
def select(self, candidate_method: str | None = None) -> tuple[str, CandidateConfig]:
|
|
259
|
+
key = candidate_method_id(candidate_method) if candidate_method is not None else self.default_method
|
|
260
|
+
if key is None:
|
|
261
|
+
raise HostedError("candidate_method_required", "this family has no default; specify one of its declared method names")
|
|
262
|
+
config = next((config for name, config in self.methods if name == key), None)
|
|
263
|
+
if config is None:
|
|
264
|
+
raise HostedError("candidate_method_not_found", "candidate_method is not declared in the pinned family manifest")
|
|
265
|
+
return key, config
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _parse_candidate_family(raw: Mapping[str, Any]) -> CandidateFamily:
|
|
269
|
+
fields(raw, {"schema_version", "family", "methods"}, "candidate family")
|
|
270
|
+
if raw["schema_version"] != FAMILY_SCHEMA:
|
|
271
|
+
raise HostedError("unsupported_configuration_version", "expected " + FAMILY_SCHEMA)
|
|
272
|
+
family = fields(raw["family"], {"name", "version"} |
|
|
273
|
+
({"default_method"} if isinstance(raw["family"], Mapping) and "default_method" in raw["family"] else set()), "family")
|
|
274
|
+
name, version = stable_id(family["name"], "family.name"), stable_id(family["version"], "family.version")
|
|
275
|
+
methods = raw["methods"]
|
|
276
|
+
if not isinstance(methods, Mapping) or not 1 <= len(methods) <= MAX_FAMILY_METHODS:
|
|
277
|
+
raise HostedError("invalid_configuration", "a family must declare 1 to 8 named methods")
|
|
278
|
+
parsed = tuple((candidate_method_id(key), _parse_candidate_config(value)) for key, value in sorted(methods.items()))
|
|
279
|
+
default = candidate_method_id(family["default_method"]) if "default_method" in family else None
|
|
280
|
+
if default is not None and default not in methods:
|
|
281
|
+
raise HostedError("candidate_method_not_found", "the declared family default is not one of its methods")
|
|
282
|
+
return CandidateFamily(name, version, default, parsed)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def parse_candidate_family(payload: bytes) -> CandidateFamily:
|
|
286
|
+
"""Validate all declared configurations; never fetch or execute any method."""
|
|
287
|
+
return _parse_candidate_family(_load_configuration(payload))
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@dataclass(frozen=True)
|
|
291
|
+
class SelectedCandidateRepository:
|
|
292
|
+
config: CandidateConfig
|
|
293
|
+
family: CandidateFamily | None = None
|
|
294
|
+
candidate_method: str | None = None
|
|
295
|
+
manifest_digest: str | None = None
|
|
296
|
+
|
|
297
|
+
def family_selection(self) -> dict[str, Any] | None:
|
|
298
|
+
if self.family is None:
|
|
299
|
+
return None
|
|
300
|
+
return {"schema_version": FAMILY_SELECTION_SCHEMA, "name": self.family.name,
|
|
301
|
+
"version": self.family.version, "method": self.candidate_method,
|
|
302
|
+
"manifest_digest": self.manifest_digest, "configuration_digest": self.family.digest}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def parse_candidate_repository(payload: bytes, *, candidate_method: str | None = None) -> SelectedCandidateRepository:
|
|
306
|
+
if candidate_method is not None:
|
|
307
|
+
candidate_method = candidate_method_id(candidate_method)
|
|
308
|
+
raw = _load_configuration(payload)
|
|
309
|
+
if raw.get("schema_version") == FAMILY_SCHEMA:
|
|
310
|
+
family = _parse_candidate_family(raw)
|
|
311
|
+
key, config = family.select(candidate_method)
|
|
312
|
+
return SelectedCandidateRepository(config, family, key, "sha256:" + hashlib.sha256(payload).hexdigest())
|
|
313
|
+
if candidate_method is not None:
|
|
314
|
+
raise HostedError("candidate_method_not_supported", "single-candidate v1 repositories do not accept candidate_method")
|
|
315
|
+
if raw.get("schema_version") == "sphynx.candidate.repository.v2":
|
|
316
|
+
return SelectedCandidateRepository(_parse_registered_model(raw))
|
|
317
|
+
return SelectedCandidateRepository(_parse_candidate_config(raw))
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def verify_candidate_repository(payload: bytes, manifest: Mapping[str, Any]) -> SelectedCandidateRepository:
|
|
321
|
+
"""Reparse frozen root bytes and bind the selected config and exact source closure.
|
|
322
|
+
|
|
323
|
+
Source byte/hash verification belongs to the caller; this verifies that the
|
|
324
|
+
frozen manifest can only build the configuration selected from those bytes.
|
|
325
|
+
"""
|
|
326
|
+
if not isinstance(manifest, Mapping):
|
|
327
|
+
raise HostedError("candidate_selection_mismatch", "prepared candidate selection is invalid")
|
|
328
|
+
selection = parse_candidate_repository(payload, candidate_method=manifest.get("candidate_method"))
|
|
329
|
+
is_model = selection.config.model_kind == "registered"
|
|
330
|
+
if is_model:
|
|
331
|
+
from .model_contracts import MODEL_SUBMISSION_SCHEMA
|
|
332
|
+
if selection.family is None:
|
|
333
|
+
expected_schema = MODEL_SUBMISSION_SCHEMA if is_model else "sphynx.hosted.prepared-submission.v1"
|
|
334
|
+
if (manifest.get("schema_version") != expected_schema
|
|
335
|
+
or "candidate_method" in manifest or "candidate_family" in manifest):
|
|
336
|
+
raise HostedError("candidate_selection_mismatch", "single-candidate manifest contains a family selection")
|
|
337
|
+
elif (manifest.get("schema_version") != FAMILY_SUBMISSION_SCHEMA
|
|
338
|
+
or manifest.get("candidate_method") != selection.candidate_method
|
|
339
|
+
or manifest.get("candidate_family") != selection.family_selection()):
|
|
340
|
+
raise HostedError("candidate_selection_mismatch", "family selection differs from the frozen root manifest")
|
|
341
|
+
if is_model:
|
|
342
|
+
from .model_admission import require_model_authorization
|
|
343
|
+
if any(not isinstance(manifest.get(key), Mapping) for key in ("base_repository", "head_repository", "suite", "baseline")):
|
|
344
|
+
raise HostedError("candidate_selection_mismatch", "prepared model source and evaluation identities are invalid")
|
|
345
|
+
require_model_authorization(manifest.get("model_runtime"), team_id=manifest.get("team_id"),
|
|
346
|
+
base_repository_id=manifest.get("base_repository", {}).get("id"),
|
|
347
|
+
head_repository_id=manifest.get("head_repository", {}).get("id"),
|
|
348
|
+
suite=manifest.get("suite", {}).get("id"), baseline=manifest.get("baseline", {}).get("id"))
|
|
349
|
+
if manifest.get("requested_runtime_profile") != selection.config.runtime_profile:
|
|
350
|
+
raise HostedError("candidate_selection_mismatch", "requested model runtime differs from selected configuration")
|
|
351
|
+
elif "model_runtime" in manifest:
|
|
352
|
+
raise HostedError("candidate_selection_mismatch", "legacy candidate cannot inherit model admission")
|
|
353
|
+
files = manifest.get("files")
|
|
354
|
+
if (not isinstance(files, list) or any(not isinstance(item, Mapping) for item in files)
|
|
355
|
+
or any(type(item.get("path")) is not str for item in files)
|
|
356
|
+
or len(files) != len(selection.config.source_files) + 1
|
|
357
|
+
or {item.get("path") for item in files} != {"sphynx.toml", *selection.config.source_files}
|
|
358
|
+
or manifest.get("candidate_configuration") != selection.config.to_dict()
|
|
359
|
+
or manifest.get("configuration_digest") != selection.config.digest):
|
|
360
|
+
raise HostedError("candidate_selection_mismatch", "configuration or declared source closure differs from the selected method")
|
|
361
|
+
return selection
|