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
sphynx/sable_rerank.py ADDED
@@ -0,0 +1,489 @@
1
+ """Owner-held Sable reranker development packs.
2
+
3
+ The dataset path is supplied by the owner at runtime. Neither passages nor
4
+ reference labels are installed in the wheel. Local execution is untrusted:
5
+ the candidate process can read files available to its OS identity, so genuinely
6
+ blind holdouts require a separate platform runner.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ from dataclasses import dataclass, replace
14
+ from pathlib import Path
15
+ from typing import Any, Mapping
16
+
17
+ from .contracts import INTERFACE_VERSION, SABLE_RERANK_DEV_PACK_ID, CandidateSpec, ContractError, build_candidate_request, canonical_json
18
+ from .evidence import RunRef, read_capture_artifact, read_capture_manifest, read_run_manifest, verify_run
19
+ from .metrics import METRIC_RELEASE, aggregate_case_metrics, grade_case
20
+ from .packs import ExecutionPolicy, PackDescriptor
21
+ from .rag import RagRerankPack, validate_dataset_v2
22
+ from .runner import ENV_POLICY_ID, MAX_STREAM_BYTES, _case_snapshot, _parse_response, _run_with_cases
23
+
24
+ SABLE_PACK_ID = SABLE_RERANK_DEV_PACK_ID
25
+ _SCHEMA = "sphynx.sable.rerank.private.v1"
26
+ _ORIGINS = frozenset({"reconstructed_development", "exact_pre_rerank"})
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class PrivatePack:
31
+ pack: RagRerankPack
32
+ origin: str
33
+ label_status: str
34
+ source_revision: str
35
+
36
+ @property
37
+ def dataset_digest(self) -> str:
38
+ return self.pack.dataset_digest
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class PrivateEvaluation:
43
+ run_id: str
44
+ pack_id: str
45
+ dataset_digest: str
46
+ origin: str
47
+ label_status: str
48
+ candidate: Mapping[str, str]
49
+ execution_policy: Mapping[str, Any]
50
+ runner: Mapping[str, str]
51
+ status: str
52
+ cases: tuple[Mapping[str, Any], ...]
53
+ metrics: Mapping[str, Mapping[str, Any]]
54
+
55
+ def to_dict(self) -> dict[str, Any]:
56
+ return {
57
+ "schema_version": "sphynx.sable.rerank.evaluation.v1",
58
+ "run_id": self.run_id,
59
+ "pack_id": self.pack_id,
60
+ "dataset_digest": self.dataset_digest,
61
+ "origin": self.origin,
62
+ "label_status": self.label_status,
63
+ "trust_tier": "local_unverified",
64
+ "candidate": dict(self.candidate),
65
+ "execution_policy": dict(self.execution_policy),
66
+ "runner": dict(self.runner),
67
+ "status": self.status,
68
+ "cases": [dict(case) for case in self.cases],
69
+ "metrics": {key: dict(value) for key, value in self.metrics.items()},
70
+ }
71
+
72
+ def to_json(self) -> str:
73
+ return canonical_json(self.to_dict())
74
+
75
+
76
+ def load_private_pack(dataset_path: str | Path, *, expected_sha256: str | None = None) -> PrivatePack:
77
+ """Validate an external dataset and bind its complete bytes to a release.
78
+
79
+ ``expected_sha256`` is useful when loading a separately approved release;
80
+ a changed label or passage byte then fails before candidate execution.
81
+ """
82
+
83
+ path = Path(dataset_path).expanduser().resolve(strict=True)
84
+ payload = path.read_bytes()
85
+ digest = hashlib.sha256(payload).hexdigest()
86
+ if expected_sha256 is not None and expected_sha256.removeprefix("sha256:") != digest:
87
+ raise ContractError("private pack bytes differ from expected SHA-256")
88
+ try:
89
+ value = json.loads(payload)
90
+ except (UnicodeError, json.JSONDecodeError) as exc:
91
+ raise ContractError("private pack must be UTF-8 JSON") from exc
92
+ if not isinstance(value, dict):
93
+ raise ContractError("private pack must be a JSON object")
94
+ meta = value.get("sphynx_private_pack")
95
+ if not isinstance(meta, dict) or meta.get("schema_version") != _SCHEMA:
96
+ raise ContractError("private pack metadata/schema is missing")
97
+ if meta.get("pack_id") != SABLE_PACK_ID or meta.get("visibility") != "private":
98
+ raise ContractError("private Sable pack identity or visibility is invalid")
99
+ origin = meta.get("origin")
100
+ label_status = meta.get("label_status")
101
+ if origin not in _ORIGINS or label_status not in {"provisional", "human_adjudicated"}:
102
+ raise ContractError("private pack origin or label status is invalid")
103
+ if origin == "reconstructed_development" and label_status != "provisional":
104
+ raise ContractError("reconstructed pools cannot claim adjudicated benchmark labels")
105
+ if label_status == "human_adjudicated" and meta.get("privacy_review") != "approved":
106
+ raise ContractError("adjudicated private pack requires approved privacy review")
107
+ source_revision = meta.get("source_revision")
108
+ if not isinstance(source_revision, str) or not source_revision.strip():
109
+ raise ContractError("private pack needs source_revision")
110
+ cases, provisional_refs, threshold = validate_dataset_v2(value)
111
+ for case, reference in zip(cases, provisional_refs):
112
+ has_required_group = any(group.required for group in reference.evidence_groups)
113
+ has_relevant_passage = any(item.relevance_grade >= threshold for item in reference.candidates)
114
+ if has_required_group and not has_relevant_passage:
115
+ raise ContractError(f"{case.case_id}: answerable reranker pool has no relevant passage; track retrieval coverage separately")
116
+ if not has_required_group and has_relevant_passage:
117
+ raise ContractError(f"{case.case_id}: unsupported reranker pool contains a relevant passage")
118
+ if label_status == "human_adjudicated":
119
+ for task in value["tasks"]:
120
+ if not all(isinstance(item.get("adjudication"), dict) for item in task["candidates"]):
121
+ raise ContractError("human_adjudicated status requires every candidate adjudication")
122
+ references = tuple(replace(ref, label_authority=label_status) for ref in provisional_refs)
123
+ descriptor = PackDescriptor(
124
+ pack_id=SABLE_PACK_ID,
125
+ pack_release="v1." + digest,
126
+ title="Owner-held Sable passage reranking development pack",
127
+ track="rag_rerank",
128
+ interface_version=INTERFACE_VERSION,
129
+ case_schema_version=value["schema_version"],
130
+ request_schema_version="sphynx.rag.rerank.request.v1",
131
+ response_schema_version="sphynx.rag.rerank.response.v1",
132
+ status="runnable",
133
+ description="Owner-held Sable data; local runs are development results only.",
134
+ execution_policy=ExecutionPolicy("v1", 1, 30.0, "local-unbounded.v1"),
135
+ case_count=len(cases),
136
+ metric_release=METRIC_RELEASE,
137
+ )
138
+ pack = RagRerankPack(descriptor, cases, references, METRIC_RELEASE, "sha256:" + digest, threshold)
139
+ return PrivatePack(pack, origin, label_status, source_revision)
140
+
141
+
142
+ def run_private_rerank(
143
+ candidate: CandidateSpec, dataset_path: str | Path, output_dir: str | Path,
144
+ *, expected_sha256: str | None = None,
145
+ ) -> RunRef:
146
+ """Run a candidate on the complete frozen pool without sending labels."""
147
+
148
+ private = load_private_pack(dataset_path, expected_sha256=expected_sha256)
149
+ return _run_with_cases(candidate, private.pack.descriptor, private.pack.cases, output_dir)
150
+
151
+
152
+ def grade_private_rerank(
153
+ run_ref: RunRef | str | Path, dataset_path: str | Path,
154
+ *, expected_sha256: str | None = None,
155
+ ) -> PrivateEvaluation:
156
+ """Replay verified raw captures against a matching owner-held dataset."""
157
+
158
+ private = load_private_pack(dataset_path, expected_sha256=expected_sha256)
159
+ pack = private.pack
160
+ run_path = (run_ref.path if isinstance(run_ref, RunRef) else Path(run_ref)).expanduser().resolve()
161
+ manifest = read_run_manifest(run_path)
162
+ ref = RunRef(manifest["run_id"], run_path, manifest["pack_id"])
163
+ verify_run(ref)
164
+ if manifest.get("pack_id") != SABLE_PACK_ID or manifest.get("pack_release") != pack.descriptor.pack_release:
165
+ raise ContractError("run and private pack release differ")
166
+ if manifest.get("case_count") != len(pack.cases) or manifest.get("capture_count") != len(pack.cases):
167
+ raise ContractError("run does not cover every private pack case exactly once")
168
+ policy = pack.descriptor.execution_policy
169
+ expected_policy = {
170
+ "policy_version": policy.policy_version, "trials": policy.trials,
171
+ "per_case_timeout_seconds": policy.per_case_timeout_seconds,
172
+ "resource_policy_id": policy.resource_policy_id,
173
+ "environment_policy_id": ENV_POLICY_ID,
174
+ "stdout_stderr_retention_limit_bytes_each": MAX_STREAM_BYTES,
175
+ }
176
+ if manifest.get("execution_policy") != expected_policy:
177
+ raise ContractError("run execution policy differs from private pack")
178
+ candidate_identity = manifest.get("candidate")
179
+ if not isinstance(candidate_identity, dict) or candidate_identity.get("interface_version") != INTERFACE_VERSION:
180
+ raise ContractError("run candidate interface differs from private pack")
181
+ runner_identity = manifest.get("runner")
182
+ if not isinstance(runner_identity, dict) or runner_identity.get("release") != "v1" or runner_identity.get("clock_domain") != "python.monotonic_ns.process_local":
183
+ raise ContractError("run runner identity is unsupported")
184
+ expected_cases = [
185
+ {
186
+ "dataset_id": case.dataset_id, "dataset_version": case.dataset_version,
187
+ "case_id": case.case_id, "query_id": case.query_id, "pool_digest": case.pool_digest,
188
+ "split": case.split, "split_group_id": case.split_group_id, "cluster_id": case.cluster_id,
189
+ "suite_type": case.suite_type, "versions": case.versions.to_dict(),
190
+ }
191
+ for case in pack.cases
192
+ ]
193
+ if manifest.get("cases") != expected_cases or manifest.get("track") != "rag_rerank" or manifest.get("trust_tier") != "local_unverified" or manifest.get("provenance") != "runner_observed":
194
+ raise ContractError("run pool, cohort, order, or trust identity differs from private pack")
195
+ ids = manifest.get("capture_ids")
196
+ if not isinstance(ids, list) or len(ids) != len(pack.cases):
197
+ raise ContractError("run capture order is invalid")
198
+ results = []
199
+ case_rows: list[Mapping[str, Any]] = []
200
+ for case, capture_id in zip(pack.cases, ids):
201
+ capture = read_capture_manifest(ref, capture_id)
202
+ identity = capture.get("identity", {})
203
+ if not isinstance(identity, dict) or any((
204
+ identity.get("case_id") != case.case_id,
205
+ identity.get("pool_digest") != case.pool_digest,
206
+ identity.get("trial_index") != 1,
207
+ identity.get("run_id") != ref.run_id,
208
+ )):
209
+ raise ContractError(f"capture {capture_id} does not match private case")
210
+ request = build_candidate_request(case, SABLE_PACK_ID)
211
+ if read_capture_artifact(ref, capture_id, "candidate_request") != (request.to_json() + "\n").encode("utf-8"):
212
+ raise ContractError(f"candidate request differs from private case {case.case_id}")
213
+ if read_capture_artifact(ref, capture_id, "case_snapshot") != (canonical_json(_case_snapshot(case)) + "\n").encode("utf-8"):
214
+ raise ContractError(f"case snapshot differs from private case {case.case_id}")
215
+ status = capture.get("status")
216
+ ranked = None
217
+ if status == "success":
218
+ ranked, error_code, _ = _parse_response(read_capture_artifact(ref, capture_id, "candidate_stdout"), request)
219
+ if error_code or ranked is None or capture.get("ranked_candidate_ids") != list(ranked):
220
+ raise ContractError(f"saved ranking differs from raw response for {case.case_id}")
221
+ elif capture.get("ranked_candidate_ids") is not None:
222
+ raise ContractError(f"failed capture has a ranking for {case.case_id}")
223
+ timing = capture.get("timing")
224
+ latency_ns = timing.get("candidate_latency_ns") if isinstance(timing, dict) else None
225
+ if latency_ns is not None and (type(latency_ns) is not int or latency_ns < 0):
226
+ raise ContractError(f"invalid timing for {case.case_id}")
227
+ result = grade_case(
228
+ case, pack.reference_for(case.case_id), ranked,
229
+ latency_ns / 1_000_000 if latency_ns is not None else None,
230
+ terminal_status="ok" if status == "success" else str(status),
231
+ relevance_threshold=pack.relevance_threshold, pack=pack,
232
+ )
233
+ results.append(result)
234
+ case_rows.append({
235
+ "case_id": case.case_id, "pool_digest": case.pool_digest,
236
+ "status": status, "values": dict(result.values),
237
+ "undefined_reasons": dict(result.undefined_reasons),
238
+ })
239
+ metrics = {name: value.to_dict() for name, value in aggregate_case_metrics(results, pack=pack).items()}
240
+ return PrivateEvaluation(
241
+ ref.run_id, SABLE_PACK_ID, private.dataset_digest, private.origin,
242
+ private.label_status, manifest["candidate"], expected_policy, runner_identity,
243
+ "completed" if all(row["status"] == "success" for row in case_rows) else "incomplete",
244
+ tuple(case_rows), metrics,
245
+ )
246
+
247
+
248
+ def compare_private_rerank(a: PrivateEvaluation, b: PrivateEvaluation) -> dict[str, Any]:
249
+ """Compare two fully matched frozen-pool runs with paired case deltas."""
250
+
251
+ if a.pack_id != b.pack_id or a.dataset_digest != b.dataset_digest:
252
+ raise ContractError("private reranker comparisons require the exact same pack bytes")
253
+ if [row["pool_digest"] for row in a.cases] != [row["pool_digest"] for row in b.cases]:
254
+ raise ContractError("private reranker comparisons require identical ordered pools")
255
+ if a.execution_policy != b.execution_policy or a.runner != b.runner:
256
+ raise ContractError("private reranker comparisons require matching runner and execution policy")
257
+ metrics: dict[str, Any] = {}
258
+ for metric_id in a.metrics:
259
+ av, bv = a.metrics[metric_id], b.metrics[metric_id]
260
+ pairs = [
261
+ {"case_id": left["case_id"], "a": left["values"][metric_id], "b": right["values"][metric_id],
262
+ "delta_b_minus_a": right["values"][metric_id] - left["values"][metric_id]}
263
+ for left, right in zip(a.cases, b.cases)
264
+ if left["values"][metric_id] is not None and right["values"][metric_id] is not None
265
+ ]
266
+ metrics[metric_id] = {
267
+ "a": av["value"], "b": bv["value"],
268
+ "delta_b_minus_a": bv["value"] - av["value"] if av["value"] is not None and bv["value"] is not None else None,
269
+ "paired_cases": pairs,
270
+ }
271
+ return {
272
+ "pack_id": a.pack_id, "dataset_digest": a.dataset_digest,
273
+ "origin": a.origin, "trust_tier": "local_unverified", "metrics": metrics,
274
+ }
275
+
276
+
277
+ def build_cherry_development_dataset(pool_path: str | Path, labels_path: str | Path) -> dict[str, Any]:
278
+ """Adapt the existing reconstructed Cherry study without bundling its data.
279
+
280
+ The result is a development dataset only. The study did not capture the
281
+ actual production Pinecone pool, contextualized passages, or rewritten
282
+ query, and its labels have not been Sphynx-adjudicated.
283
+ """
284
+
285
+ pool_bytes = Path(pool_path).read_bytes()
286
+ label_bytes = Path(labels_path).read_bytes()
287
+ pool = json.loads(pool_bytes)
288
+ labels = json.loads(label_bytes)
289
+ if pool.get("schema_version") != "cherry-pre-rerank-dense-pools.v1":
290
+ raise ContractError("unsupported Cherry frozen-pool source schema")
291
+ if pool.get("status") != "frozen_before_reranker_scoring":
292
+ raise ContractError("Cherry pool was not frozen before reranker scoring")
293
+ if labels.get("schema_version") != "cherry-reranker-labels.v1":
294
+ raise ContractError("unsupported Cherry label source schema")
295
+ frozen_body = {key: value for key, value in pool.items() if key not in {"runtime", "pool_sha256"}}
296
+ expected_pool_sha = hashlib.sha256(canonical_json(frozen_body).encode("utf-8")).hexdigest()
297
+ if pool.get("pool_sha256") != expected_pool_sha or labels.get("pool_sha256") != expected_pool_sha:
298
+ raise ContractError("Cherry pool and labels do not share the frozen source digest")
299
+ queries = pool.get("queries")
300
+ label_queries = labels.get("queries")
301
+ if not isinstance(queries, list) or not isinstance(label_queries, dict) or len(queries) != len(label_queries):
302
+ raise ContractError("Cherry questions and labels disagree")
303
+ source_sha = hashlib.sha256(pool_bytes).hexdigest()
304
+ label_sha = hashlib.sha256(label_bytes).hexdigest()
305
+ tasks: list[dict[str, Any]] = []
306
+ for index, item in enumerate(queries):
307
+ case_id = item["task_id"]
308
+ if case_id not in label_queries or item.get("candidate_count") != len(item.get("candidates", [])):
309
+ raise ContractError(f"Cherry pool or labels missing for {case_id}")
310
+ qrels = label_queries[case_id].get("relevance")
311
+ if not isinstance(qrels, dict):
312
+ raise ContractError(f"Cherry qrels missing for {case_id}")
313
+ candidates_raw = item["candidates"]
314
+ candidate_ids = {candidate["candidate_id"] for candidate in candidates_raw}
315
+ if len(candidate_ids) != len(candidates_raw) or not set(qrels) <= candidate_ids:
316
+ raise ContractError(f"Cherry qrels do not match frozen pool for {case_id}")
317
+ answerable = any(int(value.get("grade", 0)) >= 2 for value in qrels.values())
318
+ group_id = f"evidence:{index:02d}"
319
+ acceptable = [candidate["candidate_id"] for candidate in candidates_raw
320
+ if int(qrels.get(candidate["candidate_id"], {}).get("grade", 0)) >= 2]
321
+ candidates = []
322
+ for rank, candidate in enumerate(candidates_raw, start=1):
323
+ label = qrels.get(candidate["candidate_id"], {})
324
+ source_id = str(candidate["source_id"])
325
+ chunk_index = int(candidate["chunk_index"])
326
+ candidates.append({
327
+ "candidate_id": candidate["candidate_id"],
328
+ "document_id": "source:" + source_id,
329
+ "source_id": source_id,
330
+ "chunk_id": f"{source_id}:chunk:{chunk_index}",
331
+ "url": f"urn:sable:cherry:{source_id}:{chunk_index}",
332
+ "input": candidate["input"],
333
+ "dense_rank": rank,
334
+ "dense_score": candidate["first_stage_score"],
335
+ "relevance_grade": int(label.get("grade", 0)),
336
+ "evidence_group_ids": [group_id] if candidate["candidate_id"] in acceptable else [],
337
+ "known_hard_negative": bool(label.get("hard_negative", False)),
338
+ })
339
+ task = {
340
+ "task_id": case_id, "query_id": case_id, "cluster_id": case_id,
341
+ "split_group_id": case_id, "domain": "cherry", "query": item["query"],
342
+ "answerability": "answerable" if answerable else "unsupported",
343
+ "business_severity": "medium",
344
+ "query_categories": ["straightforward_factual" if answerable else "unsupported_not_in_kb"],
345
+ "customer": "cherry", "split": "dev", "suite_type": "customer_acceptance",
346
+ "versions": {
347
+ "corpus": "cherry-reconstruction:" + str(pool["source_corpus_sha256"]),
348
+ "index": "snowflake-local:" + source_sha,
349
+ "chunking": "public-api-chunks",
350
+ "embedding": "snowflake-arctic-m-v2:" + str(pool["first_stage"]["revision"]),
351
+ },
352
+ "candidates": candidates,
353
+ "evidence_groups": [{
354
+ "evidence_group_id": group_id,
355
+ "description": "Answer-bearing passages with Cherry study relevance grade at least two",
356
+ "required": True,
357
+ "acceptable_candidate_ids": acceptable,
358
+ }] if answerable else [],
359
+ }
360
+ if not answerable:
361
+ task["expected_unsupported_behavior"] = "abstain"
362
+ tasks.append(task)
363
+ dataset = {
364
+ "schema_version": "rag-evals.dataset.v2",
365
+ "dataset_id": "sable.cherry.reconstructed.v1",
366
+ "version": "v1",
367
+ "default_relevance_threshold": 2,
368
+ "sphynx_private_pack": {
369
+ "schema_version": _SCHEMA, "pack_id": SABLE_PACK_ID,
370
+ "visibility": "private", "origin": "reconstructed_development",
371
+ "label_status": "provisional",
372
+ "source_revision": str(pool["first_stage"]["fast_search_source_revision"]),
373
+ "source_pool_sha256": source_sha, "source_label_sha256": label_sha,
374
+ },
375
+ "tasks": tasks,
376
+ }
377
+ validate_dataset_v2(dataset)
378
+ return dataset
379
+
380
+
381
+ def build_exact_sable_dataset(
382
+ records: list[Mapping[str, Any]], *, dataset_id: str, dataset_version: str,
383
+ source_revision: str, label_status: str = "provisional",
384
+ privacy_review: str = "pending",
385
+ ) -> dict[str, Any]:
386
+ """Build a private pack from reviewed Fast Search pre-rerank exports.
387
+
388
+ Every record must provide ``case_id``, ``snapshot`` (the opt-in capture),
389
+ ``versions`` (corpus/index/chunking/embedding), ``answerability``, and a
390
+ complete ``labels`` mapping keyed by vector ID. Label groups and cohort
391
+ fields are owner-supplied; no relevance is inferred from model output.
392
+ """
393
+
394
+ if not records or label_status not in {"provisional", "human_adjudicated"}:
395
+ raise ContractError("exact Sable dataset needs records and valid label status")
396
+ if label_status == "human_adjudicated" and privacy_review != "approved":
397
+ raise ContractError("adjudicated dataset requires approved privacy review")
398
+ tasks = []
399
+ snapshots = []
400
+ for record in records:
401
+ if not isinstance(record, Mapping):
402
+ raise ContractError("each exact Sable case must be an object")
403
+ case_id = record.get("case_id")
404
+ snapshot = record.get("snapshot")
405
+ labels = record.get("labels")
406
+ versions = record.get("versions")
407
+ if not isinstance(snapshot, Mapping) or snapshot.get("schema_version") != "sphynx.fast-search.pre-rerank.v1":
408
+ raise ContractError(f"{case_id}: invalid Fast Search capture schema")
409
+ if snapshot.get("boundary") != "after_pinecone_before_reranker":
410
+ raise ContractError(f"{case_id}: capture is not at the exact pre-rerank boundary")
411
+ pool = snapshot.get("candidates")
412
+ if not isinstance(pool, list) or not pool or snapshot.get("candidate_count") != len(pool):
413
+ raise ContractError(f"{case_id}: capture has no complete candidate pool")
414
+ if not isinstance(labels, Mapping) or set(labels) != {item.get("candidate_id") for item in pool}:
415
+ raise ContractError(f"{case_id}: every captured vector needs exactly one label")
416
+ if not isinstance(versions, Mapping) or set(versions) != {"corpus", "index", "chunking", "embedding"}:
417
+ raise ContractError(f"{case_id}: explicit corpus/index/chunking/embedding versions required")
418
+ candidates = []
419
+ for rank, item in enumerate(pool, 1):
420
+ if not isinstance(item, Mapping) or item.get("dense_rank") != rank:
421
+ raise ContractError(f"{case_id}: captured dense rank/order is invalid")
422
+ candidate_id = item["candidate_id"]
423
+ label = labels[candidate_id]
424
+ if not isinstance(label, Mapping) or "relevance_grade" not in label or "evidence_group_ids" not in label:
425
+ raise ContractError(f"{case_id}: incomplete label for {candidate_id}")
426
+ candidate = {
427
+ "candidate_id": candidate_id,
428
+ "document_id": item["document_id"],
429
+ "source_id": item["source_id"],
430
+ "chunk_id": candidate_id,
431
+ "url": item.get("url") or f"urn:fast-search:{candidate_id}",
432
+ "input": item["reranker_input"],
433
+ "dense_rank": rank,
434
+ "dense_score": item["dense_score"],
435
+ "relevance_grade": label["relevance_grade"],
436
+ "evidence_group_ids": label["evidence_group_ids"],
437
+ "known_hard_negative": bool(label.get("known_hard_negative", False)),
438
+ "raw_text": item.get("raw_text"),
439
+ "source_url": item.get("url"),
440
+ "title": item.get("title"),
441
+ "section_path": item.get("section_path"),
442
+ "pipeline_id": item.get("pipeline_id"),
443
+ }
444
+ if "adjudication" in label:
445
+ candidate["adjudication"] = label["adjudication"]
446
+ candidates.append(candidate)
447
+ split = record.get("split", "dev")
448
+ task = {
449
+ "task_id": case_id, "query_id": case_id,
450
+ "cluster_id": record.get("cluster_id", case_id),
451
+ "split_group_id": record.get("split_group_id", case_id),
452
+ "domain": record.get("domain", "sable"),
453
+ "query": snapshot["query"],
454
+ "answerability": record.get("answerability"),
455
+ "business_severity": record.get("business_severity", "medium"),
456
+ "query_categories": record.get("query_categories", ["straightforward_factual"]),
457
+ "customer": snapshot["org"],
458
+ "split": split,
459
+ "suite_type": record.get("suite_type", "untouched_holdout" if split == "holdout" else "customer_acceptance"),
460
+ "versions": dict(versions),
461
+ "candidates": candidates,
462
+ "evidence_groups": record.get("evidence_groups", []),
463
+ "capture_scope": {
464
+ "org": snapshot["org"], "kbs": snapshot["kbs"],
465
+ "filters": snapshot["filters"],
466
+ "requested_top_k": snapshot["requested_top_k"],
467
+ "retrieval_depth": snapshot["retrieval_depth"],
468
+ },
469
+ }
470
+ if task["answerability"] == "unsupported":
471
+ task["expected_unsupported_behavior"] = "abstain"
472
+ tasks.append(task)
473
+ snapshots.append(snapshot)
474
+ dataset = {
475
+ "schema_version": "rag-evals.dataset.v2",
476
+ "dataset_id": dataset_id,
477
+ "version": dataset_version,
478
+ "default_relevance_threshold": 2,
479
+ "sphynx_private_pack": {
480
+ "schema_version": _SCHEMA, "pack_id": SABLE_PACK_ID,
481
+ "visibility": "private", "origin": "exact_pre_rerank",
482
+ "label_status": label_status, "privacy_review": privacy_review,
483
+ "source_revision": source_revision,
484
+ "capture_sha256": hashlib.sha256(canonical_json(snapshots).encode("utf-8")).hexdigest(),
485
+ },
486
+ "tasks": tasks,
487
+ }
488
+ validate_dataset_v2(dataset)
489
+ return dataset