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.
Files changed (174) hide show
  1. sable_sphynx-0.7.0.dev0.dist-info/METADATA +152 -0
  2. sable_sphynx-0.7.0.dev0.dist-info/RECORD +174 -0
  3. sable_sphynx-0.7.0.dev0.dist-info/WHEEL +5 -0
  4. sable_sphynx-0.7.0.dev0.dist-info/top_level.txt +1 -0
  5. sphynx/__init__.py +469 -0
  6. sphynx/__init__.pyi +376 -0
  7. sphynx/_analytical_client.py +77 -0
  8. sphynx/_analytical_values.py +252 -0
  9. sphynx/_audit_client.py +173 -0
  10. sphynx/_comparison_adapters.py +113 -0
  11. sphynx/_componentized_full_rag.py +34 -0
  12. sphynx/_composed_pipeline.py +109 -0
  13. sphynx/_full_rag_grading.py +218 -0
  14. sphynx/_full_rag_trace.py +221 -0
  15. sphynx/_local_eval.py +268 -0
  16. sphynx/_pipeline_core.py +243 -0
  17. sphynx/_pr_client.py +237 -0
  18. sphynx/_rag_diagnostics_calibration.py +285 -0
  19. sphynx/_rag_diagnostics_catalog.py +39 -0
  20. sphynx/_rag_diagnostics_common.py +26 -0
  21. sphynx/_rag_diagnostics_contracts.py +186 -0
  22. sphynx/_rag_diagnostics_engine.py +243 -0
  23. sphynx/_rag_diagnostics_importers.py +242 -0
  24. sphynx/_rag_diagnostics_ranking.py +81 -0
  25. sphynx/_rag_diagnostics_semantic.py +325 -0
  26. sphynx/_rag_ranking.py +149 -0
  27. sphynx/_speech_benchmark_scoring.py +140 -0
  28. sphynx/_speech_benchmark_sources.py +275 -0
  29. sphynx/_speech_complete_scoring.py +218 -0
  30. sphynx/_speech_complete_source.py +118 -0
  31. sphynx/_speech_kido.py +294 -0
  32. sphynx/_speech_scoring.py +77 -0
  33. sphynx/_track_capture.py +213 -0
  34. sphynx/analytical_views.py +515 -0
  35. sphynx/assessment_contracts.py +378 -0
  36. sphynx/assessment_selection.py +57 -0
  37. sphynx/audit_access.py +245 -0
  38. sphynx/audit_contracts.py +204 -0
  39. sphynx/audit_requests.py +180 -0
  40. sphynx/authoring.py +188 -0
  41. sphynx/benchmarks.py +568 -0
  42. sphynx/candidates.py +62 -0
  43. sphynx/catalog.py +272 -0
  44. sphynx/chunking.py +372 -0
  45. sphynx/clickhouse_reader.py +169 -0
  46. sphynx/client.py +486 -0
  47. sphynx/comparison_core.py +149 -0
  48. sphynx/comparison_views.py +223 -0
  49. sphynx/components.py +1661 -0
  50. sphynx/contracts.py +514 -0
  51. sphynx/customer_rerank.py +299 -0
  52. sphynx/customer_retrieval.py +146 -0
  53. sphynx/data/benchmark-releases.v1.json +164 -0
  54. sphynx/data/public-benchmark-release.v1.json +354 -0
  55. sphynx/data/rag-diagnostics-synthetic.v1.json +6982 -0
  56. sphynx/data/rag-paper-references.v1.json +176 -0
  57. sphynx/data/rag-paper-schemas.v1.json +1069 -0
  58. sphynx/data/rag-rerank-v1.json +507 -0
  59. sphynx/dataset_catalog_data.py +1214 -0
  60. sphynx/dataset_readiness.py +138 -0
  61. sphynx/dataset_v3.py +999 -0
  62. sphynx/demo.py +130 -0
  63. sphynx/demo_candidates/__init__.py +1 -0
  64. sphynx/demo_candidates/dense.py +11 -0
  65. sphynx/demo_candidates/lexical.py +21 -0
  66. sphynx/diagnostics.py +207 -0
  67. sphynx/documentation.py +737 -0
  68. sphynx/documentation_candidate.py +939 -0
  69. sphynx/evals.py +538 -0
  70. sphynx/evaluation_protocols.py +124 -0
  71. sphynx/evaluator_bindings.py +370 -0
  72. sphynx/evidence.py +352 -0
  73. sphynx/execution_capabilities.py +109 -0
  74. sphynx/fast_search.py +285 -0
  75. sphynx/full_rag.py +341 -0
  76. sphynx/hosted/__init__.py +18 -0
  77. sphynx/hosted/benchmark_intake.py +218 -0
  78. sphynx/hosted/contracts.py +361 -0
  79. sphynx/hosted/model_admission.py +130 -0
  80. sphynx/hosted/model_artifacts.py +298 -0
  81. sphynx/hosted/model_contracts.py +63 -0
  82. sphynx/hosted/model_gcs.py +181 -0
  83. sphynx/hosted/model_history.py +84 -0
  84. sphynx/hosted/model_session.py +233 -0
  85. sphynx/hosted/public_suite.py +74 -0
  86. sphynx/hosted/registry.py +97 -0
  87. sphynx/hosted/revisions.py +126 -0
  88. sphynx/hosted/sandbox.py +105 -0
  89. sphynx/hosted/submission.py +287 -0
  90. sphynx/hosted_workflow.py +451 -0
  91. sphynx/http_transport.py +86 -0
  92. sphynx/integrated_rag.py +352 -0
  93. sphynx/integrated_workflow.py +552 -0
  94. sphynx/integration_contracts.py +474 -0
  95. sphynx/invocation.py +791 -0
  96. sphynx/invocation_candidates.py +392 -0
  97. sphynx/invocation_catalog_data.py +40 -0
  98. sphynx/learned_catalog_data.py +30 -0
  99. sphynx/learned_rerank.py +267 -0
  100. sphynx/legacy_full_rag.py +67 -0
  101. sphynx/lifecycle.py +885 -0
  102. sphynx/lifecycle_workflow.py +490 -0
  103. sphynx/metric_views.py +474 -0
  104. sphynx/metrics.py +282 -0
  105. sphynx/model_adapters.py +438 -0
  106. sphynx/model_catalog_data.py +30 -0
  107. sphynx/official_benchmarks.py +385 -0
  108. sphynx/packs.py +258 -0
  109. sphynx/pipeline.py +313 -0
  110. sphynx/pipeline_workflow.py +551 -0
  111. sphynx/pr_evaluations.py +560 -0
  112. sphynx/progress.py +89 -0
  113. sphynx/public_bundle.py +173 -0
  114. sphynx/public_eval.py +62 -0
  115. sphynx/public_rerank.py +355 -0
  116. sphynx/py.typed +0 -0
  117. sphynx/pytest_plugin.py +62 -0
  118. sphynx/question_robustness.py +437 -0
  119. sphynx/rag.py +329 -0
  120. sphynx/rag_diagnostics.py +309 -0
  121. sphynx/rag_papers/__init__.py +29 -0
  122. sphynx/rag_papers/__main__.py +45 -0
  123. sphynx/rag_papers/candidates.py +19 -0
  124. sphynx/rag_papers/contracts.py +282 -0
  125. sphynx/rag_papers/datasets.py +291 -0
  126. sphynx/rag_papers/experiments.py +116 -0
  127. sphynx/rag_papers/judges.py +308 -0
  128. sphynx/rag_papers/registry.py +33 -0
  129. sphynx/rag_papers/runner.py +176 -0
  130. sphynx/rag_papers/sable.py +135 -0
  131. sphynx/rag_papers/schemas.py +12 -0
  132. sphynx/rag_papers/scoring.py +249 -0
  133. sphynx/rag_papers/semantic.py +288 -0
  134. sphynx/rag_papers/trec.py +80 -0
  135. sphynx/recorded_scores.py +352 -0
  136. sphynx/records.py +124 -0
  137. sphynx/reflex_traces.py +193 -0
  138. sphynx/report_rendering.py +71 -0
  139. sphynx/rerank_family_contracts.py +150 -0
  140. sphynx/rerank_workflow.py +424 -0
  141. sphynx/research_candidate.py +190 -0
  142. sphynx/results.py +852 -0
  143. sphynx/retrieval.py +750 -0
  144. sphynx/retrieval_candidates.py +295 -0
  145. sphynx/retrieval_catalog_data.py +19 -0
  146. sphynx/retrieval_recordings.py +125 -0
  147. sphynx/reviewer_audit.py +266 -0
  148. sphynx/routing_policies.py +375 -0
  149. sphynx/runner.py +544 -0
  150. sphynx/sable_rerank.py +489 -0
  151. sphynx/score_reliability.py +393 -0
  152. sphynx/speech_benchmarks.py +204 -0
  153. sphynx/speech_campaigns.py +378 -0
  154. sphynx/speech_complete.py +236 -0
  155. sphynx/speech_contracts.py +225 -0
  156. sphynx/speech_evidence.py +429 -0
  157. sphynx/speech_listening.py +115 -0
  158. sphynx/speech_multilingual.py +71 -0
  159. sphynx/speech_tts.py +479 -0
  160. sphynx/sql/001_sync_v1.sql +30 -0
  161. sphynx/stage_contracts.py +199 -0
  162. sphynx/statistical_catalog_data.py +53 -0
  163. sphynx/statistical_rerank.py +232 -0
  164. sphynx/sync.py +797 -0
  165. sphynx/workbench.py +378 -0
  166. sphynx/workbench_catalog_data.py +13 -0
  167. sphynx/workbench_tracks.py +78 -0
  168. sphynx/workflow_async.py +197 -0
  169. sphynx/workflow_contracts.py +594 -0
  170. sphynx/workflow_decisions.py +211 -0
  171. sphynx/workflow_defaults.py +16 -0
  172. sphynx/workflow_extensions.py +212 -0
  173. sphynx/workflow_registry.py +513 -0
  174. 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