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/fast_search.py ADDED
@@ -0,0 +1,285 @@
1
+ """Fast Search HTTP smoke adapter.
2
+
3
+ Ordinary search responses contain ranked text but no stable passage identity or
4
+ proof of corpus/label alignment. This adapter deliberately cannot produce a
5
+ ``rag.rerank.v1`` quality score or comparison group.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ import math
13
+ import time
14
+ import urllib.error
15
+ import urllib.request
16
+ from dataclasses import dataclass, field
17
+ from typing import Callable, Mapping
18
+ from urllib.parse import quote, urlsplit
19
+
20
+ from .contracts import ContractError, _id, _text, canonical_json
21
+ from .http_transport import open_no_redirect
22
+
23
+ FAST_SEARCH_TRACK = "smoke_latency"
24
+ MAX_RESPONSE_BYTES = 4 * 1024 * 1024
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class FastSearchConfig:
29
+ """Explicit endpoint and KB identity for one smoke request."""
30
+
31
+ base_url: str
32
+ org: str
33
+ kb: str
34
+ top_k: int = 5
35
+ timeout_seconds: float = 30.0
36
+ source_id: str | None = None
37
+ domain: str | None = None
38
+
39
+ def __post_init__(self) -> None:
40
+ parsed = urlsplit(self.base_url)
41
+ if (
42
+ parsed.scheme != "https"
43
+ or not parsed.hostname
44
+ or parsed.username is not None
45
+ or parsed.password is not None
46
+ or parsed.query
47
+ or parsed.fragment
48
+ ):
49
+ raise ContractError("base_url must be an HTTPS origin or path without credentials, query, or fragment")
50
+ _id(self.org, "org")
51
+ _id(self.kb, "kb")
52
+ if type(self.top_k) is not int or not 1 <= self.top_k <= 1000:
53
+ raise ContractError("top_k must be an integer from 1 through 1000")
54
+ if type(self.timeout_seconds) not in (int, float) or not math.isfinite(self.timeout_seconds) or not 0 < self.timeout_seconds <= 120:
55
+ raise ContractError("timeout_seconds must be finite, positive, and at most 120")
56
+ if self.source_id is not None:
57
+ _text(self.source_id, "source_id")
58
+ if self.domain is not None:
59
+ _text(self.domain, "domain")
60
+
61
+ @property
62
+ def search_url(self) -> str:
63
+ return (
64
+ f"{self.base_url.rstrip('/')}/{quote(self.org, safe='')}/kbs/"
65
+ f"{quote(self.kb, safe='')}/search"
66
+ )
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class TransportResponse:
71
+ status_code: int
72
+ body: bytes
73
+
74
+ def __post_init__(self) -> None:
75
+ if type(self.status_code) is not int or not 100 <= self.status_code <= 599:
76
+ raise ContractError("transport status_code must be a valid HTTP status")
77
+ if not isinstance(self.body, bytes):
78
+ raise ContractError("transport body must be bytes")
79
+
80
+
81
+ Transport = Callable[[str, bytes, Mapping[str, str], float], TransportResponse]
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class FastSearchHit:
86
+ """A hit in server-supplied order, without an invented passage ID."""
87
+
88
+ rank: int
89
+ score: float
90
+ title: str
91
+ section_path: str
92
+ url: str
93
+ text: str
94
+ chunk_index: int
95
+ total_chunks: int
96
+
97
+ def to_dict(self) -> dict[str, object]:
98
+ return {
99
+ "rank": self.rank,
100
+ "score": self.score,
101
+ "title": self.title,
102
+ "section_path": self.section_path,
103
+ "url": self.url,
104
+ "text": self.text,
105
+ "chunk_index": self.chunk_index,
106
+ "total_chunks": self.total_chunks,
107
+ }
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class FastSearchSmokeResult:
112
+ """Explicitly ineligible for labeled frozen-pool quality comparisons."""
113
+
114
+ org: str
115
+ kb: str
116
+ query: str
117
+ top_k: int
118
+ status: str
119
+ http_status: int | None
120
+ latency_ms: float
121
+ hits: tuple[FastSearchHit, ...]
122
+ response_sha256: str | None
123
+ response_size_bytes: int | None
124
+ error: str | None
125
+ reported_latency_ms: float | None = None
126
+ track: str = field(default=FAST_SEARCH_TRACK, init=False)
127
+ trust_tier: str = field(default="local_unverified", init=False)
128
+ identity_capability: str = field(default="stable_source_and_chunk_ids_unavailable", init=False)
129
+ corpus_alignment: str = field(default="unverified", init=False)
130
+ quality_eligible: bool = field(default=False, init=False)
131
+ comparison_group: None = field(default=None, init=False)
132
+
133
+ def to_dict(self) -> dict[str, object]:
134
+ return {
135
+ "track": self.track,
136
+ "trust_tier": self.trust_tier,
137
+ "org": self.org,
138
+ "kb": self.kb,
139
+ "query": self.query,
140
+ "top_k": self.top_k,
141
+ "status": self.status,
142
+ "http_status": self.http_status,
143
+ "latency_ms": self.latency_ms,
144
+ "reported_latency_ms": self.reported_latency_ms,
145
+ "hits": [hit.to_dict() for hit in self.hits],
146
+ "response_sha256": self.response_sha256,
147
+ "response_size_bytes": self.response_size_bytes,
148
+ "error": self.error,
149
+ "identity_capability": self.identity_capability,
150
+ "corpus_alignment": self.corpus_alignment,
151
+ "quality_eligible": self.quality_eligible,
152
+ "comparison_group": self.comparison_group,
153
+ }
154
+
155
+
156
+ def _urllib_transport(url: str, body: bytes, headers: Mapping[str, str], timeout_seconds: float) -> TransportResponse:
157
+ request = urllib.request.Request(url, data=body, headers=dict(headers), method="POST")
158
+ try:
159
+ with open_no_redirect(request, timeout=timeout_seconds) as response:
160
+ return TransportResponse(response.status, response.read(MAX_RESPONSE_BYTES + 1))
161
+ except urllib.error.HTTPError as exc:
162
+ return TransportResponse(exc.code, exc.read(MAX_RESPONSE_BYTES + 1))
163
+
164
+
165
+ def _require_string(value: object, field: str) -> str:
166
+ if not isinstance(value, str):
167
+ raise ContractError(f"{field} must be a string")
168
+ return value
169
+
170
+
171
+ def _require_int(value: object, field: str, minimum: int) -> int:
172
+ if type(value) is not int or value < minimum:
173
+ raise ContractError(f"{field} must be an integer >= {minimum}")
174
+ return value
175
+
176
+
177
+ def _parse_hits(body: bytes, config: FastSearchConfig, query: str) -> tuple[tuple[FastSearchHit, ...], float | None]:
178
+ try:
179
+ decoded = json.loads(
180
+ body.decode("utf-8"),
181
+ parse_constant=lambda constant: (_ for _ in ()).throw(ValueError(f"nonfinite JSON value {constant}")),
182
+ )
183
+ except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as exc:
184
+ raise ContractError(f"Fast Search returned invalid UTF-8 JSON: {exc}") from exc
185
+ if not isinstance(decoded, dict):
186
+ raise ContractError("Fast Search response must be a JSON object")
187
+ if decoded.get("org") != config.org or decoded.get("kb") != config.kb or decoded.get("query") != query:
188
+ raise ContractError("Fast Search response org, kb, or query does not match the request")
189
+ raw_hits = decoded.get("results")
190
+ if not isinstance(raw_hits, list):
191
+ raise ContractError("Fast Search response results must be an array")
192
+ hits: list[FastSearchHit] = []
193
+ for index, raw in enumerate(raw_hits):
194
+ if not isinstance(raw, dict):
195
+ raise ContractError(f"results[{index}] must be an object")
196
+ score = raw.get("score")
197
+ if type(score) not in (int, float) or not math.isfinite(score):
198
+ raise ContractError(f"results[{index}].score must be a finite number")
199
+ hits.append(
200
+ FastSearchHit(
201
+ rank=_require_int(raw.get("rank"), f"results[{index}].rank", 1),
202
+ score=float(score),
203
+ title=_require_string(raw.get("title"), f"results[{index}].title"),
204
+ section_path=_require_string(raw.get("section_path"), f"results[{index}].section_path"),
205
+ url=_require_string(raw.get("url"), f"results[{index}].url"),
206
+ text=_require_string(raw.get("text"), f"results[{index}].text"),
207
+ chunk_index=_require_int(raw.get("chunk_index"), f"results[{index}].chunk_index", 0),
208
+ total_chunks=_require_int(raw.get("total_chunks"), f"results[{index}].total_chunks", 1),
209
+ )
210
+ )
211
+ reported = decoded.get("latency_ms")
212
+ if reported is not None:
213
+ if type(reported) not in (int, float) or not math.isfinite(reported) or reported < 0:
214
+ raise ContractError("Fast Search reported latency_ms must be a finite nonnegative number")
215
+ reported = float(reported)
216
+ return tuple(hits), reported
217
+
218
+
219
+ def search_fast_search(
220
+ config: FastSearchConfig,
221
+ query: str,
222
+ *,
223
+ transport: Transport | None = None,
224
+ headers: Mapping[str, str] | None = None,
225
+ clock_ns: Callable[[], int] = time.perf_counter_ns,
226
+ ) -> FastSearchSmokeResult:
227
+ """Call Fast Search once and retain smoke/latency evidence on every outcome.
228
+
229
+ ``transport`` receives URL, UTF-8 JSON body, headers, and timeout; the
230
+ default transport uses the Python standard library. Caller-supplied headers
231
+ (including credentials) are sent but never placed in the result.
232
+ """
233
+
234
+ if not isinstance(config, FastSearchConfig):
235
+ raise ContractError("config must be FastSearchConfig")
236
+ _text(query, "query")
237
+ payload: dict[str, object] = {"query": query, "top_k": config.top_k}
238
+ if config.source_id is not None:
239
+ payload["source_id"] = config.source_id
240
+ if config.domain is not None:
241
+ payload["domain"] = config.domain
242
+ request_headers = {"Content-Type": "application/json", "Accept": "application/json"}
243
+ if headers:
244
+ request_headers.update(headers)
245
+ start = clock_ns()
246
+ try:
247
+ response = (transport or _urllib_transport)(
248
+ config.search_url,
249
+ canonical_json(payload).encode("utf-8"),
250
+ request_headers,
251
+ float(config.timeout_seconds),
252
+ )
253
+ if not isinstance(response, TransportResponse):
254
+ raise ContractError("transport must return TransportResponse")
255
+ except Exception as exc:
256
+ latency_ms = max(0.0, (clock_ns() - start) / 1_000_000)
257
+ return FastSearchSmokeResult(
258
+ config.org, config.kb, query, config.top_k,
259
+ "transport_error", None, latency_ms, (), None, None,
260
+ f"transport failed ({type(exc).__name__})",
261
+ )
262
+ latency_ms = max(0.0, (clock_ns() - start) / 1_000_000)
263
+ digest = "sha256:" + hashlib.sha256(response.body).hexdigest()
264
+ size = len(response.body)
265
+ common = (config.org, config.kb, query, config.top_k)
266
+ if size > MAX_RESPONSE_BYTES:
267
+ return FastSearchSmokeResult(
268
+ *common, "invalid_response", response.status_code, latency_ms, (), digest, size,
269
+ f"response exceeds {MAX_RESPONSE_BYTES} bytes",
270
+ )
271
+ if not 200 <= response.status_code < 300:
272
+ return FastSearchSmokeResult(
273
+ *common, "http_error", response.status_code, latency_ms, (), digest, size,
274
+ f"Fast Search returned HTTP {response.status_code}",
275
+ )
276
+ try:
277
+ hits, reported_latency_ms = _parse_hits(response.body, config, query)
278
+ except ContractError as exc:
279
+ return FastSearchSmokeResult(
280
+ *common, "invalid_response", response.status_code, latency_ms, (), digest, size, str(exc),
281
+ )
282
+ return FastSearchSmokeResult(
283
+ *common, "ok", response.status_code, latency_ms, hits, digest, size, None,
284
+ reported_latency_ms=reported_latency_ms,
285
+ )
sphynx/full_rag.py ADDED
@@ -0,0 +1,341 @@
1
+ """Source-parity full-RAG traces, retained grading and fixed-generator diagnostics.
2
+
3
+ Opaque subject traces and componentized diagnostic traces are separate scopes.
4
+ Reference actions, qrels, facts and semantic review are never candidate inputs.
5
+ Local adapters are trusted in-process Python; metadata is not runtime attestation.
6
+ """
7
+ from __future__ import annotations
8
+ from collections.abc import Mapping
9
+ from copy import deepcopy
10
+ from dataclasses import replace
11
+ from pathlib import Path
12
+ from time import perf_counter
13
+ import re
14
+
15
+ from .contracts import ContractError
16
+ from .integration_contracts import CandidateConfiguration,ComparisonIdentity,TrackCaseCapture,value_digest,require_digest,require_comparable
17
+ from .dataset_v3 import load_dataset_v3
18
+ from ._track_capture import (strict_json,create_run,write_artifact,finish_run,read_artifact,
19
+ verify_track_run,write_evaluation,verify_track_evaluation,byte_digest)
20
+ from .chunking import _policy_for_source,_select_tasks,_verify_approval
21
+ from .pipeline import _detach,_require,_number
22
+ from ._full_rag_trace import Level3Trace,validate_level3_trace,CONTEXT_CONDITIONS
23
+ from ._full_rag_grading import ClaimVerdict,grade_level3,evaluate_counterfactuals,CounterfactualObservation
24
+
25
+ PACK_ID="rag.full_rag.v1"
26
+ INTERFACE="sphynx.rag.full_rag.v1"
27
+ METRIC_RELEASE="rag.full_rag.source-level3.metric.v1"
28
+ GRADER_RELEASE="rag.full_rag.source-level3.grader.v1"
29
+ POLICY_VERSION="sphynx.rag.full_rag.policy.v1"
30
+ JUDGMENTS_SCHEMA="sphynx.rag.full_rag.judgments.v1"
31
+ _SEMANTIC=("answer_required_fact_coverage","answer_completeness","exact_value_unit_date_accuracy","faithfulness","unsupported_claim_rate","citation_support")
32
+
33
+
34
+ def build_full_rag_request(task,policy,context_ids):
35
+ pool=[{key:row[key] for key in ("candidate_id","input","input_sha256","dense_rank","dense_score")}
36
+ for row in task["candidates"]]
37
+ by_id={row["candidate_id"]:row for row in pool}
38
+ _require(type(context_ids) in (tuple,list) and all(type(cid) is str for cid in context_ids)
39
+ and len(context_ids)==len(set(context_ids)) and set(context_ids)<=set(by_id),"full-RAG context inventory differs")
40
+ body={"schema_version":"sphynx.rag.full_rag.request.v1","case_id":task["task_id"],
41
+ "customer_id":task["customer_id"],"query":task["query"],"conversation_context":task["conversation_context"],
42
+ "candidates":pool,"context_condition":policy["context_condition"],"context_candidate_ids":list(context_ids),
43
+ "context_text":"\n\n".join(by_id[cid]["input"] for cid in context_ids),
44
+ "policy":{key:value for key,value in policy.items() if key!="contexts"}}
45
+ body["input_digest"]=value_digest(body)
46
+ return _detach(body)
47
+
48
+
49
+ def validate_full_rag_response(response,request,*,expected_invocation=False):
50
+ fields={"schema_version","case_id","input_digest","trace"}
51
+ componentized=request["policy"]["execution_shape"]=="componentized"
52
+ if componentized:fields.add("stages")
53
+ _require(type(response) is dict and set(response)==fields,"full-RAG response fields differ")
54
+ _require(response["schema_version"]=="sphynx.rag.full_rag.response.v1" and response["case_id"]==request["case_id"]
55
+ and response["input_digest"]==request["input_digest"],"full-RAG response does not bind its request")
56
+ raw=response["trace"]
57
+ if componentized:
58
+ from ._componentized_full_rag import compose_full_rag
59
+ stages=response["stages"]
60
+ _require(type(stages) is dict and set(stages)=={"router","rewriter","generator"},"full-RAG component outputs differ")
61
+ derived=compose_full_rag(request,lambda kind,operation,payload:deepcopy(stages[kind]))
62
+ _require(_detach(derived)==response,"full-RAG trace differs from retained component outputs")
63
+ _require(type(raw) is dict and "expected_invocation" not in raw,"candidate trace cannot carry a reference action")
64
+ trace=deepcopy(raw)
65
+ _require(trace.get("task_id")==request["case_id"] and trace.get("as_uttered_query")==request["query"],"full-RAG trace request identity drift")
66
+ trace["expected_invocation"]=expected_invocation
67
+ unknown_latency=trace.get("latency_ms")=={}
68
+ if unknown_latency:trace["latency_ms"]={key:0.0 for key in ("routing","rewrite","retrieval","generation","total")}
69
+ else:_require(type(trace.get("latency_ms")) is dict and all(_number(v) and v>=0 for v in trace["latency_ms"].values()),"full-RAG latency values differ")
70
+ citations=trace.get("citations")
71
+ _require(type(citations) is list and all(type(row) is dict and type(row.get("start")) is int and type(row.get("end")) is int
72
+ and type(row.get("citation_id")) is str and row["citation_id"] and type(row.get("evidence_id")) is str and row["evidence_id"] for row in citations),"citation identities/offsets differ")
73
+ _require(len({row["citation_id"] for row in citations})==len(citations),"citation IDs must be unique")
74
+ parsed=validate_level3_trace(trace)
75
+ supplied=list(request["context_candidate_ids"])
76
+ _require(all(cid in supplied for cid in parsed.retrieved_candidate_ids)
77
+ and [cid for cid in supplied if cid in parsed.retrieved_candidate_ids]==list(parsed.retrieved_candidate_ids),
78
+ "trace retrieval is not an ordered subset of the supplied frozen contexts")
79
+ if request["policy"]["execution_shape"]=="componentized":
80
+ expected_context=tuple(supplied) if parsed.actual_rag_call_count else ()
81
+ _require(parsed.handed_context_ids==expected_context,"componentized context handoff differs from its fixed generator input")
82
+ if unknown_latency:parsed=replace(parsed,latency_ms={})
83
+ return parsed
84
+
85
+
86
+ def _policy(dataset,digest,candidate,*,context_condition,contexts,execution_shape,**options):
87
+ _require(context_condition in CONTEXT_CONDITIONS and execution_shape in ("componentized","opaque"),"full-RAG execution boundary differs")
88
+ generator=candidate.parameters.get("generator_digest");prompt=candidate.parameters.get("prompt_digest")
89
+ require_digest(generator,"generator_digest");require_digest(prompt,"prompt_digest")
90
+ policy=_policy_for_source(dataset,digest,**options)
91
+ policy.update(version=POLICY_VERSION,context_condition=context_condition,contexts=_detach(contexts),execution_shape=execution_shape,
92
+ generator_digest=generator,prompt_digest=prompt,execution_mode=candidate.execution_mode,
93
+ timing_authority="component_reported_unattested",semantic_review_policy="capture-bound-owner-judgments.v1")
94
+ return policy
95
+
96
+
97
+ def _identity(dataset,digest,tasks,policy):
98
+ fixed={k:v for k,v in policy.items() if k not in ("context_condition","contexts")}
99
+ semantic_refs=[{key:task[key] for key in ("task_id","should_call_rag","expected_behavior","evidence_groups")} for task in tasks]
100
+ condition="oracle" if policy["context_condition"]=="oracle" else "empty" if policy["context_condition"]=="empty" else "retrieved"
101
+ return ComparisonIdentity("rag_full_rag",PACK_ID,"v1","sable.rag.dataset.v3",digest,
102
+ value_digest([task["task_id"] for task in tasks]),value_digest(semantic_refs),value_digest(fixed),
103
+ METRIC_RELEASE,GRADER_RELEASE,policy["boundary_digest"] or value_digest({"boundary":policy["execution_shape"],"dataset":digest}),
104
+ context_condition=condition,generator_digest=policy["generator_digest"])
105
+
106
+
107
+ def run_full_rag(candidate,adapter,dataset_path,output_dir,*,expected_sha256,synthetic=False,
108
+ owner_authorized=False,pack_manifest_path=None,trials=1,attempts=1,case_ids=None,boundary_digest=None,
109
+ execution_shape="opaque",context_condition="dense",contexts=None):
110
+ _require(isinstance(candidate,CandidateConfiguration) and candidate.interface_version==INTERFACE,"full-RAG candidate interface differs")
111
+ _require(candidate.execution_mode in ("trusted_local","recorded"),"full-RAG mode requires an explicit local adapter or replay")
112
+ dataset=load_dataset_v3(dataset_path,expected_sha256=expected_sha256);tasks=_select_tasks(dataset,case_ids)
113
+ if contexts is None:
114
+ _require(context_condition in ("dense","empty"),"oracle/A0/challenger diagnostics need explicit frozen contexts")
115
+ contexts={task["task_id"]:[] if context_condition=="empty" else [row["candidate_id"] for row in sorted(task["candidates"],key=lambda row:row["dense_rank"])[:5]] for task in tasks}
116
+ _require(isinstance(contexts,Mapping) and set(contexts)=={task["task_id"] for task in tasks},"full-RAG context cohort differs")
117
+ policy=_policy(dataset,expected_sha256,candidate,context_condition=context_condition,contexts=contexts,execution_shape=execution_shape,
118
+ synthetic=synthetic,owner_authorized=owner_authorized,pack_manifest_path=pack_manifest_path,
119
+ trials=trials,attempts=attempts,boundary_digest=boundary_digest)
120
+ requests={task["task_id"]:build_full_rag_request(task,policy,contexts[task["task_id"]]) for task in tasks}
121
+ if context_condition=="empty":_require(all(not values for values in contexts.values()),"empty context diagnostics must actually be empty")
122
+ if candidate.execution_mode=="recorded":
123
+ _require(isinstance(adapter,Mapping) and candidate.parameters.get("recordings_digest")==value_digest(adapter),"full-RAG replay recording is not pinned")
124
+ adapter=_detach(adapter)
125
+ else:_require(callable(adapter),"full-RAG adapter must be callable")
126
+ observed={"approved_manifest_source":None if synthetic else Path(pack_manifest_path).read_text(encoding="utf-8"),"frozen_policy":policy}
127
+ _verify_approval(dataset,expected_sha256,policy,{"approved_manifest_source":observed["approved_manifest_source"]})
128
+ identity=_identity(dataset,expected_sha256,tasks,policy);ids=[task["task_id"] for task in tasks]
129
+ directory=create_run(output_dir,identity,candidate,ids);captures=[]
130
+ for index,task in enumerate(tasks):
131
+ request=requests[task["task_id"]]
132
+ for trial in range(1,trials+1):
133
+ for attempt in range(1,attempts+1):
134
+ prefix=f"cases/{index}/{trial}/{attempt}/";req=write_artifact(directory,prefix+"request.json",request)
135
+ response=None;failure=None;begin=perf_counter()
136
+ try:
137
+ raw=adapter[value_digest(request)] if candidate.execution_mode=="recorded" else adapter(_detach(request))
138
+ response=write_artifact(directory,prefix+"response.json",raw);validate_full_rag_response(raw,request)
139
+ except Exception:failure="invalid_response" if response is not None else "candidate_failure"
140
+ observation=write_artifact(directory,prefix+"observed.json",observed)
141
+ captures.append(TrackCaseCapture(task["task_id"],trial,attempt,req,response,observation,failure,(perf_counter()-begin)*1000))
142
+ return finish_run(directory,identity,candidate,ids,captures)
143
+
144
+
145
+ def _verified(run,dataset_path,digest):
146
+ dataset=load_dataset_v3(dataset_path,expected_sha256=digest);manifest=verify_track_run(run)
147
+ _require(manifest.candidate.interface_version==INTERFACE,"capture is not full-RAG")
148
+ tasks=_select_tasks(dataset,manifest.case_ids)
149
+ _require(manifest.captures[0].observed is not None,"full-RAG frozen control metadata is missing")
150
+ first_observed=read_artifact(run.path,manifest.captures[0].observed)
151
+ _require(type(first_observed) is dict and set(first_observed)=={"approved_manifest_source","frozen_policy"},"full-RAG control metadata fields differ")
152
+ policy=first_observed["frozen_policy"]
153
+ required={"version","synthetic","approved_manifest_digest","trials","attempts","selection","boundary_digest","context_condition",
154
+ "contexts","execution_shape","generator_digest","prompt_digest","execution_mode","timing_authority","semantic_review_policy"}
155
+ _require(type(policy) is dict and set(policy)==required and policy["version"]==POLICY_VERSION
156
+ and type(policy["synthetic"]) is bool and policy["selection"]=="first_success_per_trial"
157
+ and policy["context_condition"] in CONTEXT_CONDITIONS and policy["execution_shape"] in ("opaque","componentized")
158
+ and policy["execution_mode"]==manifest.candidate.execution_mode and policy["timing_authority"]=="component_reported_unattested"
159
+ and policy["semantic_review_policy"]=="capture-bound-owner-judgments.v1","full-RAG policy fields differ")
160
+ _require(all(type(policy[key]) is int and policy[key]>=1 for key in ("trials","attempts")),"full-RAG trial policy differs")
161
+ _require(policy["generator_digest"]==manifest.candidate.parameters["generator_digest"] and
162
+ policy["prompt_digest"]==manifest.candidate.parameters["prompt_digest"],"declared generator/prompt changed")
163
+ requests={task["task_id"]:build_full_rag_request(task,policy,policy["contexts"][task["task_id"]]) for task in tasks}
164
+ require_comparable(_identity(dataset,digest,tasks,policy),manifest.identity)
165
+ verify_track_run(run,expected_case_ids=manifest.case_ids,expected_requests=requests,trials=policy["trials"],attempts=policy["attempts"])
166
+ for capture in manifest.captures:
167
+ _require(capture.observed is not None,"full-RAG approval observation is missing")
168
+ observed=read_artifact(run.path,capture.observed)
169
+ _require(type(observed) is dict and set(observed)=={"approved_manifest_source","frozen_policy"}
170
+ and observed["frozen_policy"]==policy,"full-RAG frozen controls changed between cases")
171
+ _verify_approval(dataset,digest,policy,{"approved_manifest_source":observed["approved_manifest_source"]})
172
+ return manifest,tasks,policy,requests
173
+
174
+
175
+ def create_full_rag_review_template(run,dataset_path,*,expected_sha256):
176
+ manifest,tasks,policy,requests=_verified(run,dataset_path,expected_sha256)
177
+ rows=[]
178
+ for capture in manifest.captures:
179
+ if capture.failure is None and capture.response is not None:
180
+ rows.append({"case_id":capture.case_id,"trial_index":capture.trial_index,"attempt_index":capture.attempt_index,
181
+ "response_digest":capture.response.sha256,"required_facts":{},"exact_values":[],"claims":[],
182
+ "claims_exhaustive":False,"review_status":"pending"})
183
+ return {"schema_version":JUDGMENTS_SCHEMA,"capture_manifest_digest":run.manifest_sha256,"dataset_digest":expected_sha256,
184
+ "reviewer_id":None,"reviewed_at_utc":None,"authority":"pending","rows":rows}
185
+
186
+
187
+ def _reviews(value,run,digest,manifest,tasks):
188
+ if value is None:return {},None,None
189
+ raw=Path(value).read_bytes() if isinstance(value,(str,Path)) else None
190
+ review=strict_json(raw) if raw is not None else _detach(value)
191
+ _require(type(review) is dict and set(review)=={"schema_version","capture_manifest_digest","dataset_digest","reviewer_id","reviewed_at_utc","authority","rows"}
192
+ and review["schema_version"]==JUDGMENTS_SCHEMA and review["capture_manifest_digest"]==run.manifest_sha256
193
+ and review["dataset_digest"]==digest,"semantic judgments are not bound to this capture/dataset")
194
+ _require(type(review["reviewer_id"]) is str and review["reviewer_id"].strip() and type(review["reviewed_at_utc"]) is str
195
+ and re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z",review["reviewed_at_utc"])
196
+ and review["authority"] in ("owner_human_review","synthetic_fixture"),"semantic review identity/authority is incomplete")
197
+ _require(type(review["rows"]) is list,"semantic review rows differ")
198
+ first_policy=read_artifact(run.path,manifest.captures[0].request)["policy"]
199
+ _require(review["authority"]!="synthetic_fixture" or first_policy["synthetic"],
200
+ "synthetic judgments cannot approve a customer capture")
201
+ captures={(row.case_id,row.trial_index,row.attempt_index):row for row in manifest.captures};by_key={}
202
+ for row in review["rows"]:
203
+ fields={"case_id","trial_index","attempt_index","response_digest","required_facts","exact_values","claims","claims_exhaustive","review_status"}
204
+ _require(type(row) is dict and set(row)==fields and row["review_status"]=="reviewed" and type(row["claims_exhaustive"]) is bool,"semantic review row is incomplete")
205
+ key=(row["case_id"],row["trial_index"],row["attempt_index"]);capture=captures.get(key)
206
+ _require(key not in by_key and capture is not None and capture.failure is None and capture.response is not None
207
+ and row["response_digest"]==capture.response.sha256,"semantic review response membership/drift")
208
+ _require(type(row["required_facts"]) is dict and all(type(k) is str and k and type(v) is list and v
209
+ and all(type(phrase) is str and phrase.strip() for phrase in v) for k,v in row["required_facts"].items())
210
+ and type(row["exact_values"]) is list and all(type(v) is str and v.strip() for v in row["exact_values"])
211
+ and type(row["claims"]) is list,"semantic review facts/values/claims differ")
212
+ by_key[key]=row
213
+ truth={}
214
+ for row in review["rows"]:
215
+ criteria={"required_facts":row["required_facts"],"exact_values":row["exact_values"]}
216
+ previous=truth.get(row["case_id"])
217
+ _require(previous is None or previous==criteria,"semantic truth changed between trials/attempts")
218
+ truth[row["case_id"]]=criteria
219
+ policy_digest=value_digest({"schema_version":"sphynx.rag.full_rag.semantic-policy.v1",
220
+ "metric_release":METRIC_RELEASE,"authority":review["authority"],"truth":truth})
221
+ return by_key,value_digest(review),policy_digest
222
+
223
+
224
+ def _grade_metrics(run,dataset_path,digest,judgments=None):
225
+ manifest,tasks,policy,requests=_verified(run,dataset_path,digest)
226
+ reviews,review_digest,review_policy=_reviews(judgments,run,digest,manifest,tasks);rows=[]
227
+ for task in tasks:
228
+ for trial in range(1,policy["trials"]+1):
229
+ score=None;selected=None;reviewed=False
230
+ for capture in sorted(manifest.captures,key=lambda row:row.attempt_index):
231
+ if capture.case_id!=task["task_id"] or capture.trial_index!=trial or capture.failure is not None:continue
232
+ _require(capture.response is not None,"successful full-RAG capture lacks response")
233
+ trace=validate_full_rag_response(read_artifact(run.path,capture.response),requests[task["task_id"]],expected_invocation=task["should_call_rag"])
234
+ review=reviews.get((capture.case_id,trial,capture.attempt_index));claims=[]
235
+ if review is not None:
236
+ for raw in review["claims"]:
237
+ _require(type(raw) is dict and set(raw)=={"start","end","supported_by_evidence_ids"}
238
+ and type(raw["start"]) is int and type(raw["end"]) is int and type(raw["supported_by_evidence_ids"]) is list
239
+ and all(type(cid) is str for cid in raw["supported_by_evidence_ids"])
240
+ and len(set(raw["supported_by_evidence_ids"]))==len(raw["supported_by_evidence_ids"])
241
+ and set(raw["supported_by_evidence_ids"])<=set(trace.handed_context_ids),"claim support leaves exact handed evidence")
242
+ claims.append(ClaimVerdict(raw["start"],raw["end"],tuple(raw["supported_by_evidence_ids"])))
243
+ ordered=sorted(claims,key=lambda claim:(claim.start,claim.end))
244
+ _require(all(left.end<=right.start for left,right in zip(ordered,ordered[1:])),
245
+ "reviewed claims overlap or repeat answer spans")
246
+ current=grade_level3(task,trace,required_facts={} if review is None else review["required_facts"],
247
+ exact_values=[] if review is None else review["exact_values"],claims=claims)
248
+ if review is None:
249
+ for key in _SEMANTIC:current[key]=None
250
+ elif not review["claims_exhaustive"]:
251
+ for key in ("faithfulness","unsupported_claim_rate"):current[key]=None
252
+ if selected is None:score,selected,reviewed=current,capture,review is not None
253
+ rows.append({"case_id":task["task_id"],"trial_index":trial,"status":"failed" if selected is None else "measured",
254
+ "metrics":score,"semantic_reviewed":reviewed,"runner_wall_ms":None if selected is None else selected.runner_latency_ms,
255
+ "response_digest":None if selected is None else selected.response.sha256})
256
+ keys=("rag_call_true_positive","rag_call_false_positive","rag_call_false_negative","rewrite_preservation","evidence_handoff_completeness",
257
+ "answer_required_fact_coverage","exact_value_unit_date_accuracy","faithfulness","unsupported_claim_rate","citation_precision","citation_recall",
258
+ "citation_support","correct_abstention","answer_completeness","subject_error")
259
+ summary={}
260
+ for key in keys:
261
+ values=[row["metrics"][key] for row in rows if row["metrics"] is not None and row["metrics"][key] is not None]
262
+ summary[key]={"value":sum(values)/len(values) if len(values)==len(rows) else None,
263
+ "observed_mean":sum(values)/len(values) if values else None,"observed_count":len(values),
264
+ "eligible_count":len(rows),"missing_count":len(rows)-len(values)}
265
+ complete=all(row["metrics"] is not None for row in rows)
266
+ counts={key:sum(row["metrics"][key] for row in rows if row["metrics"] is not None)
267
+ for key in ("rag_call_true_positive","rag_call_false_positive","rag_call_false_negative")}
268
+ for key,denominator in (("rag_call_precision",counts["rag_call_true_positive"]+counts["rag_call_false_positive"]),
269
+ ("rag_call_recall",counts["rag_call_true_positive"]+counts["rag_call_false_negative"])):
270
+ summary[key]={"value":counts["rag_call_true_positive"]/denominator if complete and denominator else None,
271
+ "numerator":counts["rag_call_true_positive"],"denominator":denominator,
272
+ "complete":complete,"undefined":complete and not denominator,
273
+ "eligible_count":len(rows),"observed_count":sum(row["metrics"] is not None for row in rows),
274
+ "missing_count":sum(row["metrics"] is None for row in rows)}
275
+ return manifest,{"summary":summary,"cases":rows,"coverage":sum(row["metrics"] is not None for row in rows)/len(rows),
276
+ "execution_shape":policy["execution_shape"],"context_condition":policy["context_condition"],
277
+ "generator_digest":policy["generator_digest"],"prompt_digest":policy["prompt_digest"],
278
+ "execution_mode":policy["execution_mode"],"timing_authority":"component_reported_unattested",
279
+ "quality_scope":"synthetic_fixture" if policy["synthetic"] else "owner_approved",
280
+ "review_digest":review_digest,"judgment_policy_digest":review_policy},review_digest,review_policy
281
+
282
+
283
+ def grade_full_rag(run,dataset_path,*,expected_sha256,judgments=None):
284
+ manifest,metrics,review_digest,review_policy=_grade_metrics(run,dataset_path,expected_sha256,judgments)
285
+ if review_digest is not None:
286
+ value=strict_json(Path(judgments).read_bytes()) if isinstance(judgments,(str,Path)) else _detach(judgments)
287
+ relative="semantic-reviews/"+review_digest.removeprefix("sha256:")+".json"
288
+ if not (run.path/relative).exists():write_artifact(run.path,relative,value)
289
+ metrics["review_artifact"]=relative
290
+ return write_evaluation(run,replace(manifest.identity,judgment_policy_digest=review_policy),metrics,
291
+ judgment_artifact_digest=review_digest)
292
+
293
+
294
+ def report_full_rag(evaluation,dataset_path,*,expected_sha256):
295
+ report=verify_track_evaluation(evaluation).to_dict();review=None
296
+ if report["metrics"].get("review_artifact"):
297
+ from ._track_capture import safe_path
298
+ path=safe_path(evaluation.run.path,report["metrics"]["review_artifact"]);review=strict_json(path.read_bytes())
299
+ _require(value_digest(review)==report["judgment_artifact_digest"],"retained semantic review digest differs")
300
+ _,expected,review_digest,review_policy=_grade_metrics(evaluation.run,dataset_path,expected_sha256,review)
301
+ if review_digest is not None:expected["review_artifact"]=report["metrics"]["review_artifact"]
302
+ _require(report["metrics"]==expected,"full-RAG evaluation differs from retained trace/review")
303
+ _require(report["identity"]["judgment_policy_digest"]==review_policy,"semantic policy identity differs")
304
+ return report
305
+
306
+
307
+ def compare_full_rag(baseline,candidate,dataset_path,*,expected_sha256):
308
+ a=report_full_rag(baseline,dataset_path,expected_sha256=expected_sha256)
309
+ b=report_full_rag(candidate,dataset_path,expected_sha256=expected_sha256)
310
+ require_comparable(ComparisonIdentity.from_dict(a["identity"]),ComparisonIdentity.from_dict(b["identity"]))
311
+ _require(a["metrics"]["context_condition"]==b["metrics"]["context_condition"],"full-RAG context diagnostic differs")
312
+ _require([(row["case_id"],row["trial_index"]) for row in a["metrics"]["cases"]]==
313
+ [(row["case_id"],row["trial_index"]) for row in b["metrics"]["cases"]],"full-RAG paired cohort differs")
314
+ return {"schema_version":"sphynx.rag.full_rag.comparison.v1","baseline":a["evaluation_id"],"candidate":b["evaluation_id"],
315
+ "delta":{key:None if value["value"] is None or b["metrics"]["summary"][key]["value"] is None
316
+ else b["metrics"]["summary"][key]["value"]-value["value"] for key,value in a["metrics"]["summary"].items()},
317
+ "timing_comparable":False,"notice":"Review scopes and opaque/componentized boundaries are held fixed; local timings are not attested."}
318
+
319
+
320
+ def compare_full_rag_contexts(evaluations,dataset_path,*,expected_sha256):
321
+ _require(isinstance(evaluations,Mapping) and set(evaluations)==CONTEXT_CONDITIONS,"counterfactuals require exactly five fixed contexts")
322
+ reports={key:report_full_rag(value,dataset_path,expected_sha256=expected_sha256) for key,value in evaluations.items()}
323
+ base=reports["oracle"];identity=ComparisonIdentity.from_dict(base["identity"])
324
+ for condition,report in reports.items():
325
+ _require(report["metrics"]["context_condition"]==condition,"counterfactual condition alias changed")
326
+ require_comparable(identity,ComparisonIdentity.from_dict(report["identity"]),context_conditions=True)
327
+ _require(report["metrics"]["generator_digest"]==base["metrics"]["generator_digest"] and
328
+ report["metrics"]["prompt_digest"]==base["metrics"]["prompt_digest"],"counterfactual generator/prompt changed")
329
+ # This diagnostic uses retained coverage only. No generator/candidate is rerun.
330
+ rows={key:{(row["case_id"],row["trial_index"]):row for row in report["metrics"]["cases"]} for key,report in reports.items()}
331
+ _require(all(set(value)==set(rows["oracle"]) for value in rows.values()),"counterfactual task/trial cohorts differ")
332
+ result=[]
333
+ for case in rows["oracle"]:
334
+ coverage={key:rows[key][case]["metrics"]["answer_required_fact_coverage"] if rows[key][case]["metrics"] else None for key in CONTEXT_CONDITIONS}
335
+ def gap(a,b):return None if coverage[a] is None or coverage[b] is None else coverage[a]-coverage[b]
336
+ result.append({"case_id":case[0],"trial_index":case[1],"condition_coverage":coverage,
337
+ "retrieval_ranking_gap":gap("oracle","challenger"),"challenger_vs_a0":gap("challenger","a0"),
338
+ "a0_ranking_contribution":gap("a0","dense"),"rag_dependency":gap("challenger","empty")})
339
+ return {"schema_version":"sphynx.rag.full_rag.context-comparison.v1","cases":result,
340
+ "generator_digest":base["metrics"]["generator_digest"],"prompt_digest":base["metrics"]["prompt_digest"],
341
+ "notice":"Fixed-generator context diagnostic, separate from architecture selection; missing semantic truth remains unavailable."}
@@ -0,0 +1,18 @@
1
+ """Hosted submission preparation. Remote execution is not connected yet."""
2
+
3
+ from .contracts import (
4
+ CandidateConfig, CandidateFamily, HostedError, SelectedCandidateRepository,
5
+ parse_candidate_config, parse_candidate_family, parse_candidate_repository,
6
+ verify_candidate_repository,
7
+ )
8
+ from .registry import HostedRegistry, RepositoryRegistration, load_registry
9
+ from .submission import FrozenSubmission, GitHubReader, prepare_submission
10
+
11
+ __all__ = [
12
+ "CandidateConfig", "HostedError", "parse_candidate_config", "HostedRegistry",
13
+ "RepositoryRegistration", "load_registry", "FrozenSubmission", "GitHubReader",
14
+ "prepare_submission",
15
+ ]
16
+
17
+ __all__ += ["CandidateFamily", "SelectedCandidateRepository", "parse_candidate_family",
18
+ "parse_candidate_repository", "verify_candidate_repository"]